From 7ef2b849f256e4a5cd890d7a03cc49ebc04dd10c Mon Sep 17 00:00:00 2001 From: Brendan McAndrew Date: Tue, 17 Oct 2023 22:15:45 -0400 Subject: [PATCH 1/7] Wrap bboxes in list if list of float OR int --- pystac/collection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pystac/collection.py b/pystac/collection.py index 7b1def589..039479c11 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -40,6 +40,7 @@ C = TypeVar("C", bound="Collection") +Bboxes = list[list[Union[float, int]]] TemporalIntervals = Union[list[list[datetime]], list[list[Optional[datetime]]]] TemporalIntervalsLike = Union[ TemporalIntervals, list[datetime], list[Optional[datetime]] @@ -59,7 +60,7 @@ class SpatialExtent: Spatial Extent object. """ - bboxes: list[list[float]] + bboxes: Bboxes """A list of bboxes that represent the spatial extent of the collection. Each bbox can be 2D or 3D. The length of the bbox array must be 2*n where n is the number of dimensions. For example, a @@ -71,15 +72,18 @@ class SpatialExtent: def __init__( self, - bboxes: list[list[float]] | list[float], + bboxes: Bboxes | list[float | int], extra_fields: dict[str, Any] | None = None, ) -> None: + if not isinstance(bboxes, list): + raise TypeError("bboxes must be a list") + # A common mistake is to pass in a single bbox instead of a list of bboxes. # Account for this by transforming the input in that case. - if isinstance(bboxes, list) and isinstance(bboxes[0], float): - self.bboxes: list[list[float]] = [cast(list[float], bboxes)] + if isinstance(bboxes[0], (float, int)): + self.bboxes = [cast(list[float | int], bboxes)] else: - self.bboxes = cast(list[list[float]], bboxes) + self.bboxes = cast(Bboxes, bboxes) self.extra_fields = extra_fields or {} From 4647ed059d91c75e09957b700c69dba6f35e9e1f Mon Sep 17 00:00:00 2001 From: Brendan McAndrew Date: Tue, 17 Oct 2023 22:20:37 -0400 Subject: [PATCH 2/7] Wrap intervals in list if not list of lists --- pystac/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystac/collection.py b/pystac/collection.py index 039479c11..901d60642 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -207,7 +207,7 @@ def __init__( # list of intervals. Account for this by transforming the input # in that case. if isinstance(intervals, list) and isinstance(intervals[0], datetime): - self.intervals = intervals + self.intervals = [intervals] else: self.intervals = intervals From 3d68041e0715c33d1629929356d606ce2d011d11 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew Date: Tue, 17 Oct 2023 23:17:54 -0400 Subject: [PATCH 3/7] chore: add #1268 to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a65fb8e..158f0e854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Interactions between **pytest-recording** and the validator schema cache ([#1242](https://github.com/stac-utils/pystac/pull/1242)) - Call `registry` when instantiating `Draft7Validator` ([#1240](https://github.com/stac-utils/pystac/pull/1240)) - Migration for the classification, datacube, table, and timestamps extensions ([#1258](https://github.com/stac-utils/pystac/pull/1258)) +- Handling of `bboxes` and `intervals` arguments to `SpatialExtent` and `TemporalExtent`, respectively ([#1268](https://github.com/stac-utils/pystac/pull/1268)) ### Removed From ff5e763d2e1469eb9b40b810e5f68af49cec2536 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:32:25 -0400 Subject: [PATCH 4/7] Use union in cast --- pystac/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystac/collection.py b/pystac/collection.py index 901d60642..c21cbbb35 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -81,7 +81,7 @@ def __init__( # A common mistake is to pass in a single bbox instead of a list of bboxes. # Account for this by transforming the input in that case. if isinstance(bboxes[0], (float, int)): - self.bboxes = [cast(list[float | int], bboxes)] + self.bboxes = [cast(list[Union[float, int]], bboxes)] else: self.bboxes = cast(Bboxes, bboxes) From 8917e7b12a9f8ec6467245d579a1adb7c90f15f9 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:57:02 -0400 Subject: [PATCH 5/7] Improve TemporalExtent type checks --- pystac/collection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pystac/collection.py b/pystac/collection.py index c21cbbb35..e5e0696c2 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -200,16 +200,18 @@ class TemporalExtent: def __init__( self, - intervals: TemporalIntervals, + intervals: TemporalIntervals | list[datetime | None], extra_fields: dict[str, Any] | None = None, ): + if not isinstance(intervals, list): + raise TypeError("intervals must be a list") # A common mistake is to pass in a single interval instead of a # list of intervals. Account for this by transforming the input # in that case. - if isinstance(intervals, list) and isinstance(intervals[0], datetime): - self.intervals = [intervals] + if isinstance(intervals[0], datetime) or intervals[0] is None: + self.intervals = [cast(list[Optional[datetime]], intervals)] else: - self.intervals = intervals + self.intervals = cast(TemporalIntervals, intervals) self.extra_fields = extra_fields or {} From aafd5eec902145925c408b5123138dabde087703 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:59:28 -0400 Subject: [PATCH 6/7] Add tests for TemporalExtent --- tests/test_collection.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_collection.py b/tests/test_collection.py index ea51fbd67..0374d790c 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -381,6 +381,31 @@ def test_temporal_extent_init_typing(self) -> None: _ = TemporalExtent([[start_datetime, end_datetime]]) + @pytest.mark.block_network() + def test_temporal_extent_allows_single_interval(self) -> None: + start_datetime = str_to_datetime("2022-01-01T00:00:00Z") + end_datetime = str_to_datetime("2022-01-31T23:59:59Z") + + interval = [start_datetime, end_datetime] + temporal_extent = TemporalExtent(intervals=interval) # type: ignore + + self.assertEqual(temporal_extent.intervals, [interval]) + + @pytest.mark.block_network() + def test_temporal_extent_allows_single_interval_open_start(self) -> None: + end_datetime = str_to_datetime("2022-01-31T23:59:59Z") + + interval = [None, end_datetime] + temporal_extent = TemporalExtent(intervals=interval) + + self.assertEqual(temporal_extent.intervals, [interval]) + + @pytest.mark.block_network() + def test_temporal_extent_non_list_intervals_fails(self) -> None: + with pytest.raises(TypeError): + # Pass in non-list intervals + _ = TemporalExtent(intervals=1) # type: ignore + @pytest.mark.block_network() def test_spatial_allows_single_bbox(self) -> None: temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]]) From cb3ae33a6d7bccbe081a469920589a0f6c754182 Mon Sep 17 00:00:00 2001 From: Brendan McAndrew <19274445+bmcandr@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:00:35 -0400 Subject: [PATCH 7/7] Test SpatialExtent raises TypeError on non-list bboxes arg --- tests/test_collection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_collection.py b/tests/test_collection.py index 0374d790c..96c2cba1a 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -424,6 +424,12 @@ def test_spatial_allows_single_bbox(self) -> None: collection.validate() + @pytest.mark.block_network() + def test_spatial_extent_non_list_bboxes_fails(self) -> None: + with pytest.raises(TypeError): + # Pass in non-list bboxes + _ = SpatialExtent(bboxes=1) # type: ignore + def test_from_items(self) -> None: item1 = Item( id="test-item-1",