diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1726c6bde..0081518d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,11 +3,11 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.17.0 hooks: - id: pyupgrade args: - - "--py39-plus" + - "--py310-plus" - repo: local hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5e77e60..37056b60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Add netCDF to pystac.media_type ([#1386](https://github.com/stac-utils/pystac/pull/1386)) +- Add netCDF to pystac.media_type ([#1386](https://github.com/stac-utils/pystac/pull/1386)) - Add convenience method for accessing pystac_client ([#1365](https://github.com/stac-utils/pystac/pull/1365)) ### Changed @@ -20,7 +20,7 @@ ### Removed -- Python 3.9 ([#1384](https://github.com/stac-utils/pystac/pull/1384)) +- Python 3.9 ([#1384](https://github.com/stac-utils/pystac/pull/1384), [#1388](https://github.com/stac-utils/pystac/pull/1388)) ## [v1.10.1] - 2024-05-03 diff --git a/pyproject.toml b/pyproject.toml index e2097f19c..1683a1f58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ test = [ "html5lib~=1.1", "jinja2<4.0", "jsonschema~=4.18", - "mypy~=1.2", + "mypy~=1.11", "orjson~=3.8", "pre-commit~=3.2", "pytest-cov~=5.0", @@ -104,7 +104,7 @@ max-line-length = 88 [tool.ruff] line-length = 88 -select = ["E", "F", "I"] +lint.select = ["E", "F", "I"] [tool.pytest.ini_options] filterwarnings = ["error"] diff --git a/pystac/__init__.py b/pystac/__init__.py index c4f9fa63c..c029ac283 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -2,6 +2,7 @@ """ PySTAC is a library for working with SpatioTemporal Asset Catalogs (STACs) """ + __all__ = [ "__version__", "TemplateError", @@ -44,7 +45,7 @@ import os import warnings -from typing import Any, Optional +from typing import Any from pystac.errors import ( TemplateError, @@ -136,7 +137,7 @@ ) -def read_file(href: HREF, stac_io: Optional[StacIO] = None) -> STACObject: +def read_file(href: HREF, stac_io: StacIO | None = None) -> STACObject: """Reads a STAC object from a file. This method will return either a Catalog, a Collection, or an Item based on what @@ -168,8 +169,8 @@ def read_file(href: HREF, stac_io: Optional[StacIO] = None) -> STACObject: def write_file( obj: STACObject, include_self_link: bool = True, - dest_href: Optional[HREF] = None, - stac_io: Optional[StacIO] = None, + dest_href: HREF | None = None, + stac_io: StacIO | None = None, ) -> None: """Writes a STACObject to a file. @@ -202,9 +203,9 @@ def write_file( def read_dict( d: dict[str, Any], - href: Optional[str] = None, - root: Optional[Catalog] = None, - stac_io: Optional[StacIO] = None, + href: str | None = None, + root: Catalog | None = None, + stac_io: StacIO | None = None, ) -> STACObject: """Reads a :class:`~STACObject` or :class:`~ItemCollection` from a JSON-like dict representing a serialized STAC object. diff --git a/pystac/catalog.py b/pystac/catalog.py index 70df812e0..b2b9c4dd2 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -2,13 +2,12 @@ import os import warnings -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator from copy import deepcopy from itertools import chain from typing import ( TYPE_CHECKING, Any, - Callable, TypeVar, Union, cast, diff --git a/pystac/errors.py b/pystac/errors.py index 6d40e4d8c..d6f322a72 100644 --- a/pystac/errors.py +++ b/pystac/errors.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any class TemplateError(Exception): @@ -28,7 +28,7 @@ def __init__( self, bad_dict: dict[str, Any], expected: type, - extra_message: Optional[str] = "", + extra_message: str | None = "", ): """ Construct an exception with an appropriate error message from bad_dict and the @@ -88,9 +88,7 @@ class RequiredPropertyMissing(Exception): prop: The property that is missing """ - def __init__( - self, obj: Union[str, Any], prop: str, msg: Optional[str] = None - ) -> None: + def __init__(self, obj: str | Any, prop: str, msg: str | None = None) -> None: msg = msg or f"{repr(obj)} does not have required property {prop}" super().__init__(msg) @@ -109,7 +107,7 @@ class STACValidationError(Exception): the ``jsonschema.ValidationError``. """ - def __init__(self, message: str, source: Optional[Any] = None): + def __init__(self, message: str, source: Any | None = None): super().__init__(message) self.source = source diff --git a/pystac/extensions/mgrs.py b/pystac/extensions/mgrs.py index 24b90f494..f9a0cc61d 100644 --- a/pystac/extensions/mgrs.py +++ b/pystac/extensions/mgrs.py @@ -2,7 +2,7 @@ import re from re import Pattern -from typing import Any, Literal, Optional, Union +from typing import Any, Literal, Union import pystac from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension @@ -132,7 +132,7 @@ def validated_grid_square(v: str) -> str: return v -def validated_utm_zone(v: Optional[int]) -> Optional[int]: +def validated_utm_zone(v: int | None) -> int | None: if v is not None and not isinstance(v, int): raise ValueError("Invalid MGRS utm zone: must be None or int") if v is not None and v not in UTM_ZONES: @@ -175,7 +175,7 @@ def apply( self, latitude_band: str, grid_square: str, - utm_zone: Optional[int] = None, + utm_zone: int | None = None, ) -> None: """Applies MGRS extension properties to the extended Item. @@ -189,7 +189,7 @@ def apply( self.utm_zone = validated_utm_zone(utm_zone) @property - def latitude_band(self) -> Optional[str]: + def latitude_band(self) -> str | None: """Get or sets the latitude band of the datasource.""" return self._get_property(LATITUDE_BAND_PROP, str) @@ -200,7 +200,7 @@ def latitude_band(self, v: str) -> None: ) @property - def grid_square(self) -> Optional[str]: + def grid_square(self) -> str | None: """Get or sets the latitude band of the datasource.""" return self._get_property(GRID_SQUARE_PROP, str) @@ -211,12 +211,12 @@ def grid_square(self, v: str) -> None: ) @property - def utm_zone(self) -> Optional[int]: + def utm_zone(self) -> int | None: """Get or sets the latitude band of the datasource.""" return self._get_property(UTM_ZONE_PROP, int) @utm_zone.setter - def utm_zone(self, v: Optional[int]) -> None: + def utm_zone(self, v: int | None) -> None: self._set_property(UTM_ZONE_PROP, validated_utm_zone(v), pop_if_none=True) @classmethod diff --git a/pystac/extensions/version.py b/pystac/extensions/version.py index 9dd7c0eb1..ee394f483 100644 --- a/pystac/extensions/version.py +++ b/pystac/extensions/version.py @@ -429,7 +429,7 @@ def get_object_links(self, so: STACObject) -> list[str] | None: @contextmanager -def ignore_deprecated() -> Generator[None, None, None]: +def ignore_deprecated() -> Generator[None]: """Context manager for suppressing the :class:`pystac.DeprecatedWarning` when creating a deprecated :class:`~pystac.Item` or :class:`~pystac.Collection` from a dictionary.""" diff --git a/pystac/layout.py b/pystac/layout.py index eb8427d30..88c963d12 100644 --- a/pystac/layout.py +++ b/pystac/layout.py @@ -5,8 +5,9 @@ import warnings from abc import ABC, abstractmethod from collections import OrderedDict +from collections.abc import Callable from string import Formatter -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import pystac from pystac.utils import is_file_path diff --git a/pystac/provider.py b/pystac/provider.py index 09be4df8d..06a38ea66 100644 --- a/pystac/provider.py +++ b/pystac/provider.py @@ -1,5 +1,5 @@ from html import escape -from typing import Any, Optional +from typing import Any from pystac.html.jinja_env import get_jinja_env from pystac.utils import StringEnum @@ -36,16 +36,16 @@ class Provider: name: str """The name of the organization or the individual.""" - description: Optional[str] + description: str | None """Optional multi-line description to add further provider information such as processing details for processors and producers, hosting details for hosts or basic contact information.""" - roles: Optional[list[ProviderRole]] + roles: list[ProviderRole] | None """Optional roles of the provider. Any of licensor, producer, processor or host.""" - url: Optional[str] + url: str | None """Optional homepage on which the provider describes the dataset and publishes contact information.""" @@ -56,10 +56,10 @@ class Provider: def __init__( self, name: str, - description: Optional[str] = None, - roles: Optional[list[ProviderRole]] = None, - url: Optional[str] = None, - extra_fields: Optional[dict[str, Any]] = None, + description: str | None = None, + roles: list[ProviderRole] | None = None, + url: str | None = None, + extra_fields: dict[str, Any] | None = None, ): self.name = name self.description = description diff --git a/pystac/serialization/common_properties.py b/pystac/serialization/common_properties.py index 46c1b9218..539bae798 100644 --- a/pystac/serialization/common_properties.py +++ b/pystac/serialization/common_properties.py @@ -1,5 +1,5 @@ from collections.abc import Iterable -from typing import Any, Optional, Union, cast +from typing import Any, cast import pystac from pystac.cache import CollectionCache @@ -9,8 +9,8 @@ def merge_common_properties( item_dict: dict[str, Any], - collection_cache: Optional[CollectionCache] = None, - json_href: Optional[str] = None, + collection_cache: CollectionCache | None = None, + json_href: str | None = None, ) -> bool: """Merges Collection properties into an Item. @@ -29,8 +29,8 @@ def merge_common_properties( """ properties_merged = False - collection: Optional[Union[pystac.Collection, dict[str, Any]]] = None - collection_href: Optional[str] = None + collection: pystac.Collection | dict[str, Any] | None = None + collection_href: str | None = None stac_version = item_dict.get("stac_version") @@ -79,7 +79,7 @@ def merge_common_properties( collection = pystac.StacIO.default().read_json(collection_href) if collection is not None: - collection_props: Optional[dict[str, Any]] = None + collection_props: dict[str, Any] | None = None if isinstance(collection, pystac.Collection): collection_id = collection.id collection_props = collection.extra_fields.get("properties") diff --git a/pystac/serialization/migrate.py b/pystac/serialization/migrate.py index c863f4c1a..c58361869 100644 --- a/pystac/serialization/migrate.py +++ b/pystac/serialization/migrate.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable from copy import deepcopy -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import pystac from pystac.serialization.identify import ( diff --git a/pystac/stac_io.py b/pystac/stac_io.py index a26077777..abe5bc26a 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -4,7 +4,8 @@ import logging import os from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from urllib.error import HTTPError from urllib.request import Request, urlopen diff --git a/pystac/stac_object.py b/pystac/stac_object.py index ca167ba77..a3a4959d1 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -1,12 +1,11 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from html import escape from typing import ( TYPE_CHECKING, Any, - Callable, TypeVar, cast, ) diff --git a/pystac/utils.py b/pystac/utils.py index c60248663..6c0690c78 100644 --- a/pystac/utils.py +++ b/pystac/utils.py @@ -3,11 +3,11 @@ import os import posixpath import warnings +from collections.abc import Callable from datetime import datetime, timezone from enum import Enum from typing import ( Any, - Callable, TypeVar, Union, cast, diff --git a/pystac/validation/schema_uri_map.py b/pystac/validation/schema_uri_map.py index 0eebc9e50..756b2cf4d 100644 --- a/pystac/validation/schema_uri_map.py +++ b/pystac/validation/schema_uri_map.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod +from collections.abc import Callable from functools import lru_cache -from typing import Any, Callable, Optional +from typing import Any import pystac from pystac.serialization import STACVersionRange @@ -17,7 +18,7 @@ def __init__(self) -> None: @abstractmethod def get_object_schema_uri( self, object_type: STACObjectType, stac_version: str - ) -> Optional[str]: + ) -> str | None: """Get the schema URI for the given object type and stac version. Args: @@ -74,7 +75,7 @@ class DefaultSchemaUriMap(SchemaUriMap): } @classmethod - def _append_base_uri_if_needed(cls, uri: str, stac_version: str) -> Optional[str]: + def _append_base_uri_if_needed(cls, uri: str, stac_version: str) -> str | None: # Only append the base URI if it's not already an absolute URI if "://" not in uri: for version_range, f in cls.BASE_URIS: @@ -89,7 +90,7 @@ def _append_base_uri_if_needed(cls, uri: str, stac_version: str) -> Optional[str def get_object_schema_uri( self, object_type: STACObjectType, stac_version: str - ) -> Optional[str]: + ) -> str | None: is_latest = stac_version == pystac.get_stac_version() if object_type not in self.DEFAULT_SCHEMA_MAP: @@ -310,7 +311,7 @@ def get_schema_map(cls) -> dict[str, Any]: @classmethod def _append_base_uri_if_needed( cls, uri: str, stac_version: STACVersionID - ) -> Optional[str]: + ) -> str | None: # Only append the base URI if it's not already an absolute URI if "://" not in uri: for version_range, f in cls.get_base_uris(): @@ -326,7 +327,7 @@ def _append_base_uri_if_needed( @classmethod def get_extension_schema_uri( cls, extension_id: str, object_type: STACObjectType, stac_version: STACVersionID - ) -> Optional[str]: + ) -> str | None: 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 6f31a68b4..5cd2b5ae0 100644 --- a/pystac/validation/stac_validator.py +++ b/pystac/validation/stac_validator.py @@ -2,7 +2,7 @@ import logging import warnings from abc import ABC, abstractmethod -from typing import Any, Optional +from typing import Any import pystac import pystac.utils @@ -46,7 +46,7 @@ def validate_core( stac_dict: dict[str, Any], stac_object_type: STACObjectType, stac_version: str, - href: Optional[str] = None, + href: str | None = None, ) -> Any: """Validate a core stac object. @@ -68,7 +68,7 @@ def validate_extension( stac_object_type: STACObjectType, stac_version: str, extension_id: str, - href: Optional[str] = None, + href: str | None = None, ) -> Any: """Validate an extension stac object. @@ -90,7 +90,7 @@ def validate( stac_object_type: STACObjectType, stac_version: str, extensions: list[str], - href: Optional[str] = None, + href: str | None = None, ) -> list[Any]: """Validate a STAC object JSON. @@ -149,7 +149,7 @@ class JsonSchemaSTACValidator(STACValidator): schema_uri_map: SchemaUriMap schema_cache: dict[str, dict[str, Any]] - def __init__(self, schema_uri_map: Optional[SchemaUriMap] = None) -> None: + def __init__(self, schema_uri_map: SchemaUriMap | None = None) -> None: if not HAS_JSONSCHEMA: raise ImportError("Cannot instantiate, requires jsonschema package") @@ -196,7 +196,7 @@ def _validate_from_uri( stac_dict: dict[str, Any], stac_object_type: STACObjectType, schema_uri: str, - href: Optional[str] = None, + href: str | None = None, ) -> None: try: schema = self._get_schema(schema_uri) @@ -229,8 +229,8 @@ def validate_core( stac_dict: dict[str, Any], stac_object_type: STACObjectType, stac_version: str, - href: Optional[str] = None, - ) -> Optional[str]: + href: str | None = None, + ) -> str | None: """Validate a core stac object. Return value can be None or specific to the implementation. @@ -269,8 +269,8 @@ def validate_extension( stac_object_type: STACObjectType, stac_version: str, extension_id: str, - href: Optional[str] = None, - ) -> Optional[str]: + href: str | None = None, + ) -> str | None: """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 69d847d18..cdc786076 100644 --- a/pystac/version.py +++ b/pystac/version.py @@ -1,5 +1,4 @@ import os -from typing import Optional __version__ = "1.10.1" """Library version""" @@ -13,7 +12,7 @@ class STACVersion: """Latest STAC API version supported by PySTAC""" # Version that holds a user-set STAC version to use. - _override_version: Optional[str] = None + _override_version: str | None = None OVERRIDE_VERSION_ENV_VAR = "PYSTAC_STAC_VERSION_OVERRIDE" @@ -29,7 +28,7 @@ def get_stac_version(cls) -> str: return cls.DEFAULT_STAC_VERSION @classmethod - def set_stac_version(cls, stac_version: Optional[str]) -> None: + def set_stac_version(cls, stac_version: str | None) -> None: cls._override_version = stac_version @@ -48,7 +47,7 @@ def get_stac_version() -> str: return STACVersion.get_stac_version() -def set_stac_version(stac_version: Optional[str]) -> None: +def set_stac_version(stac_version: str | None) -> 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/tests/data-files/get_examples.py b/tests/data-files/get_examples.py index 44fd5ca37..b4979a72e 100644 --- a/tests/data-files/get_examples.py +++ b/tests/data-files/get_examples.py @@ -8,7 +8,7 @@ import os import tempfile from subprocess import call -from typing import Any, Optional +from typing import Any from urllib.error import HTTPError import pystac @@ -16,7 +16,7 @@ def remove_bad_collection(js: dict[str, Any]) -> dict[str, Any]: - links: Optional[list[dict[str, Any]]] = js.get("links") + links: list[dict[str, Any]] | None = js.get("links") if links is not None: filtered_links: list[dict[str, Any]] = [] for link in links: diff --git a/tests/extensions/test_custom.py b/tests/extensions/test_custom.py index 36ac2a421..a1ee2c061 100644 --- a/tests/extensions/test_custom.py +++ b/tests/extensions/test_custom.py @@ -2,7 +2,7 @@ import unittest from datetime import datetime -from typing import Any, Generic, Optional, TypeVar, Union, cast +from typing import Any, Generic, TypeVar, Union, cast import pytest @@ -33,18 +33,18 @@ class CustomExtension( PropertiesExtension, ExtensionManagementMixin[Union[pystac.Catalog, pystac.Collection, pystac.Item]], ): - def __init__(self, obj: Optional[pystac.STACObject]) -> None: + def __init__(self, obj: pystac.STACObject | None) -> None: self.obj = obj - def apply(self, test_prop: Optional[str]) -> None: + def apply(self, test_prop: str | None) -> None: self.test_prop = test_prop @property - def test_prop(self) -> Optional[str]: + def test_prop(self) -> str | None: return self._get_property(TEST_PROP, str) @test_prop.setter - def test_prop(self, v: Optional[str]) -> None: + def test_prop(self, v: str | None) -> None: self._set_property(TEST_PROP, v) @classmethod diff --git a/tests/extensions/test_scientific.py b/tests/extensions/test_scientific.py index c19a67d4a..0dee2edf5 100644 --- a/tests/extensions/test_scientific.py +++ b/tests/extensions/test_scientific.py @@ -2,7 +2,6 @@ import unittest from datetime import datetime, timedelta -from typing import Optional import pytest @@ -245,7 +244,7 @@ def make_collection() -> pystac.Collection: end = start + timedelta(5, 4, 3, 2, 1) bboxes = [[-180.0, -90.0, 180.0, 90.0]] spatial_extent = pystac.SpatialExtent(bboxes) - intervals: list[list[Optional[datetime]]] = [[start, end]] + intervals: list[list[datetime | None]] = [[start, end]] temporal_extent = pystac.TemporalExtent(intervals) extent = pystac.Extent(spatial_extent, temporal_extent) collection = pystac.Collection(asset_id, "desc", extent) diff --git a/tests/extensions/test_version.py b/tests/extensions/test_version.py index 2e51f26b1..e0e2fc794 100644 --- a/tests/extensions/test_version.py +++ b/tests/extensions/test_version.py @@ -2,7 +2,6 @@ from collections.abc import Generator from datetime import datetime -from typing import Optional import pytest @@ -73,7 +72,7 @@ def make_collection(year: int) -> Collection: end = datetime(year, 1, 3, 4, 5) bboxes = [[-180.0, -90.0, 180.0, 90.0]] spatial_extent = SpatialExtent(bboxes) - intervals: list[list[Optional[datetime]]] = [[start, end]] + intervals: list[list[datetime | None]] = [[start, end]] temporal_extent = TemporalExtent(intervals) extent = Extent(spatial_extent, temporal_extent) diff --git a/tests/posix_paths/test_posix_paths.py b/tests/posix_paths/test_posix_paths.py index 4bc728476..bf0218296 100644 --- a/tests/posix_paths/test_posix_paths.py +++ b/tests/posix_paths/test_posix_paths.py @@ -2,7 +2,6 @@ import os from datetime import datetime from pathlib import Path -from typing import Optional import pytest @@ -11,7 +10,7 @@ from tests.utils import ARBITRARY_BBOX, ARBITRARY_EXTENT, ARBITRARY_GEOM -def check_link(link: Optional[pystac.Link]) -> None: +def check_link(link: pystac.Link | None) -> None: assert link is not None href = link.get_target_str() assert href is not None diff --git a/tests/test_layout.py b/tests/test_layout.py index 603565711..ff27f76b2 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -1,7 +1,7 @@ import posixpath import unittest +from collections.abc import Callable from datetime import datetime, timedelta -from typing import Callable import pytest diff --git a/tests/test_utils.py b/tests/test_utils.py index bda3d3ece..44dd6998a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,6 @@ import time import unittest from datetime import datetime, timedelta, timezone -from typing import Optional import pytest from dateutil import tz @@ -291,7 +290,7 @@ def test_datetime_to_str_with_microseconds_timespec(self) -> None: self.assertEqual(expected, got) def test_str_to_datetime(self) -> None: - def _set_tzinfo(tz_str: Optional[str]) -> None: + def _set_tzinfo(tz_str: str | None) -> None: if tz_str is None: if "TZ" in os.environ: del os.environ["TZ"]