Skip to content

Commit d6531a5

Browse files
jsignellircwaves
andauthored
Add docs and hint about extension summaries (#1157)
* Add example using summaries in docs * Include hint about summaries if appropriate * Update changelog * Update pystac/extensions/base.py Co-authored-by: Ian Cooke <ircwaves@users.noreply.github.com> * Add tests for MGRS and classification, clean up punctuation --------- Co-authored-by: Ian Cooke <ircwaves@users.noreply.github.com>
1 parent 9e86cda commit d6531a5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+106
-90
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
- `get_derived_from`, `add_derived_from` and `remove_derived_from` to Items ([#1136](https://github.com/stac-utils/pystac/pull/1136))
1818
- `ItemEOExtension.get_assets` for getting assets filtered on band `name` or `common_name` ([#1140](https://github.com/stac-utils/pystac/pull/1140))
1919
- `max_items` and `recursive` to `Catalog.validate_all` ([#1141](https://github.com/stac-utils/pystac/pull/1141))
20-
- `KML` as a built in media type ([#1127](https://github.com/stac-utils/pystac/issues/1127))
20+
- `KML` as a built in media type ([#1127](https://github.com/stac-utils/pystac/issues/1127))
2121

2222
### Changed
2323

@@ -35,6 +35,7 @@
3535
- Use `id` in STACTypeError instead of entire dict ([#1126](https://github.com/stac-utils/pystac/pull/1126))
3636
- Make sure that `get_items` is backwards compatible ([#1139](https://github.com/stac-utils/pystac/pull/1139))
3737
- Make `_repr_html_` look like `_repr_json_` output ([#1142](https://github.com/stac-utils/pystac/pull/1142))
38+
- Improved error message when `.ext` is called on a Collection ([#1157](https://github.com/stac-utils/pystac/pull/1157))
3839

3940
### Deprecated
4041

docs/concepts.rst

+32-7
Original file line numberDiff line numberDiff line change
@@ -482,14 +482,15 @@ Extension <eo>`, you can access the properties associated with that extension us
482482
import pystac
483483
from pystac.extensions.eo import EOExtension
484484
485-
item = Item(...) # See docs for creating an Item
485+
item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json")
486486
487487
# Check that the Item implements the EO Extension
488488
if EOExtension.has_extension(item):
489489
eo_ext = EOExtension.ext(item)
490490
491491
bands = eo_ext.bands
492492
cloud_cover = eo_ext.cloud_cover
493+
snow_cover = eo_ext.snow_cover
493494
...
494495
495496
.. note:: The ``ext`` method will raise an :exc:`~pystac.ExtensionNotImplemented`
@@ -511,7 +512,7 @@ can therefore mutate those properties.* For instance:
511512

512513
.. code-block:: python
513514
514-
item = Item.from_file("tests/data-files/eo/eo-landsat-example.json")
515+
item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json")
515516
print(item.properties["eo:cloud_cover"])
516517
# 78
517518
@@ -563,7 +564,7 @@ extended.
563564
.. code-block:: python
564565
565566
# Load a basic item without any extensions
566-
item = Item.from_file("tests/data-files/item/sample-item.json")
567+
item = pystac.Item.from_file("tests/data-files/item/sample-item.json")
567568
print(item.stac_extensions)
568569
# []
569570
@@ -575,14 +576,38 @@ extended.
575576
Extended Summaries
576577
------------------
577578

578-
Extension classes like :class:`~pystac.extensions.eo.EOExtension` may also provide a
579-
``summaries`` static method that can be used to extend the Collection summaries. This
580-
method returns a class inheriting from
579+
Extension classes like :class:`~pystac.extensions.projection.ProjectionExtension` may
580+
also provide a ``summaries`` static method that can be used to extend the Collection
581+
summaries. This method returns a class inheriting from
581582
:class:`pystac.extensions.base.SummariesExtension` that provides tools for summarizing
582583
the properties defined by that extension. These classes also hold a reference to the
583584
Collection's :class:`pystac.Summaries` instance in the ``summaries`` attribute.
584585

585-
See :class:`pystac.extensions.eo.SummariesEOExtension` for an example implementation.
586+
587+
.. code-block:: python
588+
589+
import pystac
590+
from pystac.extensions.projection import ProjectionExtension
591+
592+
# Load a collection that does not implement the Projection extension
593+
collection = pystac.Collection.from_file(
594+
"tests/data-files/examples/1.0.0/collection.json"
595+
)
596+
597+
# Add Projection extension summaries to the collection
598+
proj = ProjectionExtension.summaries(collection, add_if_missing=True)
599+
print(collection.stac_extensions)
600+
# [
601+
# ....,
602+
# 'https://stac-extensions.github.io/projection/v1.1.0/schema.json'
603+
# ]
604+
605+
# Set the values for various extension fields
606+
proj.epsg = [4326]
607+
collection_as_dict = collection.to_dict()
608+
collection_as_dict["summaries"]["proj:epsg"]
609+
# [4326]
610+
586611
587612
Item Asset properties
588613
=====================

pystac/extensions/base.py

+8
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,11 @@ def validate_has_extension(cls, obj: S, add_if_missing: bool = False) -> None:
191191
raise pystac.ExtensionNotImplemented(
192192
f"Could not find extension schema URI {cls.get_schema_uri()} in object."
193193
)
194+
195+
@classmethod
196+
def _ext_error_message(cls, obj: Any) -> str:
197+
contents = [f"{cls.__name__} does not apply to type '{type(obj).__name__}'"]
198+
if hasattr(cls, "summaries") and isinstance(obj, pystac.Collection):
199+
hint = f"Hint: Did you mean to use `{cls.__name__}.summaries` instead?"
200+
contents.append(hint)
201+
return ". ".join(contents)

pystac/extensions/classification.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -542,12 +542,8 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]
542542
return cast(
543543
ClassificationExtension[T], RasterBandClassificationExtension(obj)
544544
)
545-
546545
else:
547-
raise pystac.ExtensionTypeError(
548-
"Classification extension does not apply to type "
549-
f"'{type(obj).__name__}'"
550-
)
546+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
551547

552548
@classmethod
553549
def summaries(

pystac/extensions/datacube.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -602,9 +602,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> DatacubeExtension[T]:
602602
cls.validate_owner_has_extension(obj, add_if_missing)
603603
return cast(DatacubeExtension[T], AssetDatacubeExtension(obj))
604604
else:
605-
raise pystac.ExtensionTypeError(
606-
f"Datacube extension does not apply to type '{type(obj).__name__}'"
607-
)
605+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
608606

609607

610608
class CollectionDatacubeExtension(DatacubeExtension[pystac.Collection]):

pystac/extensions/eo.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]:
406406
cls.validate_owner_has_extension(obj, add_if_missing)
407407
return cast(EOExtension[T], AssetEOExtension(obj))
408408
else:
409-
raise pystac.ExtensionTypeError(
410-
f"EO extension does not apply to type '{type(obj).__name__}'"
411-
)
409+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
412410

413411
@classmethod
414412
def summaries(

pystac/extensions/file.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,7 @@ def ext(cls, obj: pystac.Asset, add_if_missing: bool = False) -> FileExtension:
227227
cls.validate_owner_has_extension(obj, add_if_missing)
228228
return cls(obj)
229229
else:
230-
raise pystac.ExtensionTypeError(
231-
f"File Info extension does not apply to type '{type(obj).__name__}'"
232-
)
230+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
233231

234232

235233
class FileExtensionHooks(ExtensionHooks):

pystac/extensions/grid.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,7 @@ def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> GridExtension:
103103
cls.validate_has_extension(obj, add_if_missing)
104104
return GridExtension(obj)
105105
else:
106-
raise pystac.ExtensionTypeError(
107-
f"Grid Extension does not apply to type '{type(obj).__name__}'"
108-
)
106+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
109107

110108

111109
class GridExtensionHooks(ExtensionHooks):

pystac/extensions/item_assets.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,7 @@ def ext(
248248
cls.validate_has_extension(obj, add_if_missing)
249249
return cls(obj)
250250
else:
251-
raise pystac.ExtensionTypeError(
252-
f"Item Assets extension does not apply to type '{type(obj).__name__}'"
253-
)
251+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
254252

255253

256254
class ItemAssetsExtensionHooks(ExtensionHooks):

pystac/extensions/label.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,7 @@ def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> LabelExtension:
709709
cls.validate_has_extension(obj, add_if_missing)
710710
return cls(obj)
711711
else:
712-
raise pystac.ExtensionTypeError(
713-
f"Label extension does not apply to type '{type(obj).__name__}'"
714-
)
712+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
715713

716714
@classmethod
717715
def summaries(

pystac/extensions/mgrs.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,7 @@ def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "MgrsExtension":
235235
cls.validate_has_extension(obj, add_if_missing)
236236
return MgrsExtension(obj)
237237
else:
238-
raise pystac.ExtensionTypeError(
239-
f"MGRS Extension does not apply to type '{type(obj).__name__}'"
240-
)
238+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
241239

242240

243241
class MgrsExtensionHooks(ExtensionHooks):

pystac/extensions/pointcloud.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> PointcloudExtension[T]:
458458
cls.validate_owner_has_extension(obj, add_if_missing)
459459
return cast(PointcloudExtension[T], AssetPointcloudExtension(obj))
460460
else:
461-
raise pystac.ExtensionTypeError(
462-
f"Pointcloud extension does not apply to type '{type(obj).__name__}'"
463-
)
461+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
464462

465463
@classmethod
466464
def summaries(

pystac/extensions/projection.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ProjectionExtension[T]:
286286
cls.validate_owner_has_extension(obj, add_if_missing)
287287
return cast(ProjectionExtension[T], AssetProjectionExtension(obj))
288288
else:
289-
raise pystac.ExtensionTypeError(
290-
f"Projection extension does not apply to type '{type(obj).__name__}'"
291-
)
289+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
292290

293291
@classmethod
294292
def summaries(

pystac/extensions/raster.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -737,9 +737,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> RasterExtension[T]:
737737
)
738738
return cast(RasterExtension[T], ItemAssetsRasterExtension(obj))
739739
else:
740-
raise pystac.ExtensionTypeError(
741-
f"Raster extension does not apply to type '{type(obj).__name__}'"
742-
)
740+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
743741

744742
@classmethod
745743
def summaries(

pystac/extensions/sar.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> SarExtension[T]:
322322
cls.validate_owner_has_extension(obj, add_if_missing)
323323
return cast(SarExtension[T], AssetSarExtension(obj))
324324
else:
325-
raise pystac.ExtensionTypeError(
326-
f"SAR extension does not apply to type '{type(obj).__name__}'"
327-
)
325+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
328326

329327
@classmethod
330328
def summaries(

pystac/extensions/sat.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> SatExtension[T]:
153153
cls.validate_owner_has_extension(obj, add_if_missing)
154154
return cast(SatExtension[T], AssetSatExtension(obj))
155155
else:
156-
raise pystac.ExtensionTypeError(
157-
f"Satellite extension does not apply to type '{type(obj).__name__}'"
158-
)
156+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
159157

160158
@classmethod
161159
def summaries(

pystac/extensions/scientific.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ScientificExtension[T]:
244244
cls.validate_has_extension(obj, add_if_missing)
245245
return cast(ScientificExtension[T], ItemScientificExtension(obj))
246246
else:
247-
raise pystac.ExtensionTypeError(
248-
f"Scientific extension does not apply to type '{type(obj).__name__}'"
249-
)
247+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
250248

251249
@classmethod
252250
def summaries(

pystac/extensions/storage.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> StorageExtension[T]:
155155
cls.validate_owner_has_extension(obj, add_if_missing)
156156
return cast(StorageExtension[T], AssetStorageExtension(obj))
157157
else:
158-
raise pystac.ExtensionTypeError(
159-
f"StorageExtension does not apply to type '{type(obj).__name__}'"
160-
)
158+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
161159

162160
@classmethod
163161
def summaries(

pystac/extensions/table.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> TableExtension[T]:
161161
cls.validate_owner_has_extension(obj, add_if_missing)
162162
return cast(TableExtension[T], AssetTableExtension(obj))
163163
else:
164-
raise pystac.ExtensionTypeError(
165-
f"Table extension does not apply to type '{type(obj).__name__}'"
166-
)
164+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
167165

168166
@property
169167
def columns(self) -> Optional[List[Column]]:

pystac/extensions/timestamps.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> TimestampsExtension[T]:
134134
cls.validate_owner_has_extension(obj, add_if_missing)
135135
return cast(TimestampsExtension[T], AssetTimestampsExtension(obj))
136136
else:
137-
raise pystac.ExtensionTypeError(
138-
f"Timestamps extension does not apply to type '{type(obj).__name__}'"
139-
)
137+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
140138

141139
@classmethod
142140
def summaries(

pystac/extensions/version.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> VersionExtension[T]:
219219
cls.validate_has_extension(obj, add_if_missing)
220220
return cast(VersionExtension[T], ItemVersionExtension(obj))
221221
else:
222-
raise pystac.ExtensionTypeError(
223-
f"Version extension does not apply to type '{type(obj).__name__}'"
224-
)
222+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
225223

226224

227225
class CollectionVersionExtension(VersionExtension[pystac.Collection]):

pystac/extensions/view.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ViewExtension[T]:
163163
cls.validate_owner_has_extension(obj, add_if_missing)
164164
return cast(ViewExtension[T], AssetViewExtension(obj))
165165
else:
166-
raise pystac.ExtensionTypeError(
167-
f"View extension does not apply to type '{type(obj).__name__}'"
168-
)
166+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
169167

170168
@classmethod
171169
def summaries(

tests/data-files/examples/1.0.0/collection.json

-4
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@
6060
"minimum": 1.2,
6161
"maximum": 1.2
6262
},
63-
"proj:epsg": {
64-
"minimum": 32659,
65-
"maximum": 32659
66-
},
6763
"view:sun_elevation": {
6864
"minimum": 54.9,
6965
"maximum": 54.9

tests/extensions/test_classification.py

+9
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ def test_ext_raises_if_item_does_not_conform(plain_item: Item) -> None:
107107
ClassificationExtension.ext(plain_item)
108108

109109

110+
def test_ext_raises_on_collection(collection: pystac.Collection) -> None:
111+
with pytest.raises(
112+
pystac.errors.ExtensionTypeError,
113+
match="ClassificationExtension does not apply to type 'Collection'",
114+
) as e:
115+
ClassificationExtension.ext(collection) # type:ignore
116+
assert "Hint" in str(e.value)
117+
118+
110119
def test_apply_bitfields(plain_item: Item) -> None:
111120
ClassificationExtension.add_to(plain_item)
112121
ClassificationExtension.ext(plain_item).apply(

tests/extensions/test_custom.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> "CustomExtension[T]":
6666
cls.validate_has_extension(obj, add_if_missing)
6767
return cast(CustomExtension[T], CatalogCustomExtension(obj))
6868

69-
raise pystac.ExtensionTypeError(
70-
f"Custom extension does not apply to {type(obj).__name__}"
71-
)
69+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
7270

7371

7472
class CatalogCustomExtension(CustomExtension[pystac.Catalog]):

tests/extensions/test_datacube.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_should_raise_exception_when_passing_invalid_extension_object(
5757
) -> None:
5858
self.assertRaisesRegex(
5959
ExtensionTypeError,
60-
r"^Datacube extension does not apply to type 'object'$",
60+
r"^DatacubeExtension does not apply to type 'object'$",
6161
DatacubeExtension.ext,
6262
object(),
6363
)

tests/extensions/test_eo.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def test_should_raise_exception_when_passing_invalid_extension_object(
342342
) -> None:
343343
self.assertRaisesRegex(
344344
ExtensionTypeError,
345-
r"^EO extension does not apply to type 'object'$",
345+
r"^EOExtension does not apply to type 'object'$",
346346
EOExtension.ext,
347347
object(),
348348
)
@@ -472,3 +472,13 @@ def test_get_assets_works_even_if_band_info_is_incomplete(
472472

473473
assets = eo_ext.get_assets(common_name=common_name) # type:ignore
474474
assert len(assets) == 0
475+
476+
477+
def test_exception_should_include_hint_if_obj_is_collection(
478+
collection: pystac.Collection,
479+
) -> None:
480+
with pytest.raises(
481+
ExtensionTypeError,
482+
match="Hint: Did you mean to use `EOExtension.summaries` instead?",
483+
):
484+
EOExtension.ext(collection) # type:ignore

tests/extensions/test_file.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def test_should_raise_exception_when_passing_invalid_extension_object(
232232
) -> None:
233233
self.assertRaisesRegex(
234234
ExtensionTypeError,
235-
r"^File Info extension does not apply to type 'object'$",
235+
r"^FileExtension does not apply to type 'object'$",
236236
FileExtension.ext,
237237
object(),
238238
)

0 commit comments

Comments
 (0)