diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 9f292285..ad1f0f8d 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -22,13 +22,13 @@ jobs: - ubuntu-latest - windows-latest python-version: - - "3.8" # Earliest supported version - - "3.9" + - "3.9" # Earliest supported version - "3.10" - "3.11" - - "3.12" # Latest supported version + - "3.12" + - "3.13" # Latest supported version # commented: only enable once next Python version enters RC - # - "3.13.0-rc.1" # Development version + # - "3.14.0-rc.1" # Development version fail-fast: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6589561a..8fe0ae50 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.12.0 hooks: - id: mypy additional_dependencies: @@ -15,7 +15,7 @@ repos: - types-requests args: [] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.0 + rev: v0.6.9 hooks: - id: ruff - id: ruff-format diff --git a/doc/install.rst b/doc/install.rst index 667d00b6..e577cc86 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -4,7 +4,7 @@ Installation Dependencies ============ -:mod:`sdmx` is a pure `Python `_ package requiring Python 3.8 or higher, which can be installed: +:mod:`sdmx` is a pure `Python `_ package requiring Python 3.9 or higher, which can be installed: - from `the Python website `_, or - using a scientific Python distribution that includes other packages useful for data analysis, such as diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index a0445100..144a5ee0 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -3,8 +3,12 @@ What's new? *********** -.. Next release -.. ============ +Next release +============ + +- Python 3.13 (`released 2024-10-07 `_) is fully supported (:pull:`195`). +- Python 3.8 support is dropped, as `it has reached end-of-life `__ (:pull:`195`). + :mod:`sdmx` requires Python 3.9 or later. v2.17.0 (2024-09-03) ==================== diff --git a/pyproject.toml b/pyproject.toml index 43d29ca8..5a41d0ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,15 +19,15 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "lxml >= 3.6", "pandas >= 1.0", @@ -41,7 +41,6 @@ cache = ["requests-cache"] docs = ["IPython", "sphinx >=4", "sphinx-book-theme"] tests = [ "Jinja2", - "pyarrow", # Suppress a warning from pandas >=2.2, <3.0 "pytest >= 5", "pytest-cov", "pytest-xdist", diff --git a/sdmx/client.py b/sdmx/client.py index 06ccdf14..6d037e34 100644 --- a/sdmx/client.py +++ b/sdmx/client.py @@ -1,6 +1,6 @@ import logging from functools import partial -from typing import IO, TYPE_CHECKING, Any, Dict, Optional, Union +from typing import IO, TYPE_CHECKING, Any, Optional, Union from warnings import warn import requests @@ -56,7 +56,7 @@ class Client: """ - cache: Dict[str, "sdmx.message.Message"] = {} + cache: dict[str, "sdmx.message.Message"] = {} #: :class:`.source.Source` for requests sent from the instance. source: "sdmx.source.Source" @@ -65,7 +65,7 @@ class Client: session: requests.Session # Stored keyword arguments "allow_redirects" and "timeout" for pre-requests. - _send_kwargs: Dict[str, Any] = {} + _send_kwargs: dict[str, Any] = {} def __init__(self, source=None, log_level=None, **session_opts): try: diff --git a/sdmx/dictlike.py b/sdmx/dictlike.py index 517008ba..9792e65d 100644 --- a/sdmx/dictlike.py +++ b/sdmx/dictlike.py @@ -1,7 +1,7 @@ import logging import typing from dataclasses import fields -from typing import Generic, Tuple, TypeVar, Union, get_args, get_origin +from typing import Generic, TypeVar, Union, get_args, get_origin log = logging.getLogger(__name__) @@ -67,7 +67,7 @@ def update(self, other): def __hash__(cls): pass - def _validate_entry(self, kv: Tuple): + def _validate_entry(self, kv: tuple): """Validate one `key`/`value` pair.""" key, value = kv try: @@ -147,7 +147,7 @@ def _get_field_types(self, obj): self._field = next(filter(lambda f: f.name == self._name[1:], fields(obj))) # The type is DictLike[KeyType, ValueType]; retrieve those arguments kt, vt = get_args(self._field.type) - # Store. If ValueType is a generic, e.g. List[int], store only List. + # Store. If ValueType is a generic, e.g. list[int], store only List. self._types = (kt, get_origin(vt) or vt) def __get__(self, obj, type) -> DictLike[KT, VT]: diff --git a/sdmx/experimental.py b/sdmx/experimental.py index 2b1223ee..7a49c030 100644 --- a/sdmx/experimental.py +++ b/sdmx/experimental.py @@ -71,7 +71,7 @@ def add_obs(self, observations, series_key=None): @property def obs(self): - # In model.DataSet, .obs is typed as List[Observation]. + # In model.DataSet, .obs is typed as list[Observation]. # Here, the Observations are generated on request. for key, data in self._data.iterrows(): yield self._make_obs(key, data) diff --git a/sdmx/format/__init__.py b/sdmx/format/__init__.py index 1262a70b..cbe5ccb5 100644 --- a/sdmx/format/__init__.py +++ b/sdmx/format/__init__.py @@ -2,7 +2,7 @@ from dataclasses import InitVar, dataclass, field from enum import Enum, IntFlag from functools import lru_cache -from typing import List, Literal, Optional, Union +from typing import Literal, Optional, Union from sdmx.util import parse_content_type @@ -141,7 +141,7 @@ def is_time_series(self) -> bool: ] -def list_media_types(**filters) -> List[MediaType]: +def list_media_types(**filters) -> list[MediaType]: """Return the string for each item in :data:`MEDIA_TYPES` matching `filters`.""" result = [] for mt in MEDIA_TYPES: diff --git a/sdmx/format/xml/common.py b/sdmx/format/xml/common.py index 5f1d32cc..b92b12f2 100644 --- a/sdmx/format/xml/common.py +++ b/sdmx/format/xml/common.py @@ -6,7 +6,7 @@ from operator import itemgetter from pathlib import Path from shutil import copytree -from typing import IO, Iterable, List, Mapping, Optional, Tuple, Union +from typing import IO, Iterable, Mapping, Optional, Union from lxml import etree from lxml.etree import QName @@ -221,7 +221,7 @@ def _extracted_zipball(version: Version) -> Path: def _handle_validate_args( schema_dir: Optional[Path], version: Union[str, Version] -) -> Tuple[Path, Version]: +) -> tuple[Path, Version]: """Handle arguments for :func:`.install_schemas` and :func:`.validate_xml`.""" import platformdirs @@ -274,9 +274,9 @@ def install_schemas( class XMLFormat: NS: Mapping[str, Optional[str]] - _class_tag: List + _class_tag: list - def __init__(self, model, base_ns: str, class_tag: Iterable[Tuple[str, str]]): + def __init__(self, model, base_ns: str, class_tag: Iterable[tuple[str, str]]): from sdmx import message # noqa: F401 self.base_ns = base_ns diff --git a/sdmx/message.py b/sdmx/message.py index f0a66a30..e9007ea0 100644 --- a/sdmx/message.py +++ b/sdmx/message.py @@ -16,11 +16,8 @@ from typing import ( TYPE_CHECKING, Generator, - List, Optional, Text, - Tuple, - Type, Union, get_args, ) @@ -42,7 +39,7 @@ log = logging.getLogger(__name__) -def _summarize(obj, include: Optional[List[str]] = None): +def _summarize(obj, include: Optional[list[str]] = None): """Helper method for __repr__ on Header and Message (sub)classes.""" import requests @@ -123,7 +120,7 @@ class Footer: #: severity: Optional[str] = None #: The body text of the Footer contains zero or more blocks of text. - text: List[model.InternationalString] = field(default_factory=list) + text: list[model.InternationalString] = field(default_factory=list) #: code: Optional[int] = None @@ -315,7 +312,7 @@ def get( # separator characters urn_expr = re.compile(rf"[=:]{re.escape(id_)}") - candidates: List[model.IdentifiableArtefact] = [] + candidates: list[model.IdentifiableArtefact] = [] for key, obj in chain(*[c.items() for c in self._collections]): if id_ in (key, obj.id) or urn_expr.search(obj.urn or ""): candidates.append(obj) @@ -325,7 +322,7 @@ def get( return candidates[0] if len(candidates) == 1 else None - def iter_collections(self) -> Generator[Tuple[str, type], None, None]: + def iter_collections(self) -> Generator[tuple[str, type], None, None]: """Iterate over collections.""" for f in direct_fields(self.__class__): yield f.name, get_args(f.type)[1] @@ -380,7 +377,7 @@ class DataMessage(Message): """ #: :class:`list` of :class:`.DataSet`. - data: List[model.BaseDataSet] = field(default_factory=list) + data: list[model.BaseDataSet] = field(default_factory=list) #: :class:`.DataflowDefinition` that contains the data. dataflow: Optional[model.BaseDataflow] = None #: The "dimension at observation level". @@ -388,7 +385,7 @@ class DataMessage(Message): Union[ model._AllDimensions, model.DimensionComponent, - List[model.DimensionComponent], + list[model.DimensionComponent], ] ] = None @@ -407,7 +404,7 @@ def structure(self): return self.dataflow.structure @property - def structure_type(self) -> Type[common.Structure]: + def structure_type(self) -> type[common.Structure]: """:class:`.Structure` subtype describing the contained (meta)data.""" return { Version["2.1"]: v21.DataStructureDefinition, @@ -522,7 +519,7 @@ class MetadataMessage(DataMessage): """SDMX Metadata Message.""" @property - def structure_type(self) -> Type[common.Structure]: + def structure_type(self) -> type[common.Structure]: return { Version["2.1"]: v21.MetadataStructureDefinition, Version["3.0.0"]: v30.MetadataStructureDefinition, diff --git a/sdmx/model/__init__.py b/sdmx/model/__init__.py index dd6f7bcf..21688de7 100644 --- a/sdmx/model/__init__.py +++ b/sdmx/model/__init__.py @@ -2,7 +2,7 @@ from . import common, v21 -WARNED = set() +WARNED: set[str] = set() def __dir__(): diff --git a/sdmx/model/common.py b/sdmx/model/common.py index 16efb70c..04f418df 100644 --- a/sdmx/model/common.py +++ b/sdmx/model/common.py @@ -16,18 +16,13 @@ from typing import ( Any, ClassVar, - Dict, Generator, Generic, Iterable, - List, Mapping, MutableMapping, Optional, Sequence, - Set, - Tuple, - Type, TypeVar, Union, get_args, @@ -179,7 +174,7 @@ class AnnotableArtefact: #: #: :mod:`.sdmx` implementation detail: The IM does not specify the name of this #: feature. - annotations: List[Annotation] = field(default_factory=list) + annotations: list[Annotation] = field(default_factory=list) def get_annotation(self, **attrib): """Return a :class:`Annotation` with given `attrib`, e.g. 'id'. @@ -243,7 +238,7 @@ class IdentifiableArtefact(AnnotableArtefact): #: a URN. urn: Optional[str] = None - urn_group: Dict = field(default_factory=dict, repr=False) + urn_group: dict = field(default_factory=dict, repr=False) def __post_init__(self): if not isinstance(self.id, str): @@ -525,7 +520,7 @@ def __contains__(self, name): ... @NameableArtefact._preserve("eq", "hash", "repr") class Item(NameableArtefact, Generic[IT]): parent: Optional[Union[IT, "ItemScheme"]] = None - child: List[IT] = field(default_factory=list) + child: list[IT] = field(default_factory=list) def __post_init__(self): super().__post_init__() @@ -624,11 +619,11 @@ class ItemScheme(MaintainableArtefact, Generic[IT]): #: Members of the ItemScheme. Both ItemScheme and Item are abstract classes. #: Concrete classes are paired: for example, a :class:`.Codelist` contains #: :class:`Codes <.Code>`. - items: Dict[str, IT] = field(default_factory=dict) + items: dict[str, IT] = field(default_factory=dict) # The type of the Items in the ItemScheme. This is necessary because the type hint # in the class declaration is static; not meant to be available at runtime. - _Item: ClassVar[Type] = Item + _Item: ClassVar[type] = Item # Convenience access to items def __getattr__(self, name: str) -> IT: @@ -814,7 +809,7 @@ class Representation: #: enumerated: Optional[ItemScheme] = None #: - non_enumerated: List[Facet] = field(default_factory=list) + non_enumerated: list[Facet] = field(default_factory=list) def __repr__(self): return "<{}: {}, {}>".format( @@ -890,13 +885,13 @@ def __contains__(self, value): @dataclass class ComponentList(IdentifiableArtefact, Generic[CT]): #: - components: List[CT] = field(default_factory=list) + components: list[CT] = field(default_factory=list) #: Counter used to automatically populate :attr:`.DimensionComponent.order` values. auto_order = 1 # The default type of the Components in the ComponentList. See comment on # ItemScheme._Item - _Component: ClassVar[Type] = Component + _Component: ClassVar[type] = Component # Convenience access to the components def append(self, value: CT) -> None: @@ -1054,20 +1049,20 @@ class Contact: #: responsibility: InternationalStringDescriptor = InternationalStringDescriptor() #: - email: List[str] = field(default_factory=list) + email: list[str] = field(default_factory=list) #: - fax: List[str] = field(default_factory=list) + fax: list[str] = field(default_factory=list) #: - uri: List[str] = field(default_factory=list) + uri: list[str] = field(default_factory=list) #: - x400: List[str] = field(default_factory=list) + x400: list[str] = field(default_factory=list) @dataclass @NameableArtefact._preserve("eq", "hash", "repr") class Organisation(Item["Organisation"]): #: - contact: List[Contact] = field(default_factory=list) + contact: list[Contact] = field(default_factory=list) class OrganisationScheme(ItemScheme[IT]): @@ -1112,7 +1107,7 @@ class Structure(MaintainableArtefact): @property def grouping(self) -> Sequence[ComponentList]: """A collection of all the ComponentLists associated with a subclass.""" - result: List[ComponentList] = [] + result: list[ComponentList] = [] for f in fields(self): types = get_args(f.type) or (f.type,) try: @@ -1264,7 +1259,7 @@ class AttributeRelationship: @dataclass class DimensionRelationship(AttributeRelationship): #: - dimensions: List[DimensionComponent] = field(default_factory=list) + dimensions: list[DimensionComponent] = field(default_factory=list) #: NB the IM says "0..*" here in a diagram, but the text does not match. group_key: Optional["GroupDimensionDescriptor"] = None @@ -1309,13 +1304,13 @@ class BaseDataStructureDefinition(Structure, ConstrainableArtefact): ) # Specific types to be used in concrete subclasses - MemberValue: ClassVar[Type["BaseMemberValue"]] - MemberSelection: ClassVar[Type["BaseMemberSelection"]] - ConstraintType: ClassVar[Type[BaseConstraint]] + MemberValue: ClassVar[type["BaseMemberValue"]] + MemberSelection: ClassVar[type["BaseMemberSelection"]] + ConstraintType: ClassVar[type[BaseConstraint]] # Convenience methods def iter_keys( - self, constraint: Optional[BaseConstraint] = None, dims: List[str] = [] + self, constraint: Optional[BaseConstraint] = None, dims: list[str] = [] ) -> Generator["Key", None, None]: """Iterate over keys. @@ -1341,7 +1336,7 @@ def make_factory(id=None, value_for=None): return lambda value: KeyValue(id=id, value=value, value_for=value_for) # List of iterables of (dim.id, KeyValues) along each dimension - all_kvs: List[Iterable[KeyValue]] = [] + all_kvs: list[Iterable[KeyValue]] = [] # Iterate over dimensions for dim in self.dimensions.components: @@ -1491,7 +1486,7 @@ def make_key(self, key_cls, values: Mapping, extend=False, group_id=None): attr = getattr(self.attributes, get_method) # Arguments for creating the Key - args: Dict[str, Any] = dict(described_by=self.dimensions) + args: dict[str, Any] = dict(described_by=self.dimensions) if key_cls is GroupKey: # Get the GroupDimensionDescriptor, if indicated by group_id @@ -1564,7 +1559,7 @@ def __post_init__(self): self.structure.is_external_reference = self.is_external_reference def iter_keys( - self, constraint: Optional[BaseConstraint] = None, dims: List[str] = [] + self, constraint: Optional[BaseConstraint] = None, dims: list[str] = [] ) -> Generator["Key", None, None]: """Iterate over keys. @@ -1762,7 +1757,7 @@ def __init__(self, arg: Union[Mapping, Sequence[KeyValue], None] = None, **kwarg ) kwargs.update(arg) - kvs: Iterable[Tuple] = [] + kvs: Iterable[tuple] = [] if isinstance(arg, Sequence): # Sequence of already-prepared KeyValues; assume already sorted @@ -1909,7 +1904,7 @@ def __init__(self, arg: Optional[Mapping] = None, **kwargs): @dataclass class SeriesKey(Key): #: :mod:`sdmx` extension not in the IM. - group_keys: Set[GroupKey] = field(default_factory=set) + group_keys: set[GroupKey] = field(default_factory=set) __eq__ = Key.__eq__ __hash__ = Key.__hash__ @@ -1942,7 +1937,7 @@ class BaseObservation: #: Data value. value: Optional[Union[Any, Code]] = None #: :mod:`sdmx` extension not in the IM. - group_keys: Set[GroupKey] = field(default_factory=set) + group_keys: set[GroupKey] = field(default_factory=set) @property def attrib(self): @@ -2011,14 +2006,14 @@ class BaseDataSet(AnnotableArtefact): structured_by: Optional[BaseDataStructureDefinition] = None #: All observations in the DataSet. - obs: List[BaseObservation] = field(default_factory=list) + obs: list[BaseObservation] = field(default_factory=list) #: Map of series key → list of observations. #: :mod:`sdmx` extension not in the IM. - series: DictLikeDescriptor[SeriesKey, List[BaseObservation]] = DictLikeDescriptor() + series: DictLikeDescriptor[SeriesKey, list[BaseObservation]] = DictLikeDescriptor() #: Map of group key → list of observations. #: :mod:`sdmx` extension not in the IM. - group: DictLikeDescriptor[GroupKey, List[BaseObservation]] = DictLikeDescriptor() + group: DictLikeDescriptor[GroupKey, list[BaseObservation]] = DictLikeDescriptor() def __post_init__(self): if self.action and not isinstance(self.action, ActionType): @@ -2117,7 +2112,7 @@ class MetadataAttribute(AttributeComponent): min_occurs: Optional[int] = None parent: Optional["MetadataAttribute"] = None - child: List["MetadataAttribute"] = field(default_factory=list) + child: list["MetadataAttribute"] = field(default_factory=list) class BaseMetadataStructureDefinition(Structure, ConstrainableArtefact): @@ -2202,7 +2197,7 @@ class HierarchicalCode(IdentifiableArtefact): parent: Optional[Union["HierarchicalCode", Any]] = ( None # NB second element is "Hierarchy" ) - child: List["HierarchicalCode"] = field(default_factory=list) + child: list["HierarchicalCode"] = field(default_factory=list) # SDMX 2.1 §10.2: Constraint inheritance @@ -2276,7 +2271,7 @@ class BaseDataKey: # :obj:`False` if they are excluded. included: bool #: Mapping from :class:`.Component` to :class:`.ComponentValue` comprising the key. - key_value: Dict[Component, ComponentValue] = field(default_factory=dict) + key_value: dict[Component, ComponentValue] = field(default_factory=dict) @dataclass @@ -2287,7 +2282,7 @@ class BaseDataKeySet: #: :class:`Constraint <.BaseConstraint>`; :obj:`False` if they are excluded. included: bool #: :class:`DataKeys <.BaseDataKey>` appearing in the set. - keys: List[BaseDataKey] = field(default_factory=list) + keys: list[BaseDataKey] = field(default_factory=list) def __len__(self): """:func:`len` of the DataKeySet = :func:`len` of its :attr:`keys`.""" @@ -2307,7 +2302,7 @@ class BaseMemberSelection: included: bool = True #: Value(s) included in the selection. Note that the name of this attribute is not #: stated in the IM, so 'values' is chosen for the implementation in this package. - values: List[BaseSelectionValue] = field(default_factory=list) + values: list[BaseSelectionValue] = field(default_factory=list) def __contains__(self, value): """Compare KeyValue to MemberValue.""" @@ -2329,7 +2324,7 @@ class CubeRegion: #: included: bool = True #: - member: Dict[DimensionComponent, BaseMemberSelection] = field(default_factory=dict) + member: dict[DimensionComponent, BaseMemberSelection] = field(default_factory=dict) def __contains__(self, other: Union["Key", "KeyValue"]) -> bool: """Membership test. @@ -2561,7 +2556,7 @@ class BaseContentConstraint: #: The SDMX-IM groups classes into 'packages'; these are used in :class:`URNs <.URN>`. PACKAGE = dict() -_PACKAGE_CLASS: Dict[str, set] = { +_PACKAGE_CLASS: dict[str, set] = { "base": { "Agency", "AgencyScheme", @@ -2627,15 +2622,15 @@ class BaseContentConstraint: @dataclass class ClassFinder: module_name: str - name_map: Dict[str, str] = field(default_factory=dict) - parent_map: Dict[type, type] = field(default_factory=dict) + name_map: dict[str, str] = field(default_factory=dict) + parent_map: dict[type, type] = field(default_factory=dict) def __post_init__(self): self._module = sys.modules[self.module_name] self._parent = ChainMap(PARENT, self.parent_map) @lru_cache() - def get_class(self, name: Union[str, Resource], package=None) -> Optional[Type]: + def get_class(self, name: Union[str, Resource], package=None) -> Optional[type]: """Return a class for `name` and (optional) `package` names.""" if isinstance(name, Resource): # Convert a Resource enumeration value to a string diff --git a/sdmx/model/internationalstring.py b/sdmx/model/internationalstring.py index 0441ef9a..7597672b 100644 --- a/sdmx/model/internationalstring.py +++ b/sdmx/model/internationalstring.py @@ -1,5 +1,5 @@ from copy import copy -from typing import Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union +from typing import Iterable, Mapping, Optional, Sequence, Union # TODO read this from the environment, or use any value set in the SDMX-ML spec. # Currently set to 'en' because test_dsd.py expects it. @@ -50,9 +50,9 @@ class Foo: __slots__ = ("localizations",) # Types that can be converted into InternationalString - _CONVERTIBLE = Union[str, Sequence, Mapping, Iterable[Tuple[str, str]]] + _CONVERTIBLE = Union[str, Sequence, Mapping, Iterable[tuple[str, str]]] - localizations: Dict[str, str] + localizations: dict[str, str] def __init__(self, value: Optional[_CONVERTIBLE] = None, **kwargs): # Handle initial values according to type diff --git a/sdmx/model/v21.py b/sdmx/model/v21.py index 26158457..155bee14 100644 --- a/sdmx/model/v21.py +++ b/sdmx/model/v21.py @@ -7,13 +7,9 @@ from dataclasses import dataclass, field from typing import ( ClassVar, - Dict, Generator, Generic, - List, Optional, - Set, - Type, TypeVar, Union, ) @@ -171,9 +167,9 @@ class MemberSelection(common.BaseMemberSelection): @NameableArtefact._preserve("repr") class ContentConstraint(Constraint, common.BaseContentConstraint): #: :class:`CubeRegions <.CubeRegion>` included in the ContentConstraint. - data_content_region: List[common.CubeRegion] = field(default_factory=list) + data_content_region: list[common.CubeRegion] = field(default_factory=list) #: - content: Set[ConstrainableArtefact] = field(default_factory=set) + content: set[ConstrainableArtefact] = field(default_factory=set) metadata_content_region: Optional[common.MetadataTargetRegion] = None def __contains__(self, value): @@ -195,7 +191,7 @@ def to_query_string(self, structure): def iter_keys( self, obj: Union["DataStructureDefinition", "DataflowDefinition"], - dims: List[str] = [], + dims: list[str] = [], ) -> Generator[Key, None, None]: """Iterate over keys. @@ -350,7 +346,7 @@ class IdentifiableObjectTarget(TargetObject): """SDMX 2.1 IdentifiableObjectTarget.""" #: Type of :class:`.IdentifiableArtefact` that is targeted. - object_type: Optional[Type[IdentifiableArtefact]] = None + object_type: Optional[type[IdentifiableArtefact]] = None def compare(self, other, strict=True): """Return :obj:`True` if `self` is the same as `other`. @@ -385,7 +381,7 @@ class ReportStructure(ComponentList): _Component = common.MetadataAttribute - report_for: List[MetadataTarget] = field(default_factory=list) + report_for: list[MetadataTarget] = field(default_factory=list) @dataclass @@ -433,10 +429,18 @@ class TargetIdentifiableObject(TargetObjectValue): @dataclass class TargetObjectKey: - """SDMX 2.1 TargetObjectKey.""" + """SDMX 2.1 TargetObjectKey. + TargetObjectKey supports item access (:py:`tok["name"]`) to members of + :attr:`.key_values`. + """ + + #: Keys and values of the TargetObjectKey. key_values: DictLikeDescriptor[str, TargetObjectValue] = DictLikeDescriptor() + def __getitem__(self, name: str) -> TargetObjectValue: + return self.key_values[name] + @dataclass class ReportedAttribute: @@ -447,7 +451,7 @@ class ReportedAttribute: value_for: common.MetadataAttribute parent: Optional["ReportedAttribute"] = None - child: List["ReportedAttribute"] = field(default_factory=list) + child: list["ReportedAttribute"] = field(default_factory=list) def __getitem__(self, index: int) -> "ReportedAttribute": return self.child[index] @@ -498,7 +502,7 @@ class XHTMLAttributeValue(NonEnumeratedAttributeValue, common.BaseXHTMLAttribute class MetadataReport: """SDMX 2.1 MetadataReport.""" - metadata: List[ReportedAttribute] = field(default_factory=list) + metadata: list[ReportedAttribute] = field(default_factory=list) target: Optional[MetadataTarget] = None attaches_to: Optional[TargetObjectKey] = None @@ -521,7 +525,7 @@ class MetadataSet(NameableArtefact, common.BaseMetadataSet): #: Analogous to :attr:`.v30.MetadataSet.provided_by`. published_by: Optional[common.DataProvider] = None - report: List[MetadataReport] = field(default_factory=list) + report: list[MetadataReport] = field(default_factory=list) # §8 Hierarchical Code List @@ -534,7 +538,7 @@ class Hierarchy(NameableArtefact): has_formal_levels: bool = False #: Hierarchical codes in the hierarchy. - codes: Dict[str, common.HierarchicalCode] = field(default_factory=dict) + codes: dict[str, common.HierarchicalCode] = field(default_factory=dict) level: Optional[common.Level] = None @@ -543,7 +547,7 @@ class Hierarchy(NameableArtefact): class HierarchicalCodelist(common.MaintainableArtefact): """SDMX 2.1 HierarchicalCodelist.""" - hierarchy: List[Hierarchy] = field(default_factory=list) + hierarchy: list[Hierarchy] = field(default_factory=list) def __repr__(self) -> str: tmp = super(NameableArtefact, self).__repr__()[:-1] @@ -557,7 +561,7 @@ def __repr__(self) -> str: class ItemAssociation(common.AnnotableArtefact, Generic[IT]): """SDMX 2.1 ItemAssociation.""" - _Item: ClassVar[Type[common.Item]] = common.Item + _Item: ClassVar[type[common.Item]] = common.Item source: Optional[IT] = None target: Optional[IT] = None @@ -577,12 +581,12 @@ class CodeMap(ItemAssociation[common.Code]): class ItemSchemeMap(NameableArtefact, Generic[IST, IAT]): """SDMX 2.1 ItemSchemeMap.""" - _ItemAssociation: ClassVar[Type[ItemAssociation]] = ItemAssociation + _ItemAssociation: ClassVar[type[ItemAssociation]] = ItemAssociation source: Optional[IST] = None target: Optional[IST] = None - item_association: List[IAT] = field(default_factory=list) + item_association: list[IAT] = field(default_factory=list) class CodelistMap(ItemSchemeMap[common.Codelist, CodeMap]): @@ -595,7 +599,7 @@ class CodelistMap(ItemSchemeMap[common.Codelist, CodeMap]): class StructureSet(common.MaintainableArtefact): """SDMX 2.1 StructureSet.""" - item_scheme_map: List[ItemSchemeMap] = field(default_factory=list) + item_scheme_map: list[ItemSchemeMap] = field(default_factory=list) CF = common.ClassFinder( diff --git a/sdmx/model/v30.py b/sdmx/model/v30.py index 707e39fb..8a8916f9 100644 --- a/sdmx/model/v30.py +++ b/sdmx/model/v30.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from datetime import date from enum import Enum -from typing import Any, ClassVar, Dict, List, Optional, Set +from typing import Any, ClassVar, Optional from . import common from .common import ( @@ -77,7 +77,7 @@ @dataclass class CodeSelection: - mv: List["MemberValue"] = field(default_factory=list) + mv: list["MemberValue"] = field(default_factory=list) class ExclusiveCodeSelection(CodeSelection): @@ -190,7 +190,7 @@ class ValueList(EnumeratedList): _Item = ValueItem - items: List[ValueItem] = field(default_factory=list) + items: list[ValueItem] = field(default_factory=list) def append(self, item: ValueItem) -> None: self.items.append(item) @@ -284,7 +284,7 @@ class MemberSelection(common.BaseMemberSelection): @NameableArtefact._preserve("repr") class DataConstraint(Constraint): #: - content: Set[ConstrainableArtefact] = field(default_factory=set) + content: set[ConstrainableArtefact] = field(default_factory=set) data_content_keys: Optional[DataKeySet] = None data_content_region: Optional[common.CubeRegion] = None @@ -432,7 +432,7 @@ class MetadataAttributeValue: # offends mypy. parent: Optional["MetadataAttributeValue"] = None - child: List["MetadataAttributeValue"] = field(default_factory=list) + child: list["MetadataAttributeValue"] = field(default_factory=list) class CodedMetadataAttributeValue(MetadataAttributeValue): @@ -505,9 +505,9 @@ class MetadataSet(MaintainableArtefact, common.BaseMetadataSet): #: Analogous to :attr:`.v21.MetadataSet.published_by`. provided_by: Optional[MetadataProvider] = None - attaches_to: List[TargetIdentifiableObject] = field(default_factory=list) + attaches_to: list[TargetIdentifiableObject] = field(default_factory=list) - metadata: List[MetadataAttributeValue] = field(default_factory=list) + metadata: list[MetadataAttributeValue] = field(default_factory=list) # §8: Hierarchy @@ -523,7 +523,7 @@ class Hierarchy(MaintainableArtefact): level: Optional[common.Level] = None #: The top-level :class:`HierarchicalCodes ` in the hierarchy. - codes: Dict[str, common.HierarchicalCode] = field(default_factory=dict) + codes: dict[str, common.HierarchicalCode] = field(default_factory=dict) @dataclass diff --git a/sdmx/reader/base.py b/sdmx/reader/base.py index 7465b41e..97c60ef5 100644 --- a/sdmx/reader/base.py +++ b/sdmx/reader/base.py @@ -1,7 +1,7 @@ import logging from abc import ABC, abstractmethod from functools import lru_cache -from typing import TYPE_CHECKING, ClassVar, List, Optional +from typing import TYPE_CHECKING, ClassVar, Optional from warnings import warn from sdmx.format import MediaType @@ -14,10 +14,10 @@ class BaseReader(ABC): #: List of media types handled by the reader. - media_types: ClassVar[List[MediaType]] = [] + media_types: ClassVar[list[MediaType]] = [] #: List of file name suffixes handled by the reader. - suffixes: ClassVar[List[str]] = [] + suffixes: ClassVar[list[str]] = [] @classmethod def detect(cls, content: bytes) -> bool: diff --git a/sdmx/reader/xml/common.py b/sdmx/reader/xml/common.py index d7d0603a..53195f5e 100644 --- a/sdmx/reader/xml/common.py +++ b/sdmx/reader/xml/common.py @@ -8,14 +8,11 @@ Any, Callable, ClassVar, - Dict, Iterable, Iterator, Mapping, Optional, Sequence, - Tuple, - Type, Union, cast, ) @@ -105,7 +102,7 @@ def __init__(self, reader, elem, cls_hint=None): @classmethod @abstractmethod - def info_from_element(cls, elem) -> Dict[str, Any]: ... + def info_from_element(cls, elem) -> dict[str, Any]: ... def __str__(self): # NB for debugging only @@ -130,16 +127,16 @@ class XMLEventReader(BaseReader): model: ClassVar["types.ModuleType"] #: :class:`.BaseReference` subclass used by this reader. - Reference: ClassVar[Type[BaseReference]] + Reference: ClassVar[type[BaseReference]] # Mapping from (QName, ["start", "end"]) to a function that parses the element/event # or else None - parser: ClassVar[Mapping[Tuple[QName, str], Callable]] + parser: ClassVar[Mapping[tuple[QName, str], Callable]] # One-way counter for use in stacks _count: Iterator[int] - def __init_subclass__(cls: Type["XMLEventReader"]): + def __init_subclass__(cls: type["XMLEventReader"]): # Empty dictionary cls.parser = {} @@ -162,7 +159,7 @@ def read_message( # noqa: C901 TODO reduce complexity 12 → ≤11 **kwargs, ) -> message.Message: # Initialize stacks - self.stack: Dict[Union[Type, str], Dict[Union[str, int], Any]] = defaultdict( + self.stack: dict[Union[type, str], dict[Union[str, int], Any]] = defaultdict( dict ) @@ -177,7 +174,7 @@ def read_message( # noqa: C901 TODO reduce complexity 12 → ≤11 if _events is None: events = cast( - Iterator[Tuple[str, etree._Element]], + Iterator[tuple[str, etree._Element]], etree.iterparse(source, events=("start", "end")), ) else: @@ -385,7 +382,7 @@ def qname(cls, ns_or_name, name=None) -> QName: def get_single( self, - cls_or_name: Union[Type, str], + cls_or_name: Union[type, str], id: Optional[str] = None, version: Optional[str] = None, subclass: bool = False, @@ -402,7 +399,7 @@ def get_single( the stack `cls_or_name` *or any stack for a subclass of this class*. """ if subclass: - keys: Iterable[Union[Type, str]] = filter( + keys: Iterable[Union[type, str]] = filter( matching_class(cls_or_name), self.stack.keys() ) results: Mapping = ChainMap(*[self.stack[k] for k in keys]) @@ -422,14 +419,14 @@ def get_single( else: return next(iter(results.values())) - def pop_all(self, cls_or_name: Union[Type, str], subclass=False) -> Sequence: + def pop_all(self, cls_or_name: Union[type, str], subclass=False) -> Sequence: """Pop all objects from stack *cls_or_name* and return. If `cls_or_name` is a class and `subclass` is :obj:`True`; return all objects in the stack `cls_or_name` *or any stack for a subclass of this class*. """ if subclass: - keys: Iterable[Union[Type, str]] = list( + keys: Iterable[Union[type, str]] = list( filter(matching_class(cls_or_name), self.stack.keys()) ) result: Iterable = chain(*[self.stack.pop(k).values() for k in keys]) @@ -438,14 +435,14 @@ def pop_all(self, cls_or_name: Union[Type, str], subclass=False) -> Sequence: return list(result) - def pop_single(self, cls_or_name: Union[Type, str]): + def pop_single(self, cls_or_name: Union[type, str]): """Pop a single object from the stack for `cls_or_name` and return.""" try: return self.stack[cls_or_name].popitem()[1] except KeyError: return None - def peek(self, cls_or_name: Union[Type, str]): + def peek(self, cls_or_name: Union[type, str]): """Get the object at the top of stack `cls_or_name` without removing it.""" try: key, value = self.stack[cls_or_name].popitem() @@ -454,7 +451,7 @@ def peek(self, cls_or_name: Union[Type, str]): except KeyError: # pragma: no cover return None - def pop_resolved_ref(self, cls_or_name: Union[Type, str]): + def pop_resolved_ref(self, cls_or_name: Union[type, str]): """Pop a reference to `cls_or_name` and resolve it.""" return self.resolve(self.pop_single(cls_or_name)) diff --git a/sdmx/reader/xml/v21.py b/sdmx/reader/xml/v21.py index f246ccdc..ee23e45d 100644 --- a/sdmx/reader/xml/v21.py +++ b/sdmx/reader/xml/v21.py @@ -11,7 +11,7 @@ from copy import copy from itertools import chain from sys import maxsize -from typing import Any, Dict, MutableMapping, Optional, Type, cast +from typing import Any, MutableMapping, Optional, cast from dateutil.parser import isoparse from lxml import etree @@ -392,7 +392,7 @@ def _ref(reader: Reader, elem): # /: use message property for a class hint msg = reader.get_single(message.DataMessage, subclass=True) if msg: - cls_hint = cast(Type[message.DataMessage], type(msg))( + cls_hint = cast(type[message.DataMessage], type(msg))( version=reader.xml_version ).structure_type elif QName(elem.getparent()).localname == "Dataflow": @@ -497,7 +497,7 @@ def _item_end(reader: Reader, elem): ) @possible_reference() # in def _itemscheme(reader: Reader, elem): - cls: Type[common.ItemScheme] = reader.class_for_tag(elem.tag) + cls: type[common.ItemScheme] = reader.class_for_tag(elem.tag) try: args = dict(is_partial=elem.attrib["isPartial"]) @@ -510,7 +510,7 @@ def _itemscheme(reader: Reader, elem): iter_all = chain(*[iter(item) for item in reader.pop_all(cls._Item, subclass=True)]) # Set of objects already added to `items` - seen: Dict[Any, Any] = dict() + seen: dict[Any, Any] = dict() # Flatten the list, with each item appearing only once for i in filter(lambda i: i not in seen, iter_all): @@ -1464,14 +1464,14 @@ def _hcl(reader: Reader, elem): @start("str:CodelistMap", only=False) def _ismap_start(reader: Reader, elem): - cls: Type[model.ItemSchemeMap] = reader.class_for_tag(elem.tag) + cls: type[model.ItemSchemeMap] = reader.class_for_tag(elem.tag) # Push class for reference while parsing sub-elements reader.push("ItemAssociation class", cls._ItemAssociation._Item) @end("str:CodelistMap", only=False) def _ismap_end(reader: Reader, elem): - cls: Type[model.ItemSchemeMap] = reader.class_for_tag(elem.tag) + cls: type[model.ItemSchemeMap] = reader.class_for_tag(elem.tag) # Remove class from stacks reader.pop_single("ItemAssociation class") @@ -1506,7 +1506,7 @@ def _ismap_end(reader: Reader, elem): @end("str:CodeMap") def _item_map(reader: Reader, elem): - cls: Type[model.ItemAssociation] = reader.class_for_tag(elem.tag) + cls: type[model.ItemAssociation] = reader.class_for_tag(elem.tag) # Store Source and Target as Reference instances return reader.annotable( @@ -1622,7 +1622,7 @@ def _udo(reader: Reader, elem): @end("str:VtlMapping") def _vtlm(reader: Reader, elem): ref = reader.resolve(reader.pop_single(reader.Reference)) - args: Dict[str, Any] = dict() + args: dict[str, Any] = dict() if isinstance(ref, common.BaseDataflow): cls = model.VTLDataflowMapping args["dataflow_alias"] = ref diff --git a/sdmx/reader/xml/v30.py b/sdmx/reader/xml/v30.py index 29a043fd..b7fb69d0 100644 --- a/sdmx/reader/xml/v30.py +++ b/sdmx/reader/xml/v30.py @@ -1,6 +1,6 @@ """SDMX-ML 3.0.0 reader.""" -from typing import Any, Dict +from typing import Any import sdmx.urn from sdmx.format import Version @@ -140,7 +140,7 @@ def _ar(reader: Reader, elem): return # Iterate over parsed references to Components - args: Dict[str, Any] = dict(dimensions=list()) + args: dict[str, Any] = dict(dimensions=list()) for ref in refs: # Use the to retrieve a Component from the DSD if issubclass(ref.target_cls, model.DimensionComponent): diff --git a/sdmx/rest/common.py b/sdmx/rest/common.py index d1190f80..d00c7c02 100644 --- a/sdmx/rest/common.py +++ b/sdmx/rest/common.py @@ -5,7 +5,7 @@ from copy import copy from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional +from typing import TYPE_CHECKING, Any, ClassVar, Mapping, Optional from urllib.parse import urlsplit, urlunsplit if TYPE_CHECKING: @@ -155,7 +155,7 @@ class Parameter(abc.ABC): default: Optional[str] = None @abc.abstractmethod - def handle(self, parameters: Dict[str, Any]) -> Dict[str, str]: + def handle(self, parameters: dict[str, Any]) -> dict[str, str]: """Return a dict to update :attr:`.URL.path` or :attr:`.URL.query`.""" @@ -246,7 +246,7 @@ def handle(self, parameters): # - common:NCNameIDType # - common:VersionType -PARAM: Dict[str, Parameter] = { +PARAM: dict[str, Parameter] = { # Path parameters "agency_id": PathParameter("agency_id"), "key": OptionalPath("key"), @@ -321,10 +321,10 @@ class URL(abc.ABC): resource_type: Resource #: Pieces for the hierarchical path component of the URL. If - _path: Dict[str, Optional[str]] + _path: dict[str, Optional[str]] #: Pieces for the query component of the URL. - query: Dict[str, str] + query: dict[str, str] # Keyword arguments to the constructor _params: dict diff --git a/sdmx/rest/v21.py b/sdmx/rest/v21.py index faa0feff..783f7878 100644 --- a/sdmx/rest/v21.py +++ b/sdmx/rest/v21.py @@ -9,14 +9,13 @@ """ from collections import ChainMap -from typing import Dict from warnings import warn from . import common from .common import OptionalPath, PathParameter, QueryParameter, Resource #: v1.5.0-specific path and query parameters. -PARAM: Dict[str, common.Parameter] = { +PARAM: dict[str, common.Parameter] = { # Path parameters # NB the text and YAML OpenAPI specification disagree on whether this is required "component_id": OptionalPath("component_id"), diff --git a/sdmx/rest/v30.py b/sdmx/rest/v30.py index edd83bac..f9f49c42 100644 --- a/sdmx/rest/v30.py +++ b/sdmx/rest/v30.py @@ -8,13 +8,12 @@ """ from collections import ChainMap -from typing import Dict from . import common from .common import PathParameter, QueryParameter, Resource #: v2.1.0-specific path and query parameters. -PARAM: Dict[str, common.Parameter] = { +PARAM: dict[str, common.Parameter] = { # Path parameters "component_id": PathParameter("component_id"), "context": PathParameter( diff --git a/sdmx/source/__init__.py b/sdmx/source/__init__.py index 8e905192..6cbbfa9f 100644 --- a/sdmx/source/__init__.py +++ b/sdmx/source/__init__.py @@ -4,7 +4,7 @@ from enum import Enum from importlib import import_module from io import IOBase -from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Optional, Union from requests import Response @@ -16,7 +16,7 @@ import sdmx.rest.common #: Data sources registered with :mod:`sdmx`. -sources: Dict[str, "Source"] = {} +sources: dict[str, "Source"] = {} #: Valid data content types for SDMX REST API messages. DataContentType = Enum("DataContentType", "CSV JSON XML") @@ -68,14 +68,14 @@ class Source: name: str #: Additional HTTP headers to supply by default with all requests. - headers: Dict[str, Any] = field(default_factory=dict) + headers: dict[str, Any] = field(default_factory=dict) #: :class:`.DataContentType` indicating the type of data returned by the source. data_content_type: DataContentType = DataContentType.XML #: SDMX REST API version(s) supported. Default: :class:`.Version["2.1"] <.Version>` #: only. - versions: Set[Version] = field(default_factory=lambda: {Version["2.1"]}) + versions: set[Version] = field(default_factory=lambda: {Version["2.1"]}) #: Mapping from :class:`.Resource` values to :class:`bool` indicating support for #: SDMX-REST endpoints and features. If not supplied, the defaults from @@ -87,7 +87,7 @@ class Source: #: See :meth:`.preview_data`. #: - ``"structure-specific data"=True`` if the source can return structure- #: specific data messages. - supports: Dict[Union[str, Resource], bool] = field(default_factory=dict) + supports: dict[Union[str, Resource], bool] = field(default_factory=dict) def __post_init__(self): # Sanity check: _id attribute of a subclass matches the loaded ID. @@ -121,7 +121,7 @@ def __post_init__(self): self.supports.pop(f_name, SDMX_ML_SUPPORTS.get(feature, sdmx_ml)), ) - def get_url_class(self) -> Type["sdmx.rest.common.URL"]: + def get_url_class(self) -> type["sdmx.rest.common.URL"]: """Return a class for constructing URLs for this Source. - If :attr:`.versions` includes *only* SDMX 3.0.0, return :class:`.v30.URL`. @@ -142,7 +142,7 @@ def get_url_class(self) -> Type["sdmx.rest.common.URL"]: # Hooks def handle_response( self, response: Response, content: IOBase - ) -> Tuple[Response, IOBase]: + ) -> tuple[Response, IOBase]: """Handle response content of unknown type. This hook is called by :meth:`.Client.get` *only* when the `content` cannot be @@ -198,7 +198,7 @@ class _NoSource(Source): def add_source( - info: Union[Dict, str], id: Optional[str] = None, override: bool = False, **kwargs + info: Union[dict, str], id: Optional[str] = None, override: bool = False, **kwargs ) -> None: """Add a new data source. diff --git a/sdmx/source/estat.py b/sdmx/source/estat.py index b6624ca3..f177fca0 100644 --- a/sdmx/source/estat.py +++ b/sdmx/source/estat.py @@ -1,7 +1,6 @@ import logging from tempfile import NamedTemporaryFile from time import sleep -from typing import Dict from urllib.parse import urlparse from zipfile import ZipFile @@ -13,7 +12,7 @@ log = logging.getLogger(__name__) -def handle_references_param(kwargs: Dict) -> None: +def handle_references_param(kwargs: dict) -> None: """Handle the "references" query parameter for ESTAT and similar. For this parameter, the server software behind ESTAT's data source only supports diff --git a/sdmx/testing/__init__.py b/sdmx/testing/__init__.py index c32e7769..6ad4b152 100644 --- a/sdmx/testing/__init__.py +++ b/sdmx/testing/__init__.py @@ -3,7 +3,7 @@ from collections import ChainMap from contextlib import contextmanager from pathlib import Path, PurePosixPath -from typing import List, Tuple, Union +from typing import Union import numpy as np import pandas as pd @@ -233,7 +233,7 @@ class SpecimenCollection: # Path to specimen; file format; data/structure # TODO add version - specimens: List[Tuple[Path, str, str]] + specimens: list[tuple[Path, str, str]] def __init__(self, base_path): self.base_path = base_path diff --git a/sdmx/testing/report.py b/sdmx/testing/report.py index 8c550acf..95a04de7 100644 --- a/sdmx/testing/report.py +++ b/sdmx/testing/report.py @@ -2,7 +2,7 @@ import os from itertools import chain from pathlib import Path -from typing import Dict, Optional +from typing import Optional from jinja2 import Template @@ -207,7 +207,7 @@ def main(base_path: Optional[Path] = None): base_path = base_path or Path.cwd().joinpath("source-tests") # Locate, read, and merge JSON files - data: Dict[str, Dict[str, str]] = {} + data: dict[str, dict[str, str]] = {} for path in base_path.glob("**/*.json"): # Update `data` with the file contents with open(path) as f: diff --git a/sdmx/tests/__init__.py b/sdmx/tests/__init__.py index 4464ea80..d1fe6729 100644 --- a/sdmx/tests/__init__.py +++ b/sdmx/tests/__init__.py @@ -1,5 +1,5 @@ import importlib -from typing import Optional, Tuple +from typing import Optional import pytest from packaging.version import Version @@ -8,7 +8,7 @@ # thanks to xarray def _importorskip( modname: str, minversion: Optional[str] = None -) -> Tuple[bool, pytest.MarkDecorator]: +) -> tuple[bool, pytest.MarkDecorator]: try: mod = importlib.import_module(modname) has = True diff --git a/sdmx/tests/model/test_v21.py b/sdmx/tests/model/test_v21.py index eb0e3317..d53bb21a 100644 --- a/sdmx/tests/model/test_v21.py +++ b/sdmx/tests/model/test_v21.py @@ -1,10 +1,10 @@ from operator import attrgetter -from typing import List import pytest import sdmx import sdmx.message +from sdmx.model import v21 from sdmx.model import v21 as model from sdmx.model.v21 import ( AttributeDescriptor, @@ -33,6 +33,7 @@ MemberSelection, MemberValue, Observation, + TargetObjectKey, value_for_dsd_ref, ) @@ -55,7 +56,7 @@ def cl(self): def components(self): return [Dimension(id="C1"), Dimension(id="C2"), Dimension(id="C3")] - def test_append(self, cl: ComponentList, components: List[Dimension]) -> None: + def test_append(self, cl: ComponentList, components: list[Dimension]) -> None: # Components have no order assert (None, None, None) == tuple(map(attrgetter("order"), components)) @@ -75,14 +76,14 @@ def test_getdefault(self, cl) -> None: assert not hasattr(foo, "order") def test_extend_no_order( - self, cl: ComponentList, components: List[Dimension] + self, cl: ComponentList, components: list[Dimension] ) -> None: cl.extend(components) # extend() also adds order assert (1, 2, 3) == tuple(map(attrgetter("order"), components)) - def test_extend_order(self, cl: ComponentList, components: List[Dimension]) -> None: + def test_extend_order(self, cl: ComponentList, components: list[Dimension]) -> None: components[2].order = 1 components[1].order = 2 components[0].order = 3 @@ -649,3 +650,15 @@ def test_hierarchy(self, msg: sdmx.message.StructureMessage) -> None: def test_repr(self, obj: model.HierarchicalCodelist): assert "" == repr(obj) + + +class TestTargetObjectKey: + def test_getitem(self) -> None: + """:meth:`TargetObjectKey.__getitem__` works.""" + to = v21.TargetObject(id="FOO") + c = Code(id="BAR") + tok = TargetObjectKey( + key_values={"FOO": v21.TargetIdentifiableObject(value_for=to, obj=c)} + ) + + assert tok["FOO"].obj is c # type: ignore [attr-defined] diff --git a/sdmx/tests/reader/test_base.py b/sdmx/tests/reader/test_base.py index 9cfd9e12..456bd41a 100644 --- a/sdmx/tests/reader/test_base.py +++ b/sdmx/tests/reader/test_base.py @@ -26,9 +26,12 @@ def test_deprecated_kwarg(self): dsd0 = v21.DataStructureDefinition(id="FOO") dsd1 = v21.DataStructureDefinition(id="BAR") - with pytest.warns( - DeprecationWarning, match="dsd=.* keyword argument; use structure=" - ), pytest.raises(ValueError, match="Mismatched structure=FOO, dsd=BAR"): + with ( + pytest.warns( + DeprecationWarning, match="dsd=.* keyword argument; use structure=" + ), + pytest.raises(ValueError, match="Mismatched structure=FOO, dsd=BAR"), + ): r.read_message(None, structure=dsd0, dsd=dsd1) def test_detect(self, MinimalReader): diff --git a/sdmx/tests/test_client.py b/sdmx/tests/test_client.py index d74ad2c4..37519dbc 100644 --- a/sdmx/tests/test_client.py +++ b/sdmx/tests/test_client.py @@ -172,8 +172,11 @@ def test_v3_unsupported(self, testsource, client): headers={"Content-Type": "application/vnd.sdmx.data+xml; version=3.0.0"}, ) - with mock, pytest.raises( - ValueError, match="can't determine a reader for response content type" + with ( + mock, + pytest.raises( + ValueError, match="can't determine a reader for response content type" + ), ): client.get("data", resource_id=df_id, key=key) diff --git a/sdmx/tests/test_rest.py b/sdmx/tests/test_rest.py index d0170750..bf848947 100644 --- a/sdmx/tests/test_rest.py +++ b/sdmx/tests/test_rest.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Optional, Union import pytest @@ -77,9 +77,9 @@ def test_handle(self, p): _S = "?startPeriod=2024-02-12" R = Resource -PARAMS: Tuple[ +PARAMS: tuple[ Union[ - Tuple[Resource, Dict[str, Any], Optional[str], Optional[str]], + tuple[Resource, dict[str, Any], Optional[str], Optional[str]], "_pytest.mark.ParameterSet", ], ..., diff --git a/sdmx/tests/test_sources.py b/sdmx/tests/test_sources.py index 81eccdb9..ef829846 100644 --- a/sdmx/tests/test_sources.py +++ b/sdmx/tests/test_sources.py @@ -7,7 +7,7 @@ # TODO add a pytest argument for clearing this cache in conftest.py import logging from pathlib import Path -from typing import Any, Dict, Tuple, Type, Union +from typing import Any, Union import pytest import requests_mock @@ -31,17 +31,17 @@ class DataSourceTest: source_id: str #: Failures affecting **all** data sources, internal to :mod:`sdmx`. - xfail_common: Dict[str, Any] = {} + xfail_common: dict[str, Any] = {} #: Mapping of endpoint → Exception subclass. Tests of these endpoints are expected #: to fail with the given kind of exception. - xfail: Dict[str, Union[Type[Exception], Tuple[Type[Exception], str]]] = {} + xfail: dict[str, Union[type[Exception], tuple[type[Exception], str]]] = {} #: True to xfail if a 503 Error is returned. tolerate_503 = False #: Keyword arguments for particular endpoints. - endpoint_args: Dict[str, Dict[str, Any]] = {} + endpoint_args: dict[str, dict[str, Any]] = {} @pytest.fixture def cache_path(self, test_data_path): diff --git a/sdmx/tests/test_testing.py b/sdmx/tests/test_testing.py index 78121322..d8627a77 100644 --- a/sdmx/tests/test_testing.py +++ b/sdmx/tests/test_testing.py @@ -1,15 +1,8 @@ import json -import sys - -import pytest from sdmx.testing.report import main -@pytest.mark.skipif( - sys.version_info.minor < 9, - reason="Uses dict() | other, not available in Python 3.8", -) def test_report_main(tmp_path): # Example input data with open(tmp_path.joinpath("TEST.json"), "w") as f: diff --git a/sdmx/urn.py b/sdmx/urn.py index bb889a45..81db8e60 100644 --- a/sdmx/urn.py +++ b/sdmx/urn.py @@ -1,5 +1,5 @@ import re -from typing import Dict, Optional +from typing import Optional from sdmx.model import PACKAGE, MaintainableArtefact @@ -140,7 +140,7 @@ def make( ) -def match(value: str) -> Dict[str, str]: +def match(value: str) -> dict[str, str]: """Match :data:`URN` in `value`, returning a :class:`dict` with the match groups. Example diff --git a/sdmx/util/__init__.py b/sdmx/util/__init__.py index 889715ed..eab0c84a 100644 --- a/sdmx/util/__init__.py +++ b/sdmx/util/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Iterator from dataclasses import Field, fields from functools import lru_cache -from typing import Any, Dict, Iterable, List, Tuple +from typing import Any, Iterable import requests @@ -57,7 +57,7 @@ def only(iterator: Iterator) -> Any: @lru_cache() -def parse_content_type(value: str) -> Tuple[str, Dict[str, Any]]: +def parse_content_type(value: str) -> tuple[str, dict[str, Any]]: """Return content type and parameters from `value`. Modified from :mod:`requests.util`. @@ -85,7 +85,7 @@ def ucfirst(value: str) -> str: return value[0].upper() + value[1:] -_FIELDS_CACHE: Dict[str, List[Field]] = dict() +_FIELDS_CACHE: dict[str, list[Field]] = dict() def direct_fields(cls) -> Iterable[Field]: diff --git a/sdmx/util/item_structure.py b/sdmx/util/item_structure.py index bb5083d7..3eddd6fd 100644 --- a/sdmx/util/item_structure.py +++ b/sdmx/util/item_structure.py @@ -3,7 +3,7 @@ import logging import operator import re -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Optional, Union from sdmx.model.common import DEFAULT_LOCALE, Item, ItemScheme @@ -16,7 +16,7 @@ def parse_item_description( item: Item, locale: Optional[str] = None -) -> List[Tuple[Callable, str]]: +) -> list[tuple[Callable, str]]: """Parse the :attr:`.description` of `item` for a structure expression. A common—but **non-standard**—SDMX usage is that :class:`Items <.Item>` in @@ -119,7 +119,7 @@ def parse_item_description( def parse_item( itemscheme: ItemScheme, id=str, **kwargs -) -> List[Tuple[Callable, Union[Item, str]]]: +) -> list[tuple[Callable, Union[Item, str]]]: """Parse a structure expression for the item in `itemscheme` with the given `id`. In addition to the behaviour of :func:`parse_item_description`, :func:`parse_item` @@ -175,7 +175,7 @@ def parse_item( def parse_all( itemscheme: ItemScheme, **kwargs -) -> Dict[str, List[Tuple[Callable, Union[Item, str]]]]: +) -> dict[str, list[tuple[Callable, Union[Item, str]]]]: """Parse structure expressions for every item in `itemscheme`. Parameters diff --git a/sdmx/writer/csv.py b/sdmx/writer/csv.py index ce27d4bc..77b69386 100644 --- a/sdmx/writer/csv.py +++ b/sdmx/writer/csv.py @@ -4,7 +4,7 @@ """ from os import PathLike -from typing import Literal, Optional, Type, Union +from typing import Literal, Optional, Union import pandas as pd @@ -21,7 +21,7 @@ def to_csv( obj, *args, path: Optional[PathLike] = None, - rtype: Type[Union[str, pd.DataFrame]] = str, + rtype: type[Union[str, pd.DataFrame]] = str, **kwargs, ) -> Union[None, str, pd.DataFrame]: """Convert an SDMX *obj* to SDMX-CSV. diff --git a/sdmx/writer/pandas.py b/sdmx/writer/pandas.py index 80c57feb..9f341b77 100644 --- a/sdmx/writer/pandas.py +++ b/sdmx/writer/pandas.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Any, Dict, Hashable, Set, Union +from typing import Any, Hashable, Union import numpy as np import pandas as pd @@ -294,7 +294,7 @@ def write_dataset( # noqa: C901 TODO reduce complexity 12 → ≤11 raise ValueError(f"attributes must be in 'osgd'; got {attributes}") # Iterate on observations - data: Dict[Hashable, Dict[str, Any]] = {} + data: dict[Hashable, dict[str, Any]] = {} for observation in obj.obs: # Check that the Observation is within the constraint, if any key = observation.key.order() @@ -501,7 +501,7 @@ def write_itemscheme(obj: model.ItemScheme, locale=DEFAULT_LOCALE): pandas.Series or pandas.DataFrame """ items = {} - seen: Set[Item] = set() + seen: set[Item] = set() def add_item(item): """Recursive helper for adding items.""" diff --git a/sdmx/writer/xml.py b/sdmx/writer/xml.py index 1dac8cc1..54f2353e 100644 --- a/sdmx/writer/xml.py +++ b/sdmx/writer/xml.py @@ -6,7 +6,7 @@ # - writer functions for sdmx.model classes, in the same order as model.py import logging -from typing import Iterable, List, Literal, MutableMapping, Optional +from typing import Iterable, Literal, MutableMapping, Optional from lxml import etree from lxml.builder import ElementMaker @@ -263,7 +263,7 @@ def _footer(obj: message.Footer): # §3.2: Base structures -def i11lstring(obj, name) -> List[etree._Element]: +def i11lstring(obj, name) -> list[etree._Element]: """InternationalString. Returns a list of elements with name `name`.