From f1f02a2066f95c8bb2630db4bc25de38f8696e08 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 09:49:09 -0400 Subject: [PATCH 1/8] Use cls argument in from_dict methods --- pystac/catalog.py | 4 ++-- pystac/collection.py | 2 +- tests/test_catalog.py | 19 +++++++++++++++++++ tests/test_collection.py | 19 +++++++++++++++++++ tests/test_item.py | 19 +++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/pystac/catalog.py b/pystac/catalog.py index 40d6c7947..1839ec1a1 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -904,7 +904,7 @@ def from_dict( if migrate: result = pystac.read_dict(d, href=href, root=root) if not isinstance(result, Catalog): - raise pystac.STACError(f"{result} is not a Catalog") + raise pystac.STACTypeError(f"{result} is not a Catalog") return result catalog_type = CatalogType.determine_type(d) @@ -919,7 +919,7 @@ def from_dict( d.pop("stac_version") - cat = Catalog( + cat = cls( id=id, description=description, title=title, diff --git a/pystac/collection.py b/pystac/collection.py index e68b06e09..22a8be857 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -610,7 +610,7 @@ def from_dict( d.pop("stac_version") - collection = Collection( + collection = cls( id=id, description=description, extent=extent, diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 76b0a1c1a..cce64f111 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1107,3 +1107,22 @@ def test_full_copy_4(self) -> None: ].get_absolute_href() assert href is not None self.assertTrue(os.path.exists(href)) + + +class CatalogSubClassTest(unittest.TestCase): + """This tests cases related to creating classes inheriting from pystac.Catalog to + ensure that inheritance, class methods, etc. function as expected.""" + + TEST_CASE_1 = TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + + class BasicCustomCatalog(pystac.Catalog): + pass + + def setUp(self) -> None: + self.stac_io = pystac.StacIO.default() + + def test_from_dict_returns_subclass(self) -> None: + + catalog_dict = self.stac_io.read_json(self.TEST_CASE_1) + custom_catalog = self.BasicCustomCatalog.from_dict(catalog_dict) + self.assertIsInstance(custom_catalog, self.BasicCustomCatalog) diff --git a/tests/test_collection.py b/tests/test_collection.py index 29972410e..e8ebf9e2e 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -264,3 +264,22 @@ def test_from_items(self) -> None: self.assertEqual(interval[0], datetime(2000, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)) self.assertEqual(interval[1], datetime(2001, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)) + + +class CollectionSubClassTest(unittest.TestCase): + """This tests cases related to creating classes inheriting from pystac.Catalog to + ensure that inheritance, class methods, etc. function as expected.""" + + MULTI_EXTENT = TestCases.get_path("data-files/collections/multi-extent.json") + + class BasicCustomCollection(pystac.Collection): + pass + + def setUp(self) -> None: + self.stac_io = pystac.StacIO.default() + + def test_from_dict_returns_subclass(self) -> None: + + collection_dict = self.stac_io.read_json(self.MULTI_EXTENT) + custom_collection = self.BasicCustomCollection.from_dict(collection_dict) + self.assertIsInstance(custom_collection, self.BasicCustomCollection) diff --git a/tests/test_item.py b/tests/test_item.py index d771a9c55..36b4022f9 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -698,3 +698,22 @@ def test_asset_updated(self) -> None: new_a1_value = cm.get_updated(item.assets["analytic"]) self.assertEqual(new_a1_value, set_value) self.assertEqual(cm.updated, item_value) + + +class ItemSubClassTest(unittest.TestCase): + """This tests cases related to creating classes inheriting from pystac.Catalog to + ensure that inheritance, class methods, etc. function as expected.""" + + SAMPLE_ITEM = TestCases.get_path("data-files/item/sample-item.json") + + class BasicCustomItem(pystac.Item): + pass + + def setUp(self) -> None: + self.stac_io = pystac.StacIO.default() + + def test_from_dict_returns_subclass(self) -> None: + + item_dict = self.stac_io.read_json(self.SAMPLE_ITEM) + custom_item = self.BasicCustomItem.from_dict(item_dict) + self.assertIsInstance(custom_item, self.BasicCustomItem) From 6e0785b45960c178c7b73039220589ac6f4723ac Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 10:53:41 -0400 Subject: [PATCH 2/8] Return calling class in from_file --- pystac/__init__.py | 13 ++++++- pystac/catalog.py | 5 +++ pystac/serialization/__init__.py | 46 ------------------------ pystac/stac_io.py | 61 ++++++++++++++++++++++++++++---- pystac/stac_object.py | 7 +++- tests/test_catalog.py | 7 +++- tests/test_collection.py | 7 +++- tests/test_item.py | 7 +++- 8 files changed, 95 insertions(+), 58 deletions(-) diff --git a/pystac/__init__.py b/pystac/__init__.py index 749fcfb63..1aa7f45af 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -94,7 +94,18 @@ def read_file(href: str) -> STACObject: a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_file ` """ - return STACObject.from_file(href) + stac_io = StacIO.default() + d = stac_io.read_json(href) + typ = pystac.serialization.identify.identify_stac_object_type(d) + + if typ == STACObjectType.CATALOG: + return Catalog.from_file(href) + elif typ == STACObjectType.COLLECTION: + return Collection.from_file(href) + elif typ == STACObjectType.ITEM: + return Item.from_file(href) + else: + raise STACTypeError(f"Cannot read file of type {typ}") def write_file( diff --git a/pystac/catalog.py b/pystac/catalog.py index 1839ec1a1..7b9367808 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -946,7 +946,12 @@ def full_copy( @classmethod def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Catalog": + if stac_io is None: + stac_io = pystac.StacIO.default() + result = super().from_file(href, stac_io) if not isinstance(result, Catalog): raise pystac.STACTypeError(f"{result} is not a {Catalog}.") + result._stac_io = stac_io + return result diff --git a/pystac/serialization/__init__.py b/pystac/serialization/__init__.py index e50412fd3..5766821c0 100644 --- a/pystac/serialization/__init__.py +++ b/pystac/serialization/__init__.py @@ -1,7 +1,4 @@ # flake8: noqa -from typing import Any, Dict, Optional, TYPE_CHECKING - -import pystac from pystac.serialization.identify import ( STACVersionRange, identify_stac_object, @@ -9,46 +6,3 @@ ) from pystac.serialization.common_properties import merge_common_properties from pystac.serialization.migrate import migrate_to_latest - -if TYPE_CHECKING: - from pystac.stac_object import STACObject - from pystac.catalog import Catalog - - -def stac_object_from_dict( - d: Dict[str, Any], href: Optional[str] = None, root: Optional["Catalog"] = None -) -> "STACObject": - """Determines how to deserialize a dictionary into a STAC object. - - Args: - d : The dict to parse. - href : Optional href that is the file location of the object being - parsed. - root : Optional root of the catalog for this object. - If provided, the root's resolved object cache can be used to search for - previously resolved instances of the STAC object. - - Note: This is used internally in StacIO instances to deserialize STAC Objects. - """ - if identify_stac_object_type(d) == pystac.STACObjectType.ITEM: - collection_cache = None - if root is not None: - collection_cache = root._resolved_objects.as_collection_cache() - - # Merge common properties in case this is an older STAC object. - merge_common_properties(d, json_href=href, collection_cache=collection_cache) - - info = identify_stac_object(d) - - d = migrate_to_latest(d, info) - - if info.object_type == pystac.STACObjectType.CATALOG: - return pystac.Catalog.from_dict(d, href=href, root=root, migrate=False) - - if info.object_type == pystac.STACObjectType.COLLECTION: - return pystac.Collection.from_dict(d, href=href, root=root, migrate=False) - - if info.object_type == pystac.STACObjectType.ITEM: - return pystac.Item.from_dict(d, href=href, root=root, migrate=False) - - raise pystac.STACTypeError(f"Unknown STAC object type {info.object_type}") diff --git a/pystac/stac_io.py b/pystac/stac_io.py index 5dfdb4d7f..f5bee808e 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -19,7 +19,12 @@ import pystac from pystac.utils import safe_urlparse -import pystac.serialization +from pystac.serialization import ( + merge_common_properties, + identify_stac_object_type, + identify_stac_object, + migrate_to_latest, +) # Use orjson if available try: @@ -95,12 +100,31 @@ def stac_object_from_dict( href: Optional[str] = None, root: Optional["Catalog_Type"] = None, ) -> "STACObject_Type": - result = pystac.serialization.stac_object_from_dict(d, href, root) - if isinstance(result, pystac.Catalog): - # Set the stac_io instance for usage by io operations - # where this catalog is the root. + if identify_stac_object_type(d) == pystac.STACObjectType.ITEM: + collection_cache = None + if root is not None: + collection_cache = root._resolved_objects.as_collection_cache() + + # Merge common properties in case this is an older STAC object. + merge_common_properties( + d, json_href=href, collection_cache=collection_cache + ) + + info = identify_stac_object(d) + d = migrate_to_latest(d, info) + + if info.object_type == pystac.STACObjectType.CATALOG: + result = pystac.Catalog.from_dict(d, href=href, root=root, migrate=False) result._stac_io = self - return result + return result + + if info.object_type == pystac.STACObjectType.COLLECTION: + return pystac.Collection.from_dict(d, href=href, root=root, migrate=False) + + if info.object_type == pystac.STACObjectType.ITEM: + return pystac.Item.from_dict(d, href=href, root=root, migrate=False) + + raise ValueError(f"Unknown STAC object type {info.object_type}") def read_json( self, source: Union[str, "Link_Type"], *args: Any, **kwargs: Any @@ -302,7 +326,30 @@ def stac_object_from_dict( root: Optional["Catalog_Type"] = None, ) -> "STACObject_Type": STAC_IO.issue_deprecation_warning() - return pystac.serialization.stac_object_from_dict(d, href, root) + if identify_stac_object_type(d) == pystac.STACObjectType.ITEM: + collection_cache = None + if root is not None: + collection_cache = root._resolved_objects.as_collection_cache() + + # Merge common properties in case this is an older STAC object. + merge_common_properties( + d, json_href=href, collection_cache=collection_cache + ) + + info = identify_stac_object(d) + + d = migrate_to_latest(d, info) + + if info.object_type == pystac.STACObjectType.CATALOG: + return pystac.Catalog.from_dict(d, href=href, root=root, migrate=False) + + if info.object_type == pystac.STACObjectType.COLLECTION: + return pystac.Collection.from_dict(d, href=href, root=root, migrate=False) + + if info.object_type == pystac.STACObjectType.ITEM: + return pystac.Item.from_dict(d, href=href, root=root, migrate=False) + + raise ValueError(f"Unknown STAC object type {info.object_type}") # This is set in __init__.py _STAC_OBJECT_CLASSES = None diff --git a/pystac/stac_object.py b/pystac/stac_object.py index e40c98443..0fc10af57 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -6,6 +6,8 @@ from pystac import STACError from pystac.link import Link from pystac.utils import is_absolute_href, make_absolute_href +from pystac import serialization +from pystac.serialization.identify import identify_stac_object if TYPE_CHECKING: from pystac.catalog import Catalog as Catalog_Type @@ -469,7 +471,10 @@ def from_file( if not is_absolute_href(href): href = make_absolute_href(href) - o = stac_io.read_stac_object(href) + d = stac_io.read_json(href) + info = identify_stac_object(d) + d = serialization.migrate.migrate_to_latest(d, info) + o = cls.from_dict(d, href=href) # Set the self HREF, if it's not already set to something else. if o.get_self_href() is None: diff --git a/tests/test_catalog.py b/tests/test_catalog.py index cce64f111..6e3209e69 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1122,7 +1122,12 @@ def setUp(self) -> None: self.stac_io = pystac.StacIO.default() def test_from_dict_returns_subclass(self) -> None: - catalog_dict = self.stac_io.read_json(self.TEST_CASE_1) custom_catalog = self.BasicCustomCatalog.from_dict(catalog_dict) + + self.assertIsInstance(custom_catalog, self.BasicCustomCatalog) + + def test_from_file_returns_subclass(self) -> None: + custom_catalog = self.BasicCustomCatalog.from_file(self.TEST_CASE_1) + self.assertIsInstance(custom_catalog, self.BasicCustomCatalog) diff --git a/tests/test_collection.py b/tests/test_collection.py index e8ebf9e2e..d0eaf7de3 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -279,7 +279,12 @@ def setUp(self) -> None: self.stac_io = pystac.StacIO.default() def test_from_dict_returns_subclass(self) -> None: - collection_dict = self.stac_io.read_json(self.MULTI_EXTENT) custom_collection = self.BasicCustomCollection.from_dict(collection_dict) + + self.assertIsInstance(custom_collection, self.BasicCustomCollection) + + def test_from_file_returns_subclass(self) -> None: + custom_collection = self.BasicCustomCollection.from_file(self.MULTI_EXTENT) + self.assertIsInstance(custom_collection, self.BasicCustomCollection) diff --git a/tests/test_item.py b/tests/test_item.py index 36b4022f9..0a201e8b8 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -713,7 +713,12 @@ def setUp(self) -> None: self.stac_io = pystac.StacIO.default() def test_from_dict_returns_subclass(self) -> None: - item_dict = self.stac_io.read_json(self.SAMPLE_ITEM) custom_item = self.BasicCustomItem.from_dict(item_dict) + + self.assertIsInstance(custom_item, self.BasicCustomItem) + + def test_from_file_returns_subclass(self) -> None: + custom_item = self.BasicCustomItem.from_file(self.SAMPLE_ITEM) + self.assertIsInstance(custom_item, self.BasicCustomItem) From 3f6b58e2f7118be968ff3f1550b3a4da3bd9bc51 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 15:13:27 -0400 Subject: [PATCH 3/8] Simplify from_file logic --- pystac/__init__.py | 29 ++++++++++------------------- pystac/catalog.py | 21 ++++++++++++++++----- pystac/collection.py | 19 +++++++++++++++---- pystac/item.py | 20 ++++++++++++++++---- pystac/stac_object.py | 22 +++++++++++++++++----- tests/test_catalog.py | 8 ++++++++ tests/test_collection.py | 8 ++++++++ tests/test_item.py | 8 ++++++++ tests/test_writing.py | 4 +++- 9 files changed, 101 insertions(+), 38 deletions(-) diff --git a/pystac/__init__.py b/pystac/__init__.py index 1aa7f45af..61c5fa117 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -76,10 +76,11 @@ def read_file(href: str) -> STACObject: """Reads a STAC object from a file. - This method will return either a Catalog, a Collection, or an Item based on what the - file contains. + This method will return either a Catalog, a Collection, or an Item based on what + the file contains. - This is a convenience method for :meth:`STACObject.from_file ` + This is a convenience method for :meth:`StacIO.read_stac_object + ` Args: href : The HREF to read the object from. @@ -90,22 +91,12 @@ def read_file(href: str) -> STACObject: Raises: STACTypeError : If the file at ``href`` does not represent a valid - :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not - a :class:`~pystac.STACObject` and must be read using + :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` + is not a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_file ` """ stac_io = StacIO.default() - d = stac_io.read_json(href) - typ = pystac.serialization.identify.identify_stac_object_type(d) - - if typ == STACObjectType.CATALOG: - return Catalog.from_file(href) - elif typ == STACObjectType.COLLECTION: - return Collection.from_file(href) - elif typ == STACObjectType.ITEM: - return Item.from_file(href) - else: - raise STACTypeError(f"Cannot read file of type {typ}") + return stac_io.read_stac_object(href) def write_file( @@ -148,7 +139,7 @@ def read_dict( or :class`~Item` based on the contents of the dict. This is a convenience method for either - :meth:`stac_io.stac_object_from_dict `. + :meth:`StacIO.stac_object_from_dict `. Args: d : The dict to parse. @@ -162,8 +153,8 @@ def read_dict( Raises: STACTypeError : If the ``d`` dictionary does not represent a valid - :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not - a :class:`~pystac.STACObject` and must be read using + :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` + is not a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_dict ` """ if stac_io is None: diff --git a/pystac/catalog.py b/pystac/catalog.py index 7b9367808..2621a30fa 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -1,6 +1,7 @@ import os from copy import deepcopy from enum import Enum +from pystac.errors import STACTypeError from typing import ( Any, Callable, @@ -15,7 +16,7 @@ ) import pystac -from pystac.stac_object import STACObject +from pystac.stac_object import STACObject, STACObjectType from pystac.layout import ( BestPracticesLayoutStrategy, HrefLayoutStrategy, @@ -23,6 +24,11 @@ ) from pystac.link import Link from pystac.cache import ResolvedObjectCache +from pystac.serialization import ( + identify_stac_object_type, + identify_stac_object, + migrate_to_latest, +) from pystac.utils import is_absolute_href, make_absolute_href if TYPE_CHECKING: @@ -902,10 +908,11 @@ def from_dict( migrate: bool = False, ) -> "Catalog": if migrate: - result = pystac.read_dict(d, href=href, root=root) - if not isinstance(result, Catalog): - raise pystac.STACTypeError(f"{result} is not a Catalog") - return result + info = identify_stac_object(d) + d = migrate_to_latest(d, info) + + if not cls.identify_dict(d): + raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -955,3 +962,7 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Catal result._stac_io = stac_io return result + + @classmethod + def identify_dict(cls, d: Dict[str, Any]) -> bool: + return identify_stac_object_type(d) == STACObjectType.CATALOG diff --git a/pystac/collection.py b/pystac/collection.py index 22a8be857..9dbcffbf0 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -1,5 +1,6 @@ from copy import copy, deepcopy from datetime import datetime as Datetime +from pystac.errors import STACTypeError from typing import ( Any, Dict, @@ -23,6 +24,11 @@ from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.utils import datetime_to_str +from pystac.serialization import ( + identify_stac_object_type, + identify_stac_object, + migrate_to_latest, +) from pystac.summaries import Summaries if TYPE_CHECKING: @@ -583,10 +589,11 @@ def from_dict( migrate: bool = False, ) -> "Collection": if migrate: - result = pystac.read_dict(d, href=href, root=root) - if not isinstance(result, Collection): - raise pystac.STACError(f"{result} is not a Catalog") - return result + info = identify_stac_object(d) + d = migrate_to_latest(d, info) + + if not cls.identify_dict(d): + raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -676,3 +683,7 @@ def from_file( if not isinstance(result, Collection): raise pystac.STACTypeError(f"{result} is not a {Collection}.") return result + + @classmethod + def identify_dict(cls, d: Dict[str, Any]) -> bool: + return identify_stac_object_type(d) == STACObjectType.COLLECTION diff --git a/pystac/item.py b/pystac/item.py index f07240bf3..86385f8fe 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -9,6 +9,11 @@ from pystac import STACError, STACObjectType from pystac.asset import Asset from pystac.link import Link +from pystac.serialization import ( + identify_stac_object_type, + identify_stac_object, + migrate_to_latest, +) from pystac.stac_object import STACObject from pystac.utils import ( is_absolute_href, @@ -912,10 +917,13 @@ def from_dict( migrate: bool = False, ) -> "Item": if migrate: - result = pystac.read_dict(d, href=href, root=root) - if not isinstance(result, Item): - raise pystac.STACError(f"{result} is not a Catalog") - return result + info = identify_stac_object(d) + d = migrate_to_latest(d, info) + + if not cls.identify_dict(d): + raise pystac.STACTypeError( + f"{d} does not represent a {cls.__name__} instance" + ) d = deepcopy(d) id = d.pop("id") @@ -980,3 +988,7 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Item" if not isinstance(result, Item): raise pystac.STACTypeError(f"{result} is not a {Item}.") return result + + @classmethod + def identify_dict(cls, d: Dict[str, Any]) -> bool: + return identify_stac_object_type(d) == STACObjectType.ITEM diff --git a/pystac/stac_object.py b/pystac/stac_object.py index 0fc10af57..3c224937b 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -6,8 +6,6 @@ from pystac import STACError from pystac.link import Link from pystac.utils import is_absolute_href, make_absolute_href -from pystac import serialization -from pystac.serialization.identify import identify_stac_object if TYPE_CHECKING: from pystac.catalog import Catalog as Catalog_Type @@ -465,6 +463,9 @@ def from_file( The specific STACObject implementation class that is represented by the JSON read from the file located at HREF. """ + if cls == STACObject: + return pystac.read_file(href) + if stac_io is None: stac_io = pystac.StacIO.default() @@ -472,9 +473,7 @@ def from_file( href = make_absolute_href(href) d = stac_io.read_json(href) - info = identify_stac_object(d) - d = serialization.migrate.migrate_to_latest(d, info) - o = cls.from_dict(d, href=href) + o = cls.from_dict(d, href=href, migrate=True) # Set the self HREF, if it's not already set to something else. if o.get_self_href() is None: @@ -513,3 +512,16 @@ def from_dict( STACObject: The STACObject parsed from this dict. """ pass + + @classmethod + @abstractmethod + def identify_dict(cls, d: Dict[str, Any]) -> bool: + """Returns a boolean indicating whether the given dictionary represents a valid + instance of this :class:`~STACObject` sub-class. + + Args: + d : A dictionary to identify + """ + raise NotImplementedError( + "identify_dict must be implemented by the STACObject subclass." + ) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 6e3209e69..905de15e7 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -974,6 +974,14 @@ def test_catalog_with_href_caches_by_href(self) -> None: # cached only by HREF self.assertEqual(len(cache.id_keys_to_objects), 0) + def testfrom_invalid_dict_raises_exception(self) -> None: + stac_io = pystac.StacIO.default() + collection_dict = stac_io.read_json( + TestCases.get_path("data-files/collections/multi-extent.json") + ) + with self.assertRaises(pystac.STACTypeError): + _ = pystac.Catalog.from_dict(collection_dict) + class FullCopyTest(unittest.TestCase): def check_link(self, link: pystac.Link, tag: str) -> None: diff --git a/tests/test_collection.py b/tests/test_collection.py index d0eaf7de3..69a3e2f1e 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -196,6 +196,14 @@ def test_schema_summary(self) -> None: self.assertIsInstance(instruments_schema, dict) + def test_from_invalid_dict_raises_exception(self) -> None: + stac_io = pystac.StacIO.default() + catalog_dict = stac_io.read_json( + TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + ) + with self.assertRaises(pystac.STACTypeError): + _ = pystac.Collection.from_dict(catalog_dict) + class ExtentTest(unittest.TestCase): def test_spatial_allows_single_bbox(self) -> None: diff --git a/tests/test_item.py b/tests/test_item.py index 0a201e8b8..490daad24 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -216,6 +216,14 @@ def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None: item.make_asset_hrefs_relative() self.assertEqual(asset.get_absolute_href(), original_href) + def test_from_invalid_dict_raises_exception(self) -> None: + stac_io = pystac.StacIO.default() + catalog_dict = stac_io.read_json( + TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + ) + with self.assertRaises(pystac.STACTypeError): + _ = pystac.Item.from_dict(catalog_dict) + class CommonMetadataTest(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/test_writing.py b/tests/test_writing.py index 52ba697a9..28779b15c 100644 --- a/tests/test_writing.py +++ b/tests/test_writing.py @@ -72,7 +72,9 @@ def validate_catalog_link_type( href: str, link_type: str, should_include_self: bool ) -> None: cat_dict = pystac.StacIO.default().read_json(href) - cat = pystac.Catalog.from_file(href) + cat = pystac.read_file(href) + if not isinstance(cat, pystac.Catalog): + raise pystac.STACTypeError(f"File at {href} is not a Catalog.") rels = set([link["rel"] for link in cat_dict["links"]]) self.assertEqual("self" in rels, should_include_self) From b73ac4d62fa360844aa233b387725b93d7463c70 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 15:31:20 -0400 Subject: [PATCH 4/8] Improve I/O test coverage --- tests/test_stac_io.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_stac_io.py b/tests/test_stac_io.py index 753bef593..376f8bd82 100644 --- a/tests/test_stac_io.py +++ b/tests/test_stac_io.py @@ -4,11 +4,14 @@ import tempfile import pystac -from pystac.stac_io import STAC_IO +from pystac.stac_io import STAC_IO, StacIO from tests.utils import TestCases class StacIOTest(unittest.TestCase): + def setUp(self) -> None: + self.stac_io = StacIO.default() + def test_stac_io_issues_warnings(self) -> None: with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. @@ -87,3 +90,30 @@ def test_read_item_collection_raises_exception(self) -> None: "data-files/item-collection/sample-item-collection.json" ) ) + + def test_read_item_dict(self) -> None: + item_dict = self.stac_io.read_json( + TestCases.get_path("data-files/item/sample-item.json") + ) + item = pystac.read_dict(item_dict) + self.assertIsInstance(item, pystac.Item) + + def test_read_collection_dict(self) -> None: + collection_dict = self.stac_io.read_json( + TestCases.get_path("data-files/collections/multi-extent.json") + ) + collection = pystac.read_dict(collection_dict) + self.assertIsInstance(collection, pystac.Collection) + + def test_read_catalog_dict(self) -> None: + catalog_dict = self.stac_io.read_json( + TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + ) + catalog = pystac.read_dict(catalog_dict) + self.assertIsInstance(catalog, pystac.Catalog) + + def test_read_from_stac_object(self) -> None: + catalog = pystac.STACObject.from_file( + TestCases.get_path("data-files/catalogs/test-case-1/catalog.json") + ) + self.assertIsInstance(catalog, pystac.Catalog) From 4ebc6609902a0d6b64bc19a59a530376b0157a1e Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 17:34:14 -0400 Subject: [PATCH 5/8] Add stac_io argument to read_file and write_file --- pystac/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pystac/__init__.py b/pystac/__init__.py index 61c5fa117..c382046df 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -73,7 +73,7 @@ ) -def read_file(href: str) -> STACObject: +def read_file(href: str, stac_io: Optional[StacIO] = None) -> STACObject: """Reads a STAC object from a file. This method will return either a Catalog, a Collection, or an Item based on what @@ -84,6 +84,8 @@ def read_file(href: str) -> STACObject: Args: href : The HREF to read the object from. + stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not + provided, will use :meth:`StacIO.default` to create an instance. Returns: The specific STACObject implementation class that is represented @@ -95,7 +97,8 @@ def read_file(href: str) -> STACObject: is not a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_file ` """ - stac_io = StacIO.default() + if stac_io is None: + stac_io = StacIO.default() return stac_io.read_stac_object(href) @@ -103,6 +106,7 @@ def write_file( obj: STACObject, include_self_link: bool = True, dest_href: Optional[str] = None, + stac_io: Optional[StacIO] = None, ) -> None: """Writes a STACObject to a file. @@ -122,8 +126,14 @@ def write_file( Otherwise, leave out the self link. dest_href : Optional HREF to save the file to. If ``None``, the object will be saved to the object's ``"self"`` href. + stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not + provided, will use :meth:`StacIO.default` to create an instance. """ - obj.save_object(include_self_link=include_self_link, dest_href=dest_href) + if stac_io is None: + stac_io = StacIO.default() + obj.save_object( + include_self_link=include_self_link, dest_href=dest_href, stac_io=stac_io + ) def read_dict( From e15c7c04d5904f144050285cf8d453fe8ea701af Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 17:48:25 -0400 Subject: [PATCH 6/8] Rename method for identifying dict type --- pystac/catalog.py | 4 ++-- pystac/collection.py | 4 ++-- pystac/item.py | 4 ++-- pystac/stac_object.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pystac/catalog.py b/pystac/catalog.py index 2621a30fa..35be90f8f 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -911,7 +911,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.identify_dict(d): + if not cls.dict_matches_object_type(d): raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -964,5 +964,5 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Catal return result @classmethod - def identify_dict(cls, d: Dict[str, Any]) -> bool: + def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.CATALOG diff --git a/pystac/collection.py b/pystac/collection.py index 9dbcffbf0..0d0b6ea4d 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -592,7 +592,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.identify_dict(d): + if not cls.dict_matches_object_type(d): raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -685,5 +685,5 @@ def from_file( return result @classmethod - def identify_dict(cls, d: Dict[str, Any]) -> bool: + def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.COLLECTION diff --git a/pystac/item.py b/pystac/item.py index 86385f8fe..ff4a0a984 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -920,7 +920,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.identify_dict(d): + if not cls.dict_matches_object_type(d): raise pystac.STACTypeError( f"{d} does not represent a {cls.__name__} instance" ) @@ -990,5 +990,5 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Item" return result @classmethod - def identify_dict(cls, d: Dict[str, Any]) -> bool: + def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.ITEM diff --git a/pystac/stac_object.py b/pystac/stac_object.py index 3c224937b..a6a16b550 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -515,7 +515,7 @@ def from_dict( @classmethod @abstractmethod - def identify_dict(cls, d: Dict[str, Any]) -> bool: + def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: """Returns a boolean indicating whether the given dictionary represents a valid instance of this :class:`~STACObject` sub-class. From 1fbf4c26d5c392f4adf6b0fbc86584632a3b6d75 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 17:48:42 -0400 Subject: [PATCH 7/8] More specific error message in test_writing --- tests/test_writing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_writing.py b/tests/test_writing.py index 28779b15c..85d197665 100644 --- a/tests/test_writing.py +++ b/tests/test_writing.py @@ -74,7 +74,7 @@ def validate_catalog_link_type( cat_dict = pystac.StacIO.default().read_json(href) cat = pystac.read_file(href) if not isinstance(cat, pystac.Catalog): - raise pystac.STACTypeError(f"File at {href} is not a Catalog.") + raise pystac.STACTypeError(f"File at {href} is a {cat.STAC_OBJECT_TYPE} not a Catalog.") rels = set([link["rel"] for link in cat_dict["links"]]) self.assertEqual("self" in rels, should_include_self) From a6aa201502b9357bbbef342d03813a790f5c3509 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 17 Jun 2021 17:52:30 -0400 Subject: [PATCH 8/8] Rename to matches_object_type --- pystac/catalog.py | 4 ++-- pystac/collection.py | 4 ++-- pystac/item.py | 4 ++-- pystac/stac_object.py | 2 +- tests/test_writing.py | 4 +++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pystac/catalog.py b/pystac/catalog.py index 35be90f8f..5fbb209b3 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -911,7 +911,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.dict_matches_object_type(d): + if not cls.matches_object_type(d): raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -964,5 +964,5 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Catal return result @classmethod - def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: + def matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.CATALOG diff --git a/pystac/collection.py b/pystac/collection.py index 0d0b6ea4d..77a9a8644 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -592,7 +592,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.dict_matches_object_type(d): + if not cls.matches_object_type(d): raise STACTypeError(f"{d} does not represent a {cls.__name__} instance") catalog_type = CatalogType.determine_type(d) @@ -685,5 +685,5 @@ def from_file( return result @classmethod - def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: + def matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.COLLECTION diff --git a/pystac/item.py b/pystac/item.py index ff4a0a984..d0896a324 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -920,7 +920,7 @@ def from_dict( info = identify_stac_object(d) d = migrate_to_latest(d, info) - if not cls.dict_matches_object_type(d): + if not cls.matches_object_type(d): raise pystac.STACTypeError( f"{d} does not represent a {cls.__name__} instance" ) @@ -990,5 +990,5 @@ def from_file(cls, href: str, stac_io: Optional[pystac.StacIO] = None) -> "Item" return result @classmethod - def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: + def matches_object_type(cls, d: Dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.ITEM diff --git a/pystac/stac_object.py b/pystac/stac_object.py index a6a16b550..7564b104f 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -515,7 +515,7 @@ def from_dict( @classmethod @abstractmethod - def dict_matches_object_type(cls, d: Dict[str, Any]) -> bool: + def matches_object_type(cls, d: Dict[str, Any]) -> bool: """Returns a boolean indicating whether the given dictionary represents a valid instance of this :class:`~STACObject` sub-class. diff --git a/tests/test_writing.py b/tests/test_writing.py index 85d197665..665a053b9 100644 --- a/tests/test_writing.py +++ b/tests/test_writing.py @@ -74,7 +74,9 @@ def validate_catalog_link_type( cat_dict = pystac.StacIO.default().read_json(href) cat = pystac.read_file(href) if not isinstance(cat, pystac.Catalog): - raise pystac.STACTypeError(f"File at {href} is a {cat.STAC_OBJECT_TYPE} not a Catalog.") + raise pystac.STACTypeError( + f"File at {href} is a {cat.STAC_OBJECT_TYPE} not a Catalog." + ) rels = set([link["rel"] for link in cat_dict["links"]]) self.assertEqual("self" in rels, should_include_self)