From f2f724227694125dd86e83ac8030ff1bca00e973 Mon Sep 17 00:00:00 2001 From: Rob Emanuele Date: Sun, 25 Apr 2021 01:46:48 -0400 Subject: [PATCH] Add mypy to test step; fixes based on mypy errors --- mypy.ini | 3 + pystac/__init__.py | 16 ++-- pystac/cache.py | 8 +- pystac/catalog.py | 83 +++++++++-------- pystac/collection.py | 68 ++++++++------ pystac/extensions/__init__.py | 2 +- pystac/extensions/eo.py | 13 +-- pystac/extensions/file.py | 17 ++-- pystac/extensions/label.py | 28 +++--- pystac/extensions/pointcloud.py | 19 ++-- pystac/extensions/projection.py | 8 +- pystac/extensions/sar.py | 9 +- pystac/extensions/sat.py | 15 ++-- pystac/extensions/scientific.py | 21 ++--- pystac/extensions/single_file_stac.py | 53 +++++------ pystac/extensions/timestamps.py | 10 +-- pystac/extensions/version.py | 103 +++++++++++----------- pystac/extensions/view.py | 14 +-- pystac/item.py | 30 ++++--- pystac/link.py | 36 ++++---- pystac/media_type.py | 2 +- pystac/serialization/common_properties.py | 11 ++- pystac/serialization/identify.py | 45 +++++----- pystac/serialization/migrate.py | 20 +++-- pystac/stac_io.py | 16 ++-- pystac/stac_object.py | 51 +++++------ pystac/utils.py | 2 +- pystac/validation/__init__.py | 20 +++-- pystac/validation/schema_uri_map.py | 14 +-- pystac/validation/stac_validator.py | 18 ++-- pystac/version.py | 2 +- requirements-dev.txt | 6 +- scripts/test | 3 + 33 files changed, 410 insertions(+), 356 deletions(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..132834a06 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +ignore_missing_imports = True +disallow_untyped_defs = True \ No newline at end of file diff --git a/pystac/__init__.py b/pystac/__init__.py index c4f4c54e4..a3ff580be 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -23,7 +23,7 @@ class STACTypeError(Exception): from typing import Any, Dict, Optional from pystac.version import (__version__, get_stac_version, set_stac_version) # type:ignore -from pystac.stac_io import STAC_IO +from pystac.stac_io import STAC_IO # type:ignore from pystac.extensions import Extensions # type:ignore from pystac.stac_object import (STACObject, STACObjectType) # type:ignore from pystac.media_type import MediaType # type:ignore @@ -37,12 +37,8 @@ class STACTypeError(Exception): Provider) # type:ignore from pystac.item import (Item, Asset, CommonMetadata) # type:ignore -from pystac.serialization import stac_object_from_dict - import pystac.validation -STAC_IO.stac_object_from_dict = stac_object_from_dict - import pystac.extensions.base import pystac.extensions.eo import pystac.extensions.label @@ -92,7 +88,9 @@ def read_file(href: str) -> STACObject: return STACObject.from_file(href) -def write_file(obj: STACObject, include_self_link: bool = True, dest_href: Optional[str] = None): +def write_file(obj: STACObject, + include_self_link: bool = True, + dest_href: Optional[str] = None) -> None: """Writes a STACObject to a file. This will write only the Catalog, Collection or Item ``obj``. It will not attempt @@ -115,7 +113,9 @@ def write_file(obj: STACObject, include_self_link: bool = True, dest_href: Optio obj.save_object(include_self_link=include_self_link, dest_href=dest_href) -def read_dict(d: Dict[str, Any], href: Optional[str] = None, root: Optional[Catalog] = None): +def read_dict(d: Dict[str, Any], + href: Optional[str] = None, + root: Optional[Catalog] = None) -> STACObject: """Reads a STAC object from a dict representing the serialized JSON version of the STAC object. @@ -132,4 +132,4 @@ def read_dict(d: Dict[str, Any], href: Optional[str] = None, root: Optional[Cata If provided, the root's resolved object cache can be used to search for previously resolved instances of the STAC object. """ - return stac_object_from_dict(d, href, root) + return STAC_IO.stac_object_from_dict(d, href, root) diff --git a/pystac/cache.py b/pystac/cache.py index c8ea88277..acbcd340b 100644 --- a/pystac/cache.py +++ b/pystac/cache.py @@ -26,7 +26,7 @@ def get_cache_key(stac_object: "STACObject") -> Tuple[str, bool]: return (href, True) else: ids: List[str] = [] - obj = stac_object + obj: Optional[STACObject] = stac_object while obj is not None: ids.append(obj.id) obj = obj.get_parent() @@ -65,7 +65,7 @@ def __init__(self, self.hrefs_to_objects = hrefs_to_objects or {} self.ids_to_collections = ids_to_collections or {} - self._collection_cache = None + self._collection_cache: Optional[ResolvedObjectCollectionCache] = None def get_or_cache(self, obj: "STACObject") -> "STACObject": """Gets the STACObject that is the cached version of the given STACObject; or, if @@ -268,7 +268,9 @@ def contains_id(self, collection_id: str) -> bool: return (self.resolved_object_cache.contains_collection_id(collection_id) or super().contains_id(collection_id)) - def cache(self, collection: Dict[str, Any], href: Optional[str] = None) -> None: + def cache(self, + collection: Union["Collection", Dict[str, Any]], + href: Optional[str] = None) -> None: super().cache(collection, href) @staticmethod diff --git a/pystac/catalog.py b/pystac/catalog.py index ac91592d9..4d1ed1350 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -14,7 +14,7 @@ class CatalogType(str, Enum): - def __str__(self): + def __str__(self) -> str: return str(self.value) SELF_CONTAINED = 'SELF_CONTAINED' @@ -137,7 +137,7 @@ def __init__(self, if href is not None: self.set_self_href(href) - self.catalog_type = catalog_type + self.catalog_type: CatalogType = catalog_type self._resolved_objects.cache(self) @@ -156,7 +156,7 @@ def is_relative(self) -> bool: def add_child(self, child: "Catalog", title: Optional[str] = None, - strategy: Optional[HrefLayoutStrategy] = None): + strategy: Optional[HrefLayoutStrategy] = None) -> None: """Adds a link to a child :class:`~pystac.Catalog` or :class:`~pystac.Collection`. This method will set the child's parent to this object, and its root to this Catalog's root. @@ -268,7 +268,7 @@ def get_children(self) -> Iterable["Catalog"]: """ return map(lambda x: cast(ps.Catalog, x), self.get_stac_objects('child')) - def get_child_links(self): + def get_child_links(self) -> List[Link]: """Return all child links of this catalog. Return: @@ -276,7 +276,7 @@ def get_child_links(self): """ return self.get_links('child') - def clear_children(self) -> "Catalog": + def clear_children(self) -> None: """Removes all children from this catalog. Return: @@ -285,7 +285,6 @@ def clear_children(self) -> "Catalog": child_ids = [child.id for child in self.get_children()] for child_id in child_ids: self.remove_child(child_id) - return self def remove_child(self, child_id: str) -> None: """Removes an child from this catalog. @@ -336,7 +335,7 @@ def get_items(self) -> Iterable["ItemType"]: """ return map(lambda x: cast(ps.Item, x), self.get_stac_objects('item')) - def clear_items(self) -> "Catalog": + def clear_items(self) -> None: """Removes all items from this catalog. Return: @@ -349,7 +348,6 @@ def clear_items(self) -> "Catalog": item.set_root(None) self.links = [link for link in self.links if link.rel != 'item'] - return self def remove_item(self, item_id: str) -> None: """Removes an item from this catalog. @@ -396,7 +394,7 @@ def get_item_links(self) -> List[Link]: def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: links = self.links if not include_self_link: - links = filter(lambda l: l.rel != 'self', links) + links = [x for x in links if x.rel != 'self'] d: Dict[str, Any] = { 'id': self.id, @@ -438,7 +436,7 @@ def clone(self) -> "Catalog": return clone - def make_all_asset_hrefs_relative(self): + def make_all_asset_hrefs_relative(self) -> None: """Makes all the HREFs of assets belonging to items in this catalog and all children to be relative, recursively. """ @@ -446,7 +444,7 @@ def make_all_asset_hrefs_relative(self): for item in items: item.make_asset_hrefs_relative() - def make_all_asset_hrefs_absolute(self): + def make_all_asset_hrefs_absolute(self) -> None: """Makes all the HREFs of assets belonging to items in this catalog and all children to be absolute, recursively. """ @@ -457,7 +455,7 @@ def make_all_asset_hrefs_absolute(self): def normalize_and_save(self, root_href: str, catalog_type: Optional[CatalogType] = None, - strategy: Optional[HrefLayoutStrategy] = None): + strategy: Optional[HrefLayoutStrategy] = None) -> None: """Normalizes link HREFs to the given root_href, and saves the catalog. This is a convenience method that simply calls :func:`Catalog.normalize_hrefs @@ -476,7 +474,9 @@ def normalize_and_save(self, self.normalize_hrefs(root_href, strategy=strategy) self.save(catalog_type) - def normalize_hrefs(self, root_href: str, strategy: Optional[HrefLayoutStrategy] = None): + def normalize_hrefs(self, + root_href: str, + strategy: Optional[HrefLayoutStrategy] = None) -> None: """Normalize HREFs will regenerate all link HREFs based on an absolute root_href and the canonical catalog layout as specified in the STAC specification's best practices. @@ -492,7 +492,9 @@ def normalize_hrefs(self, root_href: str, strategy: Optional[HrefLayoutStrategy] `STAC best practices document `_ for the canonical layout of a STAC. """ # noqa E501 if strategy is None: - strategy = BestPracticesLayoutStrategy() + _strategy: HrefLayoutStrategy = BestPracticesLayoutStrategy() + else: + _strategy = strategy # Normalizing requires an absolute path if not is_absolute_href(root_href): @@ -501,9 +503,9 @@ def normalize_hrefs(self, root_href: str, strategy: Optional[HrefLayoutStrategy] def process_item(item: "ItemType", _root_href: str) -> Callable[[], None]: item.resolve_links() - new_self_href = strategy.get_href(item, _root_href) + new_self_href = _strategy.get_href(item, _root_href) - def fn(): + def fn() -> None: item.set_self_href(new_self_href) return fn @@ -514,7 +516,7 @@ def process_catalog(cat: Catalog, _root_href: str, cat.resolve_links() - new_self_href = strategy.get_href(cat, _root_href, is_root) + new_self_href = _strategy.get_href(cat, _root_href, is_root) new_root = os.path.dirname(new_self_href) for item in cat.get_items(): @@ -523,7 +525,7 @@ def process_catalog(cat: Catalog, _root_href: str, for child in cat.get_children(): setter_funcs.extend(process_catalog(child, new_root, is_root=False)) - def fn(): + def fn() -> None: cat.set_self_href(new_self_href) setter_funcs.append(fn) @@ -538,12 +540,10 @@ def fn(): for fn in setter_funcs: fn() - return self - def generate_subcatalogs(self, template: str, defaults: Optional[Dict[str, Any]] = None, - parent_ids: List[str] = None, + parent_ids: Optional[List[str]] = None, **kwargs: Any) -> List["Catalog"]: """Walks through the catalog and generates subcatalogs for items based on the template string. See :class:`~pystac.layout.LayoutTemplate` @@ -583,7 +583,7 @@ def generate_subcatalogs(self, item_parts = layout_template.get_template_values(item) id_iter = reversed(parent_ids) if all(['{}'.format(id) == next(id_iter, None) - for id in reversed(item_parts.values())]): + for id in reversed(list(item_parts.values()))]): # Skip items for which the sub-catalog structure already # matches the template. The list of parent IDs can include more # elements on the root side, so compare the reversed sequences. @@ -602,9 +602,9 @@ def generate_subcatalogs(self, curr_parent = subcat # resolve collection link so when added back points to correct location - link = item.get_single_link('collection') - if link is not None: - link.resolve_stac_object() + col_link = item.get_single_link('collection') + if col_link is not None: + col_link.resolve_stac_object() curr_parent.add_item(item) @@ -613,7 +613,7 @@ def generate_subcatalogs(self, return result - def save(self, catalog_type: CatalogType = None) -> None: + def save(self, catalog_type: Optional[CatalogType] = None) -> None: """Save this catalog and all it's children/item to files determined by the object's self link HREF. @@ -652,14 +652,16 @@ def save(self, catalog_type: CatalogType = None) -> None: include_self_link = False # include a self link if this is the root catalog or if ABSOLUTE_PUBLISHED catalog - if ((self.get_self_href() == self.get_root_link().get_absolute_href() - and root.catalog_type != CatalogType.SELF_CONTAINED) - or root.catalog_type == CatalogType.ABSOLUTE_PUBLISHED): + if root.catalog_type == CatalogType.ABSOLUTE_PUBLISHED: include_self_link = True + elif root.catalog_type != CatalogType.SELF_CONTAINED: + root_link = self.get_root_link() + if root_link and root_link.get_absolute_href() == self.get_self_href(): + include_self_link = True self.save_object(include_self_link=include_self_link) - - self.catalog_type = catalog_type + if catalog_type is not None: + self.catalog_type = catalog_type def walk(self) -> Iterable[Tuple["Catalog", Iterable["Catalog"], Iterable["ItemType"]]]: """Walks through children and items of catalogs. @@ -702,7 +704,9 @@ def validate_all(self) -> None: def _object_links(self) -> List[str]: return ['child', 'item'] + (ps.STAC_EXTENSIONS.get_extended_object_links(self)) - def map_items(self, item_mapper: Callable[["ItemType"], Union["ItemType", List["ItemType"]]]): + def map_items( + self, item_mapper: Callable[["ItemType"], Union["ItemType", + List["ItemType"]]]) -> "Catalog": """Creates a copy of a catalog, with each item passed through the item_mapper function. @@ -718,7 +722,7 @@ def map_items(self, item_mapper: Callable[["ItemType"], Union["ItemType", List[" new_cat = cast(Catalog, self.full_copy()) - def process_catalog(catalog: Catalog): + def process_catalog(catalog: Catalog) -> None: for child in catalog.get_children(): process_catalog(child) @@ -742,9 +746,10 @@ def process_catalog(catalog: Catalog): process_catalog(new_cat) return new_cat - def map_assets(self, asset_mapper: Callable[[str, "AssetType"], - Union["AssetType", Tuple[str, "AssetType"], - Dict[str, "AssetType"]]]): + def map_assets( + self, asset_mapper: Callable[[str, "AssetType"], Union["AssetType", Tuple[str, "AssetType"], + Dict[str, "AssetType"]]] + ) -> "Catalog": """Creates a copy of a catalog, with each Asset for each Item passed through the asset_mapper function. @@ -758,7 +763,7 @@ def map_assets(self, asset_mapper: Callable[[str, "AssetType"], Catalog: A full copy of this catalog, with assets manipulated according to the asset_mapper function. """ - def apply_asset_mapper(tup: Tuple[str, "AssetType"]): + def apply_asset_mapper(tup: Tuple[str, "AssetType"]) -> List[Tuple[str, ps.Asset]]: k, v = tup result = asset_mapper(k, v) if result is None: @@ -773,7 +778,7 @@ def apply_asset_mapper(tup: Tuple[str, "AssetType"]): raise Exception('asset_mapper must return a non-empty list') return assets - def item_mapper(item: ps.Item): + def item_mapper(item: ps.Item) -> ps.Item: new_assets = [ x for result in map(apply_asset_mapper, item.assets.items()) for x in result ] @@ -782,7 +787,7 @@ def item_mapper(item: ps.Item): return self.map_items(item_mapper) - def describe(self, include_hrefs: bool = False, _indent: int = 0): + def describe(self, include_hrefs: bool = False, _indent: int = 0) -> None: """Prints out information about this Catalog and all contained STACObjects. diff --git a/pystac/collection.py b/pystac/collection.py index 2f8fbf7a0..ba9c6e3a2 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -8,6 +8,7 @@ import pystac as ps from pystac import (STACObjectType, CatalogType) from pystac.catalog import Catalog +from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.utils import datetime_to_str @@ -36,7 +37,7 @@ def __init__(self, bboxes: Union[List[List[float]], List[float]]) -> None: if isinstance(bboxes, list) and isinstance(bboxes[0], float): self.bboxes: List[List[float]] = [cast(List[float], bboxes)] else: - self.bboxes: List[List[float]] = cast(List[List[float]], bboxes) + self.bboxes = cast(List[List[float]], bboxes) def to_dict(self) -> Dict[str, Any]: """Generate a dictionary representing the JSON of this SpatialExtent. @@ -147,10 +148,10 @@ def to_dict(self) -> Dict[str, Any]: start = None end = None - if i[0]: + if i[0] is not None: start = datetime_to_str(i[0]) - if i[1]: + if i[1] is not None: end = datetime_to_str(i[1]) encoded_intervals.append([start, end]) @@ -266,30 +267,40 @@ def from_items(items: Iterable["ItemType"]) -> "Extent": Extent: An Extent that spatially and temporally covers all of the given items. """ - def extract_extent_props(item: ps.Item) -> List[Any]: - return item.bbox + [ - item.datetime, item.common_metadata.start_datetime, - item.common_metadata.end_datetime - ] # type:ignore - - xmins, ymins, xmaxs, ymaxs, datetimes, starts, ends = zip(*map(extract_extent_props, items)) + bounds_values: List[List[float]] = [[float('inf')], [float('inf')], [float('-inf')], + [float('-inf')]] + datetimes: List[Datetime] = [] + starts: List[Datetime] = [] + ends: List[Datetime] = [] + + for item in items: + if item.bbox is not None: + for i in range(0, 4): + bounds_values[i].append(item.bbox[i]) + if item.datetime is not None: + datetimes.append(item.datetime) + if item.common_metadata.start_datetime is not None: + starts.append(item.common_metadata.start_datetime) + if item.common_metadata.end_datetime is not None: + ends.append(item.common_metadata.end_datetime) if not any(datetimes + starts): start_timestamp = None else: - start_timestamp = min([ - dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) - for dt in filter(None, datetimes + starts) - ]) + start_timestamp = min( + [dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) for dt in datetimes + starts]) if not any(datetimes + ends): end_timestamp = None else: - end_timestamp = max([ - dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) - for dt in filter(None, datetimes + ends) - ]) - - spatial = SpatialExtent([[min(xmins), min(ymins), max(xmaxs), max(ymaxs)]]) + end_timestamp = max( + [dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) for dt in datetimes + ends]) + + spatial = SpatialExtent([[ + min(bounds_values[0]), + min(bounds_values[1]), + max(bounds_values[2]), + max(bounds_values[3]) + ]]) temporal = TemporalExtent([[start_timestamp, end_timestamp]]) return Extent(spatial, temporal) @@ -424,8 +435,8 @@ def __init__(self, keywords: Optional[List[str]] = None, providers: Optional[List[Provider]] = None, summaries: Optional[Dict[str, Any]] = None): - super(Collection, self).__init__(id, description, title, stac_extensions, extra_fields, - href, catalog_type or CatalogType.ABSOLUTE_PUBLISHED) + super().__init__(id, description, title, stac_extensions, extra_fields, href, catalog_type + or CatalogType.ABSOLUTE_PUBLISHED) self.extent = extent self.license = license @@ -437,12 +448,15 @@ def __init__(self, def __repr__(self) -> str: return ''.format(self.id) - def add_item(self, item: "ItemType", title: Optional[str] = None) -> None: - super(Collection, self).add_item(item, title) + def add_item(self, + item: "ItemType", + title: Optional[str] = None, + strategy: Optional[HrefLayoutStrategy] = None) -> None: + super().add_item(item, title, strategy) item.set_collection(self) def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: - d = super(Collection, self).to_dict(include_self_link) + d = super().to_dict(include_self_link) d['extent'] = self.extent.to_dict() d['license'] = self.license if self.stac_extensions is not None: @@ -456,7 +470,7 @@ def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: return d - def clone(self): + def clone(self) -> "Collection": clone = Collection(id=self.id, description=self.description, extent=self.extent.clone(), @@ -537,7 +551,7 @@ def from_dict(cls, return collection - def update_extent_from_items(self): + def update_extent_from_items(self) -> None: """ Update datetime and bbox based on all items to a single bbox and time window. """ diff --git a/pystac/extensions/__init__.py b/pystac/extensions/__init__.py index 1f944a7d6..d8911d50f 100644 --- a/pystac/extensions/__init__.py +++ b/pystac/extensions/__init__.py @@ -10,7 +10,7 @@ class ExtensionError(Exception): class Extensions(str, Enum): """Enumerates the IDs of common extensions.""" - def __str__(self): + def __str__(self) -> str: return str(self.value) CHECKSUM = 'checksum' diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py index c31fc55b5..9ea49b08f 100644 --- a/pystac/extensions/eo.py +++ b/pystac/extensions/eo.py @@ -17,7 +17,7 @@ def apply(self, common_name: Optional[str] = None, description: Optional[str] = None, center_wavelength: Optional[float] = None, - full_width_half_max: Optional[float] = None): + full_width_half_max: Optional[float] = None) -> None: """ Sets the properties for this Band. @@ -43,7 +43,7 @@ def create(cls, common_name: Optional[str] = None, description: Optional[str] = None, center_wavelength: Optional[float] = None, - full_width_half_max: Optional[float] = None): + full_width_half_max: Optional[float] = None) -> "Band": """ Creates a new band. @@ -202,8 +202,8 @@ def band_description(common_name: str) -> Optional[str]: """ # noqa E501 r = Band.band_range(common_name) if r is not None: - r = "Common name: {}, Range: {} to {}".format(common_name, r[0], r[1]) - return r + return "Common name: {}, Range: {} to {}".format(common_name, r[0], r[1]) + return None class EOItemExt(ItemExtension): @@ -228,7 +228,7 @@ def __init__(self, item: Item) -> None: self.item = item - def apply(self, bands: List[Band], cloud_cover: Optional[float] = None): + def apply(self, bands: List[Band], cloud_cover: Optional[float] = None) -> None: """Applies label extension properties to the extended Item. Args: @@ -340,4 +340,5 @@ def from_item(cls, item: Item) -> "EOItemExt": return cls(item) -EO_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.EO, [ExtendedObject(Item, EOItemExt)]) +EO_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.EO, [ExtendedObject(Item, EOItemExt)]) diff --git a/pystac/extensions/file.py b/pystac/extensions/file.py index c86861035..fbb5fb30d 100644 --- a/pystac/extensions/file.py +++ b/pystac/extensions/file.py @@ -51,7 +51,7 @@ def apply(self, data_type: Optional[FileDataType] = None, size: Optional[int] = None, nodata: Optional[List[Any]] = None, - checksum: Optional[str] = None): + checksum: Optional[str] = None) -> None: """Applies file extension properties to the extended Item. Args: @@ -66,13 +66,6 @@ def apply(self, self.nodata = nodata self.checksum = checksum - def _set_property(self, key: str, value: Any, asset: Optional[Asset]) -> None: - target = self.item.properties if asset is None else asset.properties - if value is None: - target.pop(key, None) - else: - target[key] = value - @property def data_type(self) -> Optional[FileDataType]: """Get or sets the data_type of the file. @@ -102,6 +95,7 @@ def get_data_type(self, asset: Optional[Asset] = None) -> Optional[FileDataType] if data_type is not None: return FileDataType(data_type) + return None def set_data_type(self, data_type: Optional[FileDataType], @@ -111,7 +105,8 @@ def set_data_type(self, If an Asset is supplied, sets the property on the Asset. Otherwise sets the Item's value. """ - self._set_property('file:data_type', data_type.value, asset) + v = None if data_type is None else data_type.value + self._set_property('file:data_type', v, asset) @property def size(self) -> Optional[int]: @@ -223,5 +218,5 @@ def from_item(cls, item: Item) -> "FileItemExt": return cls(item) -FILE_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.FILE, - [ExtendedObject(Item, FileItemExt)]) +FILE_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.FILE, [ExtendedObject(Item, FileItemExt)]) diff --git a/pystac/extensions/label.py b/pystac/extensions/label.py index 92d33b123..d1902610a 100644 --- a/pystac/extensions/label.py +++ b/pystac/extensions/label.py @@ -13,7 +13,7 @@ class LabelType(str, Enum): """Enumerates valid label types (RASTER or VECTOR).""" - def __str__(self): + def __str__(self) -> str: return str(self.value) VECTOR = 'vector' @@ -31,7 +31,9 @@ class LabelClasses: def __init__(self, properties: Dict[str, Any]): self.properties = properties - def apply(self, classes: Union[List[str], List[int], List[float]], name: Optional[str] = None): + def apply(self, + classes: Union[List[str], List[int], List[float]], + name: Optional[str] = None) -> None: """Sets the properties for this LabelClasses. Args: @@ -113,7 +115,7 @@ class LabelCount: def __init__(self, properties: Dict[str, Any]): self.properties = properties - def apply(self, name: str, count: int): + def apply(self, name: str, count: int) -> None: """Sets the properties for this LabelCount. Args: @@ -164,7 +166,7 @@ def count(self) -> int: return result @count.setter - def count(self, v: int): + def count(self, v: int) -> None: self.properties['count'] = v def to_dict(self) -> Dict[str, Any]: @@ -259,7 +261,7 @@ def __init__(self, properties: Dict[str, Any]): def apply(self, property_key: Optional[str], counts: Optional[List[LabelCount]] = None, - statistics: Optional[List[LabelStatistics]] = None): + statistics: Optional[List[LabelStatistics]] = None) -> None: """Sets the properties for this LabelOverview. Either ``counts`` or ``statistics``, or both, can be placed in an overview; @@ -380,7 +382,7 @@ def merge_counts(self, other: "LabelOverview") -> "LabelOverview": else: count_by_prop: Dict[str, int] = {} - def add_counts(counts: List[LabelCount]): + def add_counts(counts: List[LabelCount]) -> None: for c in counts: if c.name not in count_by_prop: count_by_prop[c.name] = c.count @@ -435,7 +437,7 @@ def apply(self, label_classes: Optional[List[LabelClasses]] = None, label_tasks: Optional[List[str]] = None, label_methods: Optional[List[str]] = None, - label_overviews: Optional[List[LabelOverview]] = None): + label_overviews: Optional[List[LabelOverview]] = None) -> None: """Applies label extension properties to the extended Item. Args: @@ -493,7 +495,7 @@ def label_type(self) -> LabelType: return LabelType(result) @label_type.setter - def label_type(self, v: LabelType): + def label_type(self, v: LabelType) -> None: if v not in LabelType.ALL: raise STACError("label_type must be one of " "{}. Invalid input: {}".format(LabelType.ALL, v)) @@ -621,7 +623,7 @@ def __repr__(self) -> str: def add_source(self, source_item: Item, title: Optional[str] = None, - assets: Optional[List[str]] = None): + assets: Optional[List[str]] = None) -> None: """Adds a link to a source item. Args: @@ -654,7 +656,7 @@ def add_labels(self, href: str, title: Optional[str] = None, media_type: Optional[str] = None, - properties: Optional[Dict[str, Any]] = None): + properties: Optional[Dict[str, Any]] = None) -> None: """Adds a label asset to this LabelItem. Args: @@ -673,7 +675,7 @@ def add_labels(self, def add_geojson_labels(self, href: str, title: Optional[str] = None, - properties: Optional[Dict[str, Any]] = None): + properties: Optional[Dict[str, Any]] = None) -> None: """Adds a GeoJSON label asset to this LabelItem. Args: @@ -694,5 +696,5 @@ def from_item(cls, item: Item) -> "LabelItemExt": return cls(item) -LABEL_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.LABEL, - [ExtendedObject(Item, LabelItemExt)]) +LABEL_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.LABEL, [ExtendedObject(Item, LabelItemExt)]) diff --git a/pystac/extensions/pointcloud.py b/pystac/extensions/pointcloud.py index c173281bd..5778149fd 100644 --- a/pystac/extensions/pointcloud.py +++ b/pystac/extensions/pointcloud.py @@ -119,7 +119,7 @@ def apply(self, maximum: Optional[float] = None, minimum: Optional[float] = None, stddev: Optional[float] = None, - variance: Optional[float] = None): + variance: Optional[float] = None) -> None: """Sets the properties for this PointcloudStatistic. Args: @@ -150,7 +150,7 @@ def create(cls, maximum: Optional[float] = None, minimum: Optional[float] = None, stddev: Optional[float] = None, - variance: Optional[float] = None): + variance: Optional[float] = None) -> "PointcloudStatistic": """Creates a new PointcloudStatistic class. Args: @@ -346,7 +346,7 @@ def apply(self, schemas: List[PointcloudSchema], density: Optional[float] = None, statistics: Optional[List[PointcloudStatistic]] = None, - epsg: Optional[int] = None): # TODO: Remove epsg per spec + epsg: Optional[int] = None) -> None: # TODO: Remove epsg per spec """Applies Pointcloud extension properties to the extended Item. Args: @@ -402,7 +402,7 @@ def get_count(self, asset: Optional[Asset] = None) -> int: return result - def set_count(self, count: int, asset: Optional[Asset] = None): + def set_count(self, count: int, asset: Optional[Asset] = None) -> None: """Set an Item or an Asset count. If an Asset is supplied, sets the property on the Asset. @@ -524,7 +524,10 @@ def get_schemas(self, asset: Optional[Asset] = None) -> List[PointcloudSchema]: else: schemas = asset.properties.get('pc:schemas') - return [PointcloudSchema(s) for s in schemas] + if schemas is None: + return [] + else: + return [PointcloudSchema(s) for s in schemas] def set_schemas(self, schemas: List[PointcloudSchema], asset: Optional[Asset] = None) -> None: """Set an Item or an Asset schema @@ -585,7 +588,7 @@ def statistics(self) -> Optional[List[PointcloudStatistic]]: return self.get_statistics() @statistics.setter - def statistics(self, v: Optional[List[PointcloudStatistic]]): + def statistics(self, v: Optional[List[PointcloudStatistic]]) -> None: self.set_statistics(v) def get_statistics(self, asset: Optional[Asset] = None) -> Optional[List[PointcloudStatistic]]: @@ -628,5 +631,5 @@ def from_item(cls, item: Item) -> "PointcloudItemExt": return cls(item) -POINTCLOUD_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.POINTCLOUD, - [ExtendedObject(Item, PointcloudItemExt)]) +POINTCLOUD_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.POINTCLOUD, [ExtendedObject(Item, PointcloudItemExt)]) diff --git a/pystac/extensions/projection.py b/pystac/extensions/projection.py index 39e28af83..3d6285f0f 100644 --- a/pystac/extensions/projection.py +++ b/pystac/extensions/projection.py @@ -34,7 +34,7 @@ def apply(self, bbox: Optional[List[float]] = None, centroid: Optional[Dict[str, float]] = None, shape: Optional[List[int]] = None, - transform: Optional[List[float]] = None): + transform: Optional[List[float]] = None) -> None: """Applies Projection extension properties to the extended Item. Args: @@ -139,7 +139,7 @@ def get_wkt2(self, asset: Optional[Asset] = None) -> Optional[str]: else: return asset.properties.get('proj:wkt2') - def set_wkt2(self, wkt2: Optional[str], asset: Optional[Asset] = None): + def set_wkt2(self, wkt2: Optional[str], asset: Optional[Asset] = None) -> None: """Set an Item or an Asset wkt2. If an Asset is supplied, sets the property on the Asset. @@ -418,5 +418,5 @@ def from_item(cls, item: Item) -> "ProjectionItemExt": return cls(item) -PROJECTION_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.PROJECTION, - [ExtendedObject(Item, ProjectionItemExt)]) +PROJECTION_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.PROJECTION, [ExtendedObject(Item, ProjectionItemExt)]) diff --git a/pystac/extensions/sar.py b/pystac/extensions/sar.py index 97f81b297..35d61f97c 100644 --- a/pystac/extensions/sar.py +++ b/pystac/extensions/sar.py @@ -83,7 +83,7 @@ def apply(self, looks_range: Optional[int] = None, looks_azimuth: Optional[int] = None, looks_equivalent_number: Optional[float] = None, - observation_direction: Optional[ObservationDirection] = None): + observation_direction: Optional[ObservationDirection] = None) -> None: """Applies sar extension properties to the extended Item. Args: @@ -337,6 +337,7 @@ def observation_direction(self, v: ObservationDirection) -> None: self.item.properties[OBSERVATION_DIRECTION] = v.value -SAR_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SAR, [ - base.ExtendedObject(pystac.Item, SarItemExt), -]) +SAR_EXTENSION_DEFINITION: base.ExtensionDefinition = base.ExtensionDefinition( + Extensions.SAR, [ + base.ExtendedObject(pystac.Item, SarItemExt), + ]) diff --git a/pystac/extensions/sat.py b/pystac/extensions/sat.py index 84faf14f9..1b670f40b 100644 --- a/pystac/extensions/sat.py +++ b/pystac/extensions/sat.py @@ -38,7 +38,9 @@ class SatItemExt(base.ItemExtension): def __init__(self, an_item: pystac.Item) -> None: self.item = an_item - def apply(self, orbit_state: Optional[OrbitState] = None, relative_orbit: Optional[int] = None): + def apply(self, + orbit_state: Optional[OrbitState] = None, + relative_orbit: Optional[int] = None) -> None: """Applies ext extension properties to the extended Item. Must specify at least one of orbit_state or relative_orbit. @@ -57,7 +59,7 @@ def apply(self, orbit_state: Optional[OrbitState] = None, relative_orbit: Option self.relative_orbit = relative_orbit @classmethod - def from_item(cls, an_item: pystac.Item): + def from_item(cls, an_item: pystac.Item) -> "SatItemExt": return cls(an_item) @classmethod @@ -72,7 +74,7 @@ def orbit_state(self) -> Optional[OrbitState]: OrbitState or None """ if ORBIT_STATE not in self.item.properties: - return + return None return OrbitState(self.item.properties[ORBIT_STATE]) @orbit_state.setter @@ -108,6 +110,7 @@ def relative_orbit(self, v: int) -> None: self.item.properties[RELATIVE_ORBIT] = v -SAT_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SAT, [ - base.ExtendedObject(pystac.Item, SatItemExt), -]) +SAT_EXTENSION_DEFINITION: base.ExtensionDefinition = base.ExtensionDefinition( + Extensions.SAT, [ + base.ExtendedObject(pystac.Item, SatItemExt), + ]) diff --git a/pystac/extensions/scientific.py b/pystac/extensions/scientific.py index ba044ef22..52c740fb6 100644 --- a/pystac/extensions/scientific.py +++ b/pystac/extensions/scientific.py @@ -50,14 +50,14 @@ def to_dict(self) -> Dict[str, str]: return copy.deepcopy({'doi': self.doi, 'citation': self.citation}) @staticmethod - def from_dict(d: Dict[str, str]): + def from_dict(d: Dict[str, str]) -> "Publication": return Publication(d['doi'], d['citation']) def get_link(self) -> pystac.Link: return pystac.Link(CITE_AS, doi_to_url(self.doi)) -def remove_link(links: List[pystac.Link], doi: str): +def remove_link(links: List[pystac.Link], doi: str) -> None: url = doi_to_url(doi) for i, a_link in enumerate(links): if a_link.rel != CITE_AS: @@ -105,7 +105,7 @@ def apply(self, self.publications = publications @classmethod - def from_item(cls, an_item: pystac.Item): + def from_item(cls, an_item: pystac.Item) -> "ScientificItemExt": return cls(an_item) @classmethod @@ -218,7 +218,7 @@ def __init__(self, a_collection: pystac.Collection): def apply(self, doi: Optional[str] = None, citation: Optional[str] = None, - publications: Optional[List[Publication]] = None): + publications: Optional[List[Publication]] = None) -> None: """Applies scientific extension properties to the extended Collection. Args: @@ -235,7 +235,7 @@ def apply(self, self.publications = publications @classmethod - def from_collection(cls, a_collection: pystac.Collection): + def from_collection(cls, a_collection: pystac.Collection) -> "ScientificCollectionExt": return cls(a_collection) @classmethod @@ -309,7 +309,7 @@ def remove_publication(self, publication: Optional[Publication] = None) -> None: return if not publication: - for one_pub in self.publications: + for one_pub in self.publications or []: remove_link(self.collection.links, one_pub.doi) del self.collection.extra_fields[PUBLICATIONS] @@ -324,7 +324,8 @@ def remove_publication(self, publication: Optional[Publication] = None) -> None: del self.collection.extra_fields[PUBLICATIONS] -SCIENTIFIC_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SCIENTIFIC, [ - base.ExtendedObject(pystac.Item, ScientificItemExt), - base.ExtendedObject(pystac.Collection, ScientificCollectionExt) -]) +SCIENTIFIC_EXTENSION_DEFINITION: base.ExtensionDefinition = base.ExtensionDefinition( + Extensions.SCIENTIFIC, [ + base.ExtendedObject(pystac.Item, ScientificItemExt), + base.ExtendedObject(pystac.Collection, ScientificCollectionExt) + ]) diff --git a/pystac/extensions/single_file_stac.py b/pystac/extensions/single_file_stac.py index ecc211a2a..d3f919d5f 100644 --- a/pystac/extensions/single_file_stac.py +++ b/pystac/extensions/single_file_stac.py @@ -1,13 +1,11 @@ from pystac.item import Item -from typing import List, Optional, cast -from pystac.catalog import Catalog +from typing import Dict, List, Optional, cast -from pystac import (STACError, Extensions) -from pystac.collection import Collection +import pystac as ps from pystac.extensions.base import (CatalogExtension, ExtensionDefinition, ExtendedObject) -def create_single_file_stac(catalog: Catalog): +def create_single_file_stac(catalog: ps.Catalog) -> ps.Catalog: """Creates a Single File STAC from a STAC catalog. This method will recursively collect any collections and items in the catalog @@ -21,10 +19,10 @@ def create_single_file_stac(catalog: Catalog): Args: catalog (Catalog): Catalog to walk while constructing the Single File STAC """ - collections = {} - items = [] + collections: Dict[str, ps.Collection] = {} + items: List[ps.Item] = [] for root, _, cat_items in catalog.walk(): - if issubclass(type(root), Collection): + if isinstance(root, ps.Collection): new_collection = root.clone() new_collection.clear_links() collections[root.id] = new_collection @@ -33,16 +31,16 @@ def create_single_file_stac(catalog: Catalog): new_item.clear_links() items.append(new_item) - filtered_collections = [] + filtered_collections: List[ps.Collection] = [] for item in items: - if item.collection_id in collections: + if item.collection_id is not None and item.collection_id in collections: filtered_collections.append(collections[item.collection_id]) collections.pop(item.collection_id) result = catalog.clone() result.clear_links() - result.ext.enable(Extensions.SINGLE_FILE_STAC) - sfs_ext = cast("SingleFileSTACCatalogExt", result.ext[Extensions.SINGLE_FILE_STAC]) + result.ext.enable(ps.Extensions.SINGLE_FILE_STAC) + sfs_ext = cast("SingleFileSTACCatalogExt", result.ext[ps.Extensions.SINGLE_FILE_STAC]) sfs_ext.apply(features=items, collections=filtered_collections) return result @@ -64,15 +62,17 @@ class SingleFileSTACCatalogExt(CatalogExtension): Using SingleFileSTACCatalogExt to directly wrap a Catalog will add the 'proj' extension ID to the catalog's stac_extensions. """ - def __init__(self, catalog: Catalog): + def __init__(self, catalog: ps.Catalog) -> None: if catalog.stac_extensions is None: - catalog.stac_extensions = [str(Extensions.SINGLE_FILE_STAC)] - elif str(Extensions.SINGLE_FILE_STAC) not in catalog.stac_extensions: - catalog.stac_extensions.append(str(Extensions.SINGLE_FILE_STAC)) + catalog.stac_extensions = [str(ps.Extensions.SINGLE_FILE_STAC)] + elif str(ps.Extensions.SINGLE_FILE_STAC) not in catalog.stac_extensions: + catalog.stac_extensions.append(str(ps.Extensions.SINGLE_FILE_STAC)) self.catalog = catalog - def apply(self, features: List[Item], collections: Optional[List[Collection]] = None): + def apply(self, + features: List[Item], + collections: Optional[List[ps.Collection]] = None) -> None: """ Args: features (List[Item]): List of items contained by @@ -84,9 +84,10 @@ def apply(self, features: List[Item], collections: Optional[List[Collection]] = self.collections = collections @classmethod - def enable_extension(cls, catalog: Catalog): + def enable_extension(cls, stac_object: ps.STACObject) -> None: # Ensure the 'type' property is correct so that the Catalog is valid GeoJSON. - catalog.extra_fields['type'] = 'FeatureCollection' + if isinstance(stac_object, ps.Catalog): + stac_object.extra_fields['type'] = 'FeatureCollection' @property def features(self) -> List[Item]: @@ -97,7 +98,7 @@ def features(self) -> List[Item]: """ features = self.catalog.extra_fields.get('features') if features is None: - raise STACError('Invalid Single File STAC: does not have "features" property.') + raise ps.STACError('Invalid Single File STAC: does not have "features" property.') return [Item.from_dict(feature) for feature in features] @@ -106,7 +107,7 @@ def features(self, v: List[Item]) -> None: self.catalog.extra_fields['features'] = [item.to_dict() for item in v] @property - def collections(self) -> Optional[List[Collection]]: + def collections(self) -> Optional[List[ps.Collection]]: """Get or sets a list of :class:`~pystac.Collection` objects contained in this Single File STAC. """ @@ -115,10 +116,10 @@ def collections(self) -> Optional[List[Collection]]: if collections is None: return None else: - return [Collection.from_dict(col) for col in collections] + return [ps.Collection.from_dict(col) for col in collections] @collections.setter - def collections(self, v: Optional[List[Collection]]) -> None: + def collections(self, v: Optional[List[ps.Collection]]) -> None: if v is not None: self.catalog.extra_fields['collections'] = [col.to_dict() for col in v] else: @@ -129,9 +130,9 @@ def _object_links(cls) -> List[str]: return [] @classmethod - def from_catalog(cls, catalog: Catalog): + def from_catalog(cls, catalog: ps.Catalog) -> "SingleFileSTACCatalogExt": return SingleFileSTACCatalogExt(catalog) -SFS_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.SINGLE_FILE_STAC, - [ExtendedObject(Catalog, SingleFileSTACCatalogExt)]) +SFS_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + ps.Extensions.SINGLE_FILE_STAC, [ExtendedObject(ps.Catalog, SingleFileSTACCatalogExt)]) diff --git a/pystac/extensions/timestamps.py b/pystac/extensions/timestamps.py index 4801029c4..095bfab8f 100644 --- a/pystac/extensions/timestamps.py +++ b/pystac/extensions/timestamps.py @@ -41,7 +41,7 @@ def _object_links(cls) -> List[str]: def apply(self, published: Optional[Datetime] = None, expires: Optional[Datetime] = None, - unpublished: Optional[Datetime] = None): + unpublished: Optional[Datetime] = None) -> None: """Applies timestamps extension properties to the extended Item. Args: @@ -74,9 +74,9 @@ def _timestamp_getter(self, key: str, asset: Optional[Asset] = None) -> Optional def _timestamp_setter(self, timestamp: Optional[Datetime], key: str, - asset: Optional[Asset] = None): + asset: Optional[Asset] = None) -> None: if timestamp is not None: - value = datetime_to_str(timestamp) + value: Optional[str] = datetime_to_str(timestamp) else: value = None self._set_property(key, value, asset) @@ -197,5 +197,5 @@ def set_unpublished(self, self._timestamp_setter(unpublished, 'unpublished', asset) -TIMESTAMPS_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.TIMESTAMPS, - [ExtendedObject(Item, TimestampsItemExt)]) +TIMESTAMPS_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.TIMESTAMPS, [ExtendedObject(Item, TimestampsItemExt)]) diff --git a/pystac/extensions/version.py b/pystac/extensions/version.py index ebb89ddf0..cf95ac9bb 100644 --- a/pystac/extensions/version.py +++ b/pystac/extensions/version.py @@ -7,7 +7,7 @@ from typing import List, Optional, cast -import pystac +import pystac as ps from pystac import Extensions, STACError from pystac.extensions import base @@ -39,17 +39,17 @@ class VersionItemExt(base.ItemExtension): Using VersionItemExt to directly wrap an item will add the 'version' extension ID to the item's stac_extensions. """ - item: pystac.Item + item: ps.Item - def __init__(self, an_item) -> None: + def __init__(self, an_item: ps.Item) -> None: self.item = an_item def apply(self, version: str, deprecated: Optional[bool] = None, - latest: Optional[pystac.Item] = None, - predecessor: Optional[pystac.Item] = None, - successor: Optional[pystac.Item] = None) -> None: + latest: Optional[ps.Item] = None, + predecessor: Optional[ps.Item] = None, + successor: Optional[ps.Item] = None) -> None: """Applies version extension properties to the extended Item. Args: @@ -99,65 +99,65 @@ def deprecated(self) -> bool: @deprecated.setter def deprecated(self, v: bool) -> None: if not isinstance(v, bool): - raise pystac.STACError(DEPRECATED + ' must be a bool') + raise ps.STACError(DEPRECATED + ' must be a bool') self.item.properties[DEPRECATED] = v @property - def latest(self) -> Optional[pystac.Item]: + def latest(self) -> Optional[ps.Item]: """Get or sets the most recent item. Returns: Item or None """ - result = next(self.item.get_stac_objects(LATEST), None) + result = next(iter(self.item.get_stac_objects(LATEST)), None) if result is None: return None - return cast(pystac.Item, result) + return cast(ps.Item, result) @latest.setter - def latest(self, source_item: pystac.Item) -> None: + def latest(self, source_item: ps.Item) -> None: self.item.clear_links(LATEST) if source_item: - self.item.add_link(pystac.Link(LATEST, source_item, MEDIA_TYPE)) + self.item.add_link(ps.Link(LATEST, source_item, MEDIA_TYPE)) @property - def predecessor(self) -> Optional[pystac.Item]: + def predecessor(self) -> Optional[ps.Item]: """Get or sets the previous item. Returns: Item or None """ - result = next(self.item.get_stac_objects(PREDECESSOR), None) + result = next(iter(self.item.get_stac_objects(PREDECESSOR)), None) if result is None: return None - return cast(pystac.Item, result) + return cast(ps.Item, result) @predecessor.setter - def predecessor(self, source_item: pystac.Item) -> None: + def predecessor(self, source_item: ps.Item) -> None: self.item.clear_links(PREDECESSOR) if source_item: - self.item.add_link(pystac.Link(PREDECESSOR, source_item, MEDIA_TYPE)) + self.item.add_link(ps.Link(PREDECESSOR, source_item, MEDIA_TYPE)) @property - def successor(self) -> Optional[pystac.Item]: + def successor(self) -> Optional[ps.Item]: """Get or sets the next item. Returns: Item or None """ - result = next(self.item.get_stac_objects(SUCCESSOR), None) + result = next(iter(self.item.get_stac_objects(SUCCESSOR)), None) if result is None: return None - return cast(pystac.Item, result) + return cast(ps.Item, result) @successor.setter - def successor(self, source_item: pystac.Item) -> None: + def successor(self, source_item: ps.Item) -> None: self.item.clear_links(SUCCESSOR) if source_item: - self.item.add_link(pystac.Link(SUCCESSOR, source_item, MEDIA_TYPE)) + self.item.add_link(ps.Link(SUCCESSOR, source_item, MEDIA_TYPE)) @classmethod - def from_item(cls, an_item: pystac.Item): + def from_item(cls, an_item: ps.Item) -> "VersionItemExt": return cls(an_item) @classmethod @@ -179,9 +179,9 @@ class VersionCollectionExt(base.CollectionExtension): Using VersionCollectionExt to directly wrap a collection will add the 'version' extension ID to the collections's stac_extensions. """ - collection: pystac.Collection + collection: ps.Collection - def __init__(self, a_collection) -> None: + def __init__(self, a_collection: ps.Collection) -> None: self.collection = a_collection @property @@ -210,67 +210,67 @@ def deprecated(self) -> bool: return bool(self.collection.extra_fields.get(DEPRECATED)) @deprecated.setter - def deprecated(self, v) -> None: + def deprecated(self, v: bool) -> None: if not isinstance(v, bool): - raise pystac.STACError(DEPRECATED + ' must be a bool') + raise ps.STACError(DEPRECATED + ' must be a bool') self.collection.extra_fields[DEPRECATED] = v @property - def latest(self) -> Optional[pystac.Collection]: + def latest(self) -> Optional[ps.Collection]: """Get or sets the most recent collection. Returns: Collection or None """ - result = next(self.collection.get_stac_objects(LATEST), None) + result = next(iter(self.collection.get_stac_objects(LATEST)), None) if result is None: return None - return cast(pystac.Collection, result) + return cast(ps.Collection, result) @latest.setter - def latest(self, source_collection: pystac.Collection) -> None: + def latest(self, source_collection: ps.Collection) -> None: self.collection.clear_links(LATEST) if source_collection: - self.collection.add_link(pystac.Link(LATEST, source_collection, MEDIA_TYPE)) + self.collection.add_link(ps.Link(LATEST, source_collection, MEDIA_TYPE)) @property - def predecessor(self) -> Optional[pystac.Collection]: + def predecessor(self) -> Optional[ps.Collection]: """Get or sets the previous collection. Returns: Collection or None """ - result = next(self.collection.get_stac_objects(PREDECESSOR), None) + result = next(iter(self.collection.get_stac_objects(PREDECESSOR)), None) if result is None: return None - return cast(pystac.Collection, result) + return cast(ps.Collection, result) @predecessor.setter - def predecessor(self, source_collection: pystac.Collection) -> None: + def predecessor(self, source_collection: ps.Collection) -> None: self.collection.clear_links(PREDECESSOR) if source_collection: - self.collection.add_link(pystac.Link(PREDECESSOR, source_collection, MEDIA_TYPE)) + self.collection.add_link(ps.Link(PREDECESSOR, source_collection, MEDIA_TYPE)) @property - def successor(self) -> Optional[pystac.Collection]: + def successor(self) -> Optional[ps.Collection]: """Get or sets the next collection. Returns: Collection or None """ - result = next(self.collection.get_stac_objects(SUCCESSOR), None) + result = next(iter(self.collection.get_stac_objects(SUCCESSOR)), None) if result is None: return None - return cast(pystac.Collection, result) + return cast(ps.Collection, result) @successor.setter - def successor(self, source_collection: pystac.Collection) -> None: + def successor(self, source_collection: ps.Collection) -> None: self.collection.clear_links(SUCCESSOR) if source_collection: - self.collection.add_link(pystac.Link(SUCCESSOR, source_collection, MEDIA_TYPE)) + self.collection.add_link(ps.Link(SUCCESSOR, source_collection, MEDIA_TYPE)) @classmethod - def from_collection(cls, a_collection: pystac.Collection): + def from_collection(cls, a_collection: ps.Collection) -> "VersionCollectionExt": return cls(a_collection) @classmethod @@ -279,10 +279,10 @@ def _object_links(cls) -> List[str]: def apply(self, version: str, - deprecated: Optional[str] = None, - latest: Optional[pystac.Collection] = None, - predecessor=None, - successor=None) -> None: + deprecated: Optional[bool] = None, + latest: Optional[ps.Collection] = None, + predecessor: Optional[ps.Collection] = None, + successor: Optional[ps.Collection] = None) -> None: """Applies version extension properties to the extended Collection. Args: @@ -305,7 +305,8 @@ def apply(self, self.successor = successor -VERSION_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.VERSION, [ - base.ExtendedObject(pystac.Item, VersionItemExt), - base.ExtendedObject(pystac.Collection, VersionCollectionExt) -]) +VERSION_EXTENSION_DEFINITION: base.ExtensionDefinition = base.ExtensionDefinition( + Extensions.VERSION, [ + base.ExtendedObject(ps.Item, VersionItemExt), + base.ExtendedObject(ps.Collection, VersionCollectionExt) + ]) diff --git a/pystac/extensions/view.py b/pystac/extensions/view.py index 9d8554ebd..cab79ea87 100644 --- a/pystac/extensions/view.py +++ b/pystac/extensions/view.py @@ -34,7 +34,7 @@ def apply(self, incidence_angle: Optional[float] = None, azimuth: Optional[float] = None, sun_azimuth: Optional[float] = None, - sun_elevation: Optional[float] = None): + sun_elevation: Optional[float] = None) -> None: """Applies View Geometry extension properties to the extended Item. Args: @@ -78,7 +78,7 @@ def off_nadir(self) -> Optional[float]: return self.get_off_nadir() @off_nadir.setter - def off_nadir(self, v: Optional[float]): + def off_nadir(self, v: Optional[float]) -> None: self.set_off_nadir(v) def get_off_nadir(self, asset: Optional[Asset] = None) -> Optional[float]: @@ -95,7 +95,7 @@ def get_off_nadir(self, asset: Optional[Asset] = None) -> Optional[float]: else: return asset.properties.get('view:off_nadir') - def set_off_nadir(self, off_nadir: Optional[float], asset: Optional[Asset] = None): + def set_off_nadir(self, off_nadir: Optional[float], asset: Optional[Asset] = None) -> None: """Set an Item or an Asset off_nadir. If an Asset is supplied, sets the property on the Asset. @@ -132,7 +132,9 @@ def get_incidence_angle(self, asset: Optional[Asset] = None) -> Optional[float]: else: return asset.properties.get('view:incidence_angle') - def set_incidence_angle(self, incidence_angle: Optional[float], asset: Optional[Asset] = None): + def set_incidence_angle(self, + incidence_angle: Optional[float], + asset: Optional[Asset] = None) -> None: """Set an Item or an Asset incidence_angle. If an Asset is supplied, sets the property on the Asset. @@ -260,5 +262,5 @@ def from_item(cls, item: Item) -> "ViewItemExt": return cls(item) -VIEW_EXTENSION_DEFINITION = ExtensionDefinition(Extensions.VIEW, - [ExtendedObject(Item, ViewItemExt)]) +VIEW_EXTENSION_DEFINITION: ExtensionDefinition = ExtensionDefinition( + Extensions.VIEW, [ExtendedObject(Item, ViewItemExt)]) diff --git a/pystac/item.py b/pystac/item.py index d0c881c4b..a0c71bf82 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -36,7 +36,7 @@ def title(self) -> Optional[str]: return self.properties.get('title') @title.setter - def title(self, v: Optional[str]): + def title(self, v: Optional[str]) -> None: self.properties['title'] = v @property @@ -49,7 +49,7 @@ def description(self) -> Optional[str]: return self.properties.get('description') @description.setter - def description(self, v: Optional[str]): + def description(self, v: Optional[str]) -> None: self.properties['description'] = v # Date and Time Range @@ -232,11 +232,18 @@ def set_providers(self, If an Asset is supplied, sets the property on the Asset. Otherwise sets the Item's value. """ - providers_dicts = [d.to_dict() for d in providers] if asset is None: - self.properties['providers'] = providers_dicts + if providers is None: + self.properties.pop('providers', None) + else: + providers_dicts = [d.to_dict() for d in providers] + self.properties['providers'] = providers_dicts else: - asset.properties['providers'] = providers_dicts + if providers is None: + asset.properties.pop('providers', None) + else: + providers_dicts = [d.to_dict() for d in providers] + asset.properties['providers'] = providers_dicts # Instrument @property @@ -599,7 +606,7 @@ def __init__(self, self.properties = {} # The Item which owns this Asset. - self.owner = None + self.owner: Optional[Item] = None def set_owner(self, item: "Item") -> None: """Sets the owning item of this Asset. @@ -655,7 +662,7 @@ def to_dict(self) -> Dict[str, Any]: return d - def clone(self): + def clone(self) -> "Asset": """Clones this asset. Returns: @@ -668,7 +675,7 @@ def clone(self): roles=self.roles, properties=self.properties) - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.href) @staticmethod @@ -783,15 +790,14 @@ def __init__(self, if href is not None: self.set_self_href(href) + self.collection_id: Optional[str] = None if collection is not None: if isinstance(collection, Collection): self.set_collection(collection) else: self.collection_id = collection - else: - self.collection_id = None - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.id) def set_self_href(self, href: str) -> None: @@ -950,7 +956,7 @@ def get_collection(self) -> Optional[Collection]: def to_dict(self, include_self_link: bool = True) -> Dict[str, Any]: links = self.links if not include_self_link: - links = filter(lambda x: x.rel != 'self', links) + links = [x for x in links if x.rel != 'self'] assets = dict(map(lambda x: (x[0], x[1].to_dict()), self.assets.items())) diff --git a/pystac/link.py b/pystac/link.py index 0490ecaa7..6c0aeff28 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -6,10 +6,10 @@ from pystac.utils import (make_absolute_href, make_relative_href, is_absolute_href) if TYPE_CHECKING: - from pystac.stac_object import STACObject - from pystac.item import Item - from pystac.catalog import Catalog - from pystac.collection import Collection + from pystac.stac_object import STACObject as STACObjectType + from pystac.item import Item as ItemType + from pystac.catalog import Catalog as CatalogType + from pystac.collection import Collection as CollectionType HIERARCHICAL_LINKS = ['root', 'child', 'parent', 'collection', 'item', 'items'] @@ -58,18 +58,18 @@ class Link: """ def __init__(self, rel: str, - target: Union[str, "STACObject"], + target: Union[str, "STACObjectType"], media_type: Optional[str] = None, title: Optional[str] = None, properties: Optional[Dict[str, Any]] = None) -> None: self.rel = rel - self.target: Union[str, "STACObject"] = target # An object or an href + self.target: Union[str, "STACObjectType"] = target # An object or an href self.media_type = media_type self.title = title self.properties = properties - self.owner = None + self.owner: Optional["STACObjectType"] = None - def set_owner(self, owner: "STACObject") -> "Link": + def set_owner(self, owner: "STACObjectType") -> "Link": """Sets the owner of this link. Args: @@ -110,7 +110,7 @@ def get_href(self) -> Optional[str]: rel_links = HIERARCHICAL_LINKS + \ ps.STAC_EXTENSIONS.get_extended_object_links(self.owner) # if a hierarchical link with an owner and root, and relative catalog - if root.is_relative() and self.rel in rel_links: + if root and root.is_relative() and self.rel in rel_links: owner_href = self.owner.get_self_href() if owner_href is not None: href = make_relative_href(href, owner_href) @@ -147,10 +147,10 @@ def get_absolute_href(self) -> Optional[str]: return href - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.rel, self.target) - def resolve_stac_object(self, root: Optional["Catalog"] = None) -> "Link": + def resolve_stac_object(self, root: Optional["CatalogType"] = None) -> "Link": """Resolves a STAC object from the HREF of this link, if the link is not already resolved. @@ -195,7 +195,7 @@ def resolve_stac_object(self, root: Optional["Catalog"] = None) -> "Link": return self - def is_resolved(self): + def is_resolved(self) -> bool: """Determines if the link's target is a resolved STACObject. Returns: @@ -226,7 +226,7 @@ def to_dict(self) -> Dict[str, Any]: return d - def clone(self): + def clone(self) -> "Link": """Clones this link. This makes a copy of all link information, but does not clone a STACObject @@ -261,17 +261,17 @@ def from_dict(d: Dict[str, Any]) -> "Link": return Link(rel=rel, target=href, media_type=media_type, title=title, properties=properties) @staticmethod - def root(c: "Catalog") -> "Link": + def root(c: "CatalogType") -> "Link": """Creates a link to a root Catalog or Collection.""" return Link('root', c, media_type='application/json') @staticmethod - def parent(c: "Catalog") -> "Link": + def parent(c: "CatalogType") -> "Link": """Creates a link to a parent Catalog or Collection.""" return Link('parent', c, media_type='application/json') @staticmethod - def collection(c: "Collection") -> "Link": + def collection(c: "CollectionType") -> "Link": """Creates a link to an item's Collection.""" return Link('collection', c, media_type='application/json') @@ -281,11 +281,11 @@ def self_href(href: str) -> "Link": return Link('self', href, media_type='application/json') @staticmethod - def child(c: "Catalog", title: Optional[str] = None) -> "Link": + def child(c: "CatalogType", title: Optional[str] = None) -> "Link": """Creates a link to a child Catalog or Collection.""" return Link('child', c, title=title, media_type='application/json') @staticmethod - def item(item: "Item", title: Optional[str] = None) -> "Link": + def item(item: "ItemType", title: Optional[str] = None) -> "Link": """Creates a link to an Item.""" return Link('item', item, title=title, media_type='application/json') diff --git a/pystac/media_type.py b/pystac/media_type.py index 5bc4c2798..d9e2d50e0 100644 --- a/pystac/media_type.py +++ b/pystac/media_type.py @@ -4,7 +4,7 @@ class MediaType(str, Enum): """A list of common media types that can be used in STAC Asset and Link metadata. """ - def __str__(self): + def __str__(self) -> str: return str(self.value) COG = 'image/tiff; application=geotiff; profile=cloud-optimized' diff --git a/pystac/serialization/common_properties.py b/pystac/serialization/common_properties.py index 05a249762..72ea7b925 100644 --- a/pystac/serialization/common_properties.py +++ b/pystac/serialization/common_properties.py @@ -1,10 +1,9 @@ -from typing import Any, Dict, Iterable, Optional, Union, cast -from pystac.utils import make_absolute_href -from pystac.stac_io import STAC_IO -from pystac.serialization.identify import STACVersionID +from typing import Any, Dict, Iterable, List, Optional, Union, cast import pystac as ps from pystac.cache import CollectionCache +from pystac.serialization.identify import STACVersionID +from pystac.utils import make_absolute_href def merge_common_properties(item_dict: Dict[str, Any], @@ -59,7 +58,7 @@ def merge_common_properties(item_dict: Dict[str, Any], if isinstance(item_dict['links'], dict): links = list(cast(Iterable[Dict[str, Any]], item_dict['links'].values())) else: - links = cast(Iterable[Dict[str, Any]], item_dict['links']) + links = cast(List[Dict[str, Any]], item_dict['links']) collection_link = next((link for link in links if link['rel'] == 'collection'), None) if collection_link is not None: @@ -71,7 +70,7 @@ def merge_common_properties(item_dict: Dict[str, Any], collection = collection_cache.get_by_href(collection_href) if collection is None: - collection = STAC_IO.read_json(collection_href) + collection = ps.STAC_IO.read_json(collection_href) if collection is not None: collection_id = None diff --git a/pystac/serialization/identify.py b/pystac/serialization/identify.py index 8c4216830..b434fd198 100644 --- a/pystac/serialization/identify.py +++ b/pystac/serialization/identify.py @@ -1,10 +1,13 @@ from functools import total_ordering -from typing import Any, Dict, List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union, cast -import pystac +import pystac as ps from pystac.version import STACVersion from pystac.extensions import Extensions +if TYPE_CHECKING: + from pystac.stac_object import STACObjectType as STACObjectTypeType + @total_ordering class STACVersionID: @@ -103,7 +106,7 @@ def is_later_than(self, v: Union[str, STACVersionID]) -> bool: v = STACVersionID(v) return v < self.min_version - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.min_version, self.max_version) @@ -119,7 +122,7 @@ class STACJSONDescription: custom_extensions (List[str]): List of custom extensions (URIs to JSON Schemas) used by this STAC Object. """ - def __init__(self, object_type: str, version_range: STACVersionRange, + def __init__(self, object_type: "STACObjectTypeType", version_range: STACVersionRange, common_extensions: List[str], custom_extensions: List[str]) -> None: self.object_type = object_type self.version_range = version_range @@ -144,7 +147,7 @@ def _identify_stac_extensions(object_type: str, d: Dict[str, Any], # assets (collection assets) - if object_type == pystac.STACObjectType.ITEMCOLLECTION: + if object_type == ps.STACObjectType.ITEMCOLLECTION: if 'assets' in d: stac_extensions.add('assets') version_range.set_min(STACVersionID('0.8.0')) @@ -173,19 +176,19 @@ def _identify_stac_extensions(object_type: str, d: Dict[str, Any], version_range.set_min(STACVersionID('0.6.2')) # datacube - if object_type == pystac.STACObjectType.ITEM: + if object_type == ps.STACObjectType.ITEM: if any(k.startswith('cube:') for k in cast(Dict[str, Any], d['properties'])): stac_extensions.add(Extensions.DATACUBE) version_range.set_min(STACVersionID('0.6.1')) # datetime-range (old extension) - if object_type == pystac.STACObjectType.ITEM: + if object_type == ps.STACObjectType.ITEM: if 'dtr:start_datetime' in d['properties']: stac_extensions.add('datetime-range') version_range.set_min(STACVersionID('0.6.0')) # eo - if object_type == pystac.STACObjectType.ITEM: + if object_type == ps.STACObjectType.ITEM: if any(k.startswith('eo:') for k in cast(Dict[str, Any], d['properties'])): stac_extensions.add(Extensions.EO) if 'eo:epsg' in d['properties']: @@ -200,13 +203,13 @@ def _identify_stac_extensions(object_type: str, d: Dict[str, Any], version_range.set_max(STACVersionID('0.5.2')) # pointcloud - if object_type == pystac.STACObjectType.ITEM: + if object_type == ps.STACObjectType.ITEM: if any(k.startswith('pc:') for k in cast(Dict[str, Any], d['properties'])): stac_extensions.add(Extensions.POINTCLOUD) version_range.set_min(STACVersionID('0.6.2')) # sar - if object_type == pystac.STACObjectType.ITEM: + if object_type == ps.STACObjectType.ITEM: if any(k.startswith('sar:') for k in cast(Dict[str, Any], d['properties'])): stac_extensions.add(Extensions.SAR) version_range.set_min(STACVersionID('0.6.2')) @@ -233,7 +236,7 @@ def _identify_stac_extensions(object_type: str, d: Dict[str, Any], version_range.set_max(STACVersionID('0.6.2')) # scientific - if object_type == pystac.STACObjectType.ITEM or object_type == pystac.STACObjectType.COLLECTION: + if object_type == ps.STACObjectType.ITEM or object_type == ps.STACObjectType.COLLECTION: if 'properties' in d: prop_keys = cast(Dict[str, Any], d['properties']).keys() if any(k.startswith('sci:') for k in prop_keys): @@ -241,7 +244,7 @@ def _identify_stac_extensions(object_type: str, d: Dict[str, Any], version_range.set_min(STACVersionID('0.6.0')) # Single File STAC - if object_type == pystac.STACObjectType.ITEMCOLLECTION: + if object_type == ps.STACObjectType.ITEMCOLLECTION: if 'collections' in d: stac_extensions.add(Extensions.SINGLE_FILE_STAC) version_range.set_min(STACVersionID('0.8.0')) @@ -266,7 +269,7 @@ def _split_extensions(stac_extensions: List[str]) -> Tuple[List[str], List[str]] return (common_extensions, custom_extensions) -def identify_stac_object_type(json_dict: Dict[str, Any]): +def identify_stac_object_type(json_dict: Dict[str, Any]) -> "STACObjectTypeType": """Determines the STACObjectType of the provided JSON dict. Args: @@ -281,14 +284,14 @@ def identify_stac_object_type(json_dict: Dict[str, Any]): if 'type' in json_dict and 'assets' not in json_dict: if 'stac_version' in json_dict and cast(str, json_dict['stac_version']).startswith('0'): if json_dict['type'] == 'FeatureCollection': - object_type = pystac.STACObjectType.ITEMCOLLECTION + object_type = ps.STACObjectType.ITEMCOLLECTION if 'extent' in json_dict: - object_type = pystac.STACObjectType.COLLECTION + object_type = ps.STACObjectType.COLLECTION elif 'assets' in json_dict: - object_type = pystac.STACObjectType.ITEM + object_type = ps.STACObjectType.ITEM else: - object_type = pystac.STACObjectType.CATALOG + object_type = ps.STACObjectType.CATALOG return object_type @@ -311,10 +314,10 @@ def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription: stac_extensions = json_dict.get('stac_extensions', None) if stac_version is None: - if (object_type == pystac.STACObjectType.CATALOG - or object_type == pystac.STACObjectType.COLLECTION): + if (object_type == ps.STACObjectType.CATALOG + or object_type == ps.STACObjectType.COLLECTION): version_range.set_max(STACVersionID('0.5.2')) - elif object_type == pystac.STACObjectType.ITEM: + elif object_type == ps.STACObjectType.ITEM: version_range.set_max(STACVersionID('0.7.0')) else: # ItemCollection version_range.set_min(STACVersionID('0.8.0')) @@ -330,7 +333,7 @@ def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription: # but ItemCollection (except after 0.9.0, when ItemCollection also got # the stac_extensions property). if version_range.is_earlier_than('0.8.0') or \ - (object_type == pystac.STACObjectType.ITEMCOLLECTION and not version_range.is_later_than( + (object_type == ps.STACObjectType.ITEMCOLLECTION and not version_range.is_later_than( '0.8.1')): stac_extensions = _identify_stac_extensions(object_type, json_dict, version_range) else: diff --git a/pystac/serialization/migrate.py b/pystac/serialization/migrate.py index 3442be738..a7f0f9bcf 100644 --- a/pystac/serialization/migrate.py +++ b/pystac/serialization/migrate.py @@ -50,16 +50,17 @@ def _migrate_item_assets(d: Dict[str, Any], version: STACVersionID, if 'assets' in d: d['item_assets'] = d['assets'] del d['assets'] + return None def _migrate_checksum(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: - pass + return None def _migrate_datacube(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: - pass + return None def _migrate_datetime_range(d: Dict[str, Any], version: STACVersionID, @@ -74,6 +75,8 @@ def _migrate_datetime_range(d: Dict[str, Any], version: STACVersionID, d['properties']['end_datetime'] = d['properties']['dtr:end_datetime'] del d['properties']['dtr:end_datetime'] + return None + def _migrate_eo(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: @@ -159,7 +162,8 @@ def _migrate_eo(d: Dict[str, Any], version: STACVersionID, return added_extensions -def _migrate_label(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> None: +def _migrate_label(d: Dict[str, Any], version: STACVersionID, + info: STACJSONDescription) -> Optional[Set[str]]: if info.object_type == ps.STACObjectType.ITEM and version < '1.0.0': props = d['properties'] # Migrate 0.8.0-rc1 non-pluralized forms @@ -180,10 +184,12 @@ def _migrate_label(d: Dict[str, Any], version: STACVersionID, info: STACJSONDesc props['label:methods'] = props['label:method'] del props['label:method'] + return None + def _migrate_pointcloud(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: - pass + return None def _migrate_sar(d: Dict[str, Any], version: STACVersionID, @@ -202,15 +208,17 @@ def _migrate_sar(d: Dict[str, Any], version: STACVersionID, d['properties']['constellation'] = d['properties']['sar:constellation'] del d['properties']['sar:constellation'] + return None + def _migrate_scientific(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: - pass + return None def _migrate_single_file_stac(d: Dict[str, Any], version: STACVersionID, info: STACJSONDescription) -> Optional[Set[str]]: - pass + return None def _get_object_migrations( diff --git a/pystac/stac_io.py b/pystac/stac_io.py index c213f0a0d..d00693b1e 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -6,9 +6,11 @@ from urllib.request import urlopen from urllib.error import HTTPError +import pystac.serialization + if TYPE_CHECKING: - from pystac.stac_object import STACObject - from pystac.catalog import Catalog + from pystac.stac_object import STACObject as STACObjectType + from pystac.catalog import Catalog as CatalogType class STAC_IO: @@ -55,9 +57,11 @@ def default_write_text_method(uri: str, txt: str) -> None: cloud storage. """ - # Replaced in __init__ to account for extension objects. - stac_object_from_dict: Optional[Callable[[Dict[str, Any], Optional[str], Optional["Catalog"]], - "STACObject"]] = None + @staticmethod + def stac_object_from_dict(d: Dict[str, Any], + href: Optional[str] = None, + root: Optional["CatalogType"] = None) -> "STACObjectType": + return pystac.serialization.stac_object_from_dict(d, href, root) # This is set in __init__.py _STAC_OBJECT_CLASSES = None @@ -116,7 +120,7 @@ def read_json(cls, uri: str) -> Dict[str, Any]: return json.loads(STAC_IO.read_text(uri)) @classmethod - def read_stac_object(cls, uri: str, root: Optional["Catalog"] = None) -> "STACObject": + def read_stac_object(cls, uri: str, root: Optional["CatalogType"] = None) -> "STACObjectType": """Read a STACObject from a JSON file at the given URI. Args: diff --git a/pystac/stac_object.py b/pystac/stac_object.py index ed0f8d9fb..65c10e366 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -1,6 +1,6 @@ from abc import (ABC, abstractmethod) from enum import Enum -from typing import Any, Dict, Generator, List, Optional, cast, TYPE_CHECKING +from typing import Any, Dict, Iterable, List, Optional, cast, TYPE_CHECKING import pystac as ps from pystac import STACError @@ -15,7 +15,7 @@ class STACObjectType(str, Enum): - def __str__(self): + def __str__(self) -> str: return str(self.value) CATALOG = 'CATALOG' @@ -33,7 +33,7 @@ class LinkMixin: links: List[Link] - def add_link(self, link: Link): + def add_link(self, link: Link) -> None: """Add a link to this object's set of links. Args: @@ -41,9 +41,8 @@ def add_link(self, link: Link): """ link.set_owner(cast(STACObject, self)) self.links.append(link) - return self - def add_links(self, links: List[Link]) -> "LinkMixin": + def add_links(self, links: List[Link]) -> None: """Add links to this object's set of links. Args: @@ -52,9 +51,8 @@ def add_links(self, links: List[Link]) -> "LinkMixin": for link in links: self.add_link(link) - return self - def remove_links(self, rel: str) -> "LinkMixin": + def remove_links(self, rel: str) -> None: """Remove links to this object's set of links that match the given ``rel``. Args: @@ -62,7 +60,6 @@ def remove_links(self, rel: str) -> "LinkMixin": """ self.links = [link for link in self.links if link.rel != rel] - return self def get_single_link(self, rel: str) -> Optional[Link]: """Get single link that match the given ``rel``. @@ -73,7 +70,7 @@ def get_single_link(self, rel: str) -> Optional[Link]: return next((link for link in self.links if link.rel == rel), None) - def get_links(self, rel: Optional[str] = None): + def get_links(self, rel: Optional[str] = None) -> List[Link]: """Gets the :class:`~pystac.Link` instances associated with this object. Args: @@ -89,7 +86,7 @@ def get_links(self, rel: Optional[str] = None): else: return [link for link in self.links if link.rel == rel] - def clear_links(self, rel: Optional[str] = None): + def clear_links(self, rel: Optional[str] = None) -> None: """Clears all :class:`~pystac.Link` instances associated with this object. Args: @@ -99,9 +96,8 @@ def clear_links(self, rel: Optional[str] = None): self.links = [link for link in self.links if link.rel != rel] else: self.links = [] - return self - def get_root_link(self): + def get_root_link(self) -> Optional[Link]: """Get the :class:`~pystac.Link` representing the root for this object. @@ -146,7 +142,7 @@ def get_self_href(self) -> Optional[str]: else: return None - def set_self_href(self, href: str) -> "LinkMixin": + def set_self_href(self, href: str) -> None: """Sets the absolute HREF that is represented by the ``rel == 'self'`` :class:`~pystac.Link`. @@ -167,8 +163,6 @@ def set_self_href(self, href: str) -> "LinkMixin": if root_link is not None and root_link.is_resolved(): cast(ps.Catalog, root_link.target)._resolved_objects.cache(cast(STACObject, self)) - return self - class STACObject(LinkMixin, ABC): """A STACObject is the base class for any element of STAC that @@ -182,9 +176,9 @@ class STACObject(LinkMixin, ABC): """ id: str - STAC_OBJECT_TYPE = None # Overridden by the child classes with their type. + STAC_OBJECT_TYPE: STACObjectType - def __init__(self, stac_extensions: List[str]): + def __init__(self, stac_extensions: List[str]) -> None: self.links = [] self.stac_extensions = stac_extensions @@ -220,7 +214,7 @@ def get_root(self) -> Optional["CatalogType"]: else: return None - def set_root(self, root: Optional["CatalogType"]) -> "STACObject": + def set_root(self, root: Optional["CatalogType"]) -> None: """Sets the root :class:`~pystac.Catalog` or :class:`~pystac.Collection` for this object. @@ -248,8 +242,6 @@ def set_root(self, root: Optional["CatalogType"]) -> "STACObject": self.add_link(new_root_link) root._resolved_objects.cache(self) - return self - def get_parent(self) -> Optional["CatalogType"]: """Get the :class:`~pystac.Catalog` or :class:`~pystac.Collection` to the parent for this object. The root is represented by a @@ -266,7 +258,7 @@ def get_parent(self) -> Optional["CatalogType"]: else: return None - def set_parent(self, parent: Optional["CatalogType"]) -> "STACObject": + def set_parent(self, parent: Optional["CatalogType"]) -> None: """Sets the parent :class:`~pystac.Catalog` or :class:`~pystac.Collection` for this object. @@ -278,9 +270,8 @@ def set_parent(self, parent: Optional["CatalogType"]) -> "STACObject": self.remove_links('parent') if parent is not None: self.add_link(Link.parent(parent)) - return self - def get_stac_objects(self, rel: str) -> Generator["STACObject", None, None]: + def get_stac_objects(self, rel: str) -> Iterable["STACObject"]: """Gets the :class:`~pystac.STACObject` instances that are linked to by links with their ``rel`` property matching the passed in argument. @@ -289,7 +280,7 @@ def get_stac_objects(self, rel: str) -> Generator["STACObject", None, None]: ``rel`` property against. Returns: - Generator[STACObjects]: A possibly empty generator of STACObjects that are + Iterable[STACObjects]: A possibly empty iterable of STACObjects that are connected to this object through links with the given ``rel``. """ links = self.links[:] @@ -355,15 +346,17 @@ def full_copy(self, if link.rel in link_rels: link.resolve_stac_object() target = cast("STACObject", link.target) - if target in root._resolved_objects: - target = root._resolved_objects.get(target) - assert target is not None + if root is not None and target in root._resolved_objects: + cached_target = root._resolved_objects.get(target) + assert cached_target is not None + target = cached_target else: target_parent = None if link.rel in ['child', 'item'] and isinstance(clone, ps.Catalog): target_parent = clone copied_target = target.full_copy(root=root, parent=target_parent) - root._resolved_objects.cache(copied_target) + if root is not None: + root._resolved_objects.cache(copied_target) target = copied_target if link.rel in ['child', 'item']: target.set_root(root) @@ -390,7 +383,7 @@ def ext(self) -> "ExtensionIndex": """ return ExtensionIndex(self) - def resolve_links(self): + def resolve_links(self) -> None: """Ensure all STACObjects linked to by this STACObject are resolved. This is important for operations such as changing HREFs. diff --git a/pystac/utils.py b/pystac/utils.py index f0b30f709..b8ea00576 100644 --- a/pystac/utils.py +++ b/pystac/utils.py @@ -180,7 +180,7 @@ def geometry_to_bbox(geometry: Dict[str, Any]) -> List[float]: lats: List[float] = [] lons: List[float] = [] - def extract_coords(coords: List[Union[List[float], List[List[Any]]]]): + def extract_coords(coords: List[Union[List[float], List[List[Any]]]]) -> None: for x in coords: # This handles points if isinstance(x, float): diff --git a/pystac/validation/__init__.py b/pystac/validation/__init__.py index 3f504ad2f..e7c2b55aa 100644 --- a/pystac/validation/__init__.py +++ b/pystac/validation/__init__.py @@ -1,11 +1,13 @@ # flake8: noqa from typing import Dict, List, Any, Optional, cast, TYPE_CHECKING -import pystac + +import pystac as ps from pystac.serialization.identify import identify_stac_object from pystac.utils import make_absolute_href if TYPE_CHECKING: - from pystac.stac_object import STACObject + from pystac.stac_object import STACObject as STACObjectType + from pystac.stac_object import STACObjectType as STACObjectTypeType class STACValidationError(Exception): @@ -26,7 +28,7 @@ def __init__(self, message: str, source: Optional[Any] = None): from pystac.validation.stac_validator import (STACValidator, JsonSchemaSTACValidator) -def validate(stac_object: "STACObject") -> List[Any]: +def validate(stac_object: "STACObjectType") -> List[Any]: """Validates a :class:`~pystac.STACObject`. Args: @@ -42,13 +44,13 @@ def validate(stac_object: "STACObject") -> List[Any]: """ return validate_dict(stac_dict=stac_object.to_dict(), stac_object_type=stac_object.STAC_OBJECT_TYPE, - stac_version=pystac.get_stac_version(), + stac_version=ps.get_stac_version(), extensions=stac_object.stac_extensions, href=stac_object.get_self_href()) def validate_dict(stac_dict: Dict[str, Any], - stac_object_type: Optional[str] = None, + stac_object_type: Optional["STACObjectTypeType"] = None, stac_version: Optional[str] = None, extensions: Optional[List[str]] = None, href: Optional[str] = None) -> List[Any]: @@ -119,19 +121,19 @@ def validate_all(stac_dict: Dict[str, Any], href: str) -> None: extensions=info.common_extensions, href=href) - if info.object_type != pystac.STACObjectType.ITEM: + if info.object_type != ps.STACObjectType.ITEM: if 'links' in stac_dict: # Account for 0.6 links if isinstance(stac_dict['links'], dict): links: List[Dict[str, Any]] = list(stac_dict['links'].values()) else: - links: List[Dict[str, Any]] = cast(List[Dict[str, Any]], stac_dict.get('links')) + links = cast(List[Dict[str, Any]], stac_dict.get('links')) for link in links: rel = link.get('rel') if rel in ['item', 'child']: link_href = make_absolute_href(cast(str, link.get('href')), start_href=href) if link_href is not None: - d = pystac.STAC_IO.read_json(link_href) + d = ps.STAC_IO.read_json(link_href) validate_all(d, link_href) @@ -159,7 +161,7 @@ def set_validator(cls, validator: STACValidator) -> None: cls._validator = validator -def set_validator(validator: STACValidator): +def set_validator(validator: STACValidator) -> None: """Sets the STACValidator to use in PySTAC. Args: diff --git a/pystac/validation/schema_uri_map.py b/pystac/validation/schema_uri_map.py index c9f9a7d1f..29ba9b349 100644 --- a/pystac/validation/schema_uri_map.py +++ b/pystac/validation/schema_uri_map.py @@ -1,5 +1,5 @@ from abc import (ABC, abstractmethod) -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple import pystac from pystac import (STACObjectType, Extensions) @@ -9,11 +9,11 @@ class SchemaUriMap(ABC): """Abstract class defining schema URIs for STAC core objects and extensions. """ - def __init__(self): + def __init__(self) -> None: pass @abstractmethod - def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str) -> str: + def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str) -> Optional[str]: """Get the schema URI for the given object type and stac version. Args: @@ -27,7 +27,7 @@ def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str) -> @abstractmethod def get_extension_schema_uri(self, extension_id: str, object_type: STACObjectType, - stac_version: str) -> str: + stac_version: str) -> Optional[str]: """Get the extension's schema URI for the given object type, stac version. Args: @@ -162,7 +162,7 @@ class DefaultSchemaUriMap(SchemaUriMap): } @classmethod - def _append_base_uri_if_needed(cls, uri: str, stac_version: str): + def _append_base_uri_if_needed(cls, uri: str, stac_version: str) -> Optional[str]: # Only append the base URI if it's not already an absolute URI if '://' not in uri: base_uri = None @@ -176,7 +176,7 @@ def _append_base_uri_if_needed(cls, uri: str, stac_version: str): else: return uri - def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str): + def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str) -> Optional[str]: uri = None is_latest = stac_version == pystac.get_stac_version() @@ -194,7 +194,7 @@ def get_core_schema_uri(self, object_type: STACObjectType, stac_version: str): return self._append_base_uri_if_needed(uri, stac_version) def get_extension_schema_uri(self, extension_id: str, object_type: STACObjectType, - stac_version: str): + stac_version: str) -> Optional[str]: uri = None is_latest = stac_version == pystac.get_stac_version() diff --git a/pystac/validation/stac_validator.py b/pystac/validation/stac_validator.py index 2106040fd..f7ce27794 100644 --- a/pystac/validation/stac_validator.py +++ b/pystac/validation/stac_validator.py @@ -25,9 +25,9 @@ class STACValidator(ABC): @abstractmethod def validate_core(self, stac_dict: Dict[str, Any], - stac_object_type: str, + stac_object_type: STACObjectType, stac_version: str, - href: Optional[str] = None): + href: Optional[str] = None) -> Any: """Validate a core stac object. Return value can be None or specific to the implementation. @@ -44,10 +44,10 @@ def validate_core(self, @abstractmethod def validate_extension(self, stac_dict: Dict[str, Any], - stac_object_type: str, + stac_object_type: STACObjectType, stac_version: str, extension_id: str, - href: Optional[str] = None): + href: Optional[str] = None) -> Any: """Validate an extension stac object. Return value can be None or specific to the implementation. @@ -64,10 +64,10 @@ def validate_extension(self, def validate(self, stac_dict: Dict[str, Any], - stac_object_type: str, + stac_object_type: STACObjectType, stac_version: str, extensions: List[str], - href: Optional[str] = None): + href: Optional[str] = None) -> List[Any]: """Validate a STAC object JSON. Args: @@ -118,7 +118,7 @@ class JsonSchemaSTACValidator(STACValidator): Note: This class requires the ``jsonschema`` library to be installed. """ - def __init__(self, schema_uri_map: Optional[SchemaUriMap] = None): + def __init__(self, schema_uri_map: Optional[SchemaUriMap] = None) -> None: if jsonschema is None: raise Exception('Cannot instantiate, requires jsonschema package') @@ -167,7 +167,7 @@ def validate_core(self, stac_dict: Dict[str, Any], stac_object_type: STACObjectType, stac_version: str, - href: Optional[str] = None): + href: Optional[str] = None) -> Optional[str]: """Validate a core stac object. Return value can be None or specific to the implementation. @@ -201,7 +201,7 @@ def validate_extension(self, stac_object_type: STACObjectType, stac_version: str, extension_id: str, - href: Optional[str] = None): + href: Optional[str] = None) -> Optional[str]: """Validate an extension stac object. Return value can be None or specific to the implementation. diff --git a/pystac/version.py b/pystac/version.py index ab69c70f3..7758fda73 100644 --- a/pystac/version.py +++ b/pystac/version.py @@ -44,7 +44,7 @@ def get_stac_version() -> str: return STACVersion.get_stac_version() -def set_stac_version(stac_version: str): +def set_stac_version(stac_version: str) -> None: """Sets the STAC version that PySTAC should use. This is the version that will be set as the "stac_version" property diff --git a/requirements-dev.txt b/requirements-dev.txt index 03a4f079e..5feddb12f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,7 @@ +mypy==0.790 +flake8==3.8.* +yapf==0.30.* + codespell==1.17.1 ipython==7.16.1 jsonschema==3.2.0 @@ -6,7 +10,5 @@ Sphinx==1.8.0 sphinx-autobuild==0.7.1 sphinxcontrib-fulltoc==1.2.0 sphinxcontrib-napoleon==0.7 -flake8==3.8.* -yapf==0.30.* nbsphinx==0.7.1 coverage==5.2.* diff --git a/scripts/test b/scripts/test index 93df987d6..bc1582076 100755 --- a/scripts/test +++ b/scripts/test @@ -17,6 +17,9 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then if [ "${1:-}" = "--help" ]; then usage else + # Types + mypy pystac + # Lint flake8 pystac tests