diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f3b1571..f0d7b25 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -33,7 +33,7 @@ jobs: contents: read # Check out the repository strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/changes/73.removal.rst b/changes/73.removal.rst new file mode 100644 index 0000000..fd65bba --- /dev/null +++ b/changes/73.removal.rst @@ -0,0 +1 @@ +Drop Python 3.10 support, add Python 3.15 support diff --git a/didl_lite/didl_lite.py b/didl_lite/didl_lite.py index ecd2d9e..7dc4741 100644 --- a/didl_lite/didl_lite.py +++ b/didl_lite/didl_lite.py @@ -2,19 +2,8 @@ # pylint: disable=too-many-lines import re -from typing import ( - Any, - Dict, - Iterable, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, -) +from collections.abc import Iterable, Mapping, Sequence +from typing import Any, Self from xml.etree import ElementTree as ET import defusedxml.ElementTree @@ -28,11 +17,6 @@ to_camel_case, ) -TDO = TypeVar("TDO", bound="DidlObject") # pylint: disable=invalid-name -TC = TypeVar("TC", bound="Container") # pylint: disable=invalid-name -TD = TypeVar("TD", bound="Descriptor") # pylint: disable=invalid-name -TR = TypeVar("TR", bound="Resource") # pylint: disable=invalid-name - class DidlLiteException(Exception): """DIDL Lite Exception.""" @@ -41,16 +25,16 @@ class DidlLiteException(Exception): # region: DidlObjects # upnp_class to python type mapping -_upnp_class_map: Dict[str, Type["DidlObject"]] = {} -_upnp_class_map_lowercase: Dict[str, Type["DidlObject"]] = {} +_upnp_class_map: dict[str, type["DidlObject"]] = {} +_upnp_class_map_lowercase: dict[str, type["DidlObject"]] = {} class DidlObject: """DIDL Object.""" - tag: Optional[str] = None + tag: str | None = None upnp_class: str = "object" - didl_properties_defs: List[Tuple[str, str, str]] = [ + didl_properties_defs: list[tuple[str, str, str]] = [ ("didl_lite", "@id", "R"), ("didl_lite", "@parentID", "R"), ("didl_lite", "@restricted", "R"), @@ -63,12 +47,12 @@ class DidlObject: id: str parent_id: str - res: List["Resource"] - xml_el: Optional[ET.Element] + res: list["Resource"] + xml_el: ET.Element | None descriptors: Sequence["Descriptor"] @classmethod - def __init_subclass__(cls: Type["DidlObject"], **kwargs: Any) -> None: + def __init_subclass__(cls: type["DidlObject"], **kwargs: Any) -> None: """Create mapping of upnp_class to Python type for fast lookup.""" super().__init_subclass__(**kwargs) assert cls.upnp_class not in _upnp_class_map @@ -80,8 +64,8 @@ def __init__( self, id: str = "", parent_id: str = "", - descriptors: Optional[Sequence["Descriptor"]] = None, - xml_el: Optional[ET.Element] = None, + descriptors: Sequence["Descriptor"] | None = None, + xml_el: ET.Element | None = None, strict: bool = True, **properties: Any, ) -> None: @@ -124,14 +108,14 @@ def _set_properties(self, properties: Mapping[str, Any]) -> None: setattr(self, key, value) @classmethod - def from_xml(cls: Type[TDO], xml_el: ET.Element, strict: bool = True) -> TDO: + def from_xml(cls, xml_el: ET.Element, strict: bool = True) -> Self: """ Initialize from an XML node. I.e., parse XML and return instance. """ # pylint: disable=too-many-locals - properties = {} # type: Dict[str, Any] + properties: dict[str, Any] = {} # attributes for attr_key, attr_value in xml_el.attrib.items(): @@ -613,8 +597,8 @@ def __init__( self, id: str = "", parent_id: str = "", - descriptors: Optional[Sequence["Descriptor"]] = None, - xml_el: Optional[ET.Element] = None, + descriptors: Sequence["Descriptor"] | None = None, + xml_el: ET.Element | None = None, strict: bool = True, children: Iterable[DidlObject] = (), **properties: Any, @@ -625,7 +609,7 @@ def __init__( self.extend(children) @classmethod - def from_xml(cls: Type[TC], xml_el: ET.Element, strict: bool = True) -> TC: + def from_xml(cls, xml_el: ET.Element, strict: bool = True) -> Self: """ Initialize from an XML node. @@ -903,19 +887,19 @@ class Resource: def __init__( self, - uri: Optional[str], - protocol_info: Optional[str], - import_uri: Optional[str] = None, - size: Optional[str] = None, - duration: Optional[str] = None, - bitrate: Optional[str] = None, - sample_frequency: Optional[str] = None, - bits_per_sample: Optional[str] = None, - nr_audio_channels: Optional[str] = None, - resolution: Optional[str] = None, - color_depth: Optional[str] = None, - protection: Optional[str] = None, - xml_el: Optional[ET.Element] = None, + uri: str | None, + protocol_info: str | None, + import_uri: str | None = None, + size: str | None = None, + duration: str | None = None, + bitrate: str | None = None, + sample_frequency: str | None = None, + bits_per_sample: str | None = None, + nr_audio_channels: str | None = None, + resolution: str | None = None, + color_depth: str | None = None, + protection: str | None = None, + xml_el: ET.Element | None = None, ) -> None: """Initialize.""" # pylint: disable=too-many-arguments,too-many-positional-arguments @@ -934,7 +918,7 @@ def __init__( self.xml_el = xml_el @classmethod - def from_xml(cls: Type[TR], xml_el: ET.Element) -> TR: + def from_xml(cls, xml_el: ET.Element) -> Self: """Initialize from an XML node.""" uri = xml_el.text protocol_info = xml_el.attrib.get("protocolInfo") @@ -987,9 +971,9 @@ def __init__( self, id: str, name_space: str, - type: Optional[str] = None, - text: Optional[str] = None, - xml_el: Optional[ET.Element] = None, + type: str | None = None, + text: str | None = None, + xml_el: ET.Element | None = None, ) -> None: """Initialize.""" # pylint: disable=invalid-name,redefined-builtin,too-many-arguments,too-many-positional-arguments @@ -1000,7 +984,7 @@ def __init__( self.xml_el = xml_el @classmethod - def from_xml(cls: Type[TD], xml_el: ET.Element) -> TD: + def from_xml(cls, xml_el: ET.Element) -> Self: """Initialize from an XML node.""" id_ = xml_el.attrib["id"] name_space = xml_el.attrib["nameSpace"] @@ -1051,7 +1035,7 @@ def to_xml_string(*objects: DidlObject) -> bytes: return ET.tostring(root_el) -def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObject, Descriptor]]: +def from_xml_string(xml_string: str, strict: bool = True) -> list[DidlObject | Descriptor]: """Parse DIDL-Lite XML string.""" if not strict: # Find all prefixes used in tags, e.g., @@ -1087,9 +1071,9 @@ def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObje return from_xml_el(xml_el, strict) -def from_xml_el(xml_el: ET.Element, strict: bool = True) -> List[Union[DidlObject, Descriptor]]: +def from_xml_el(xml_el: ET.Element, strict: bool = True) -> list[DidlObject | Descriptor]: """Convert XML Element to DIDL Objects.""" - didl_objects = [] # type: List[Union[DidlObject, Descriptor]] + didl_objects: list[DidlObject | Descriptor] = [] # items and containers, in order for child_el in xml_el: @@ -1125,7 +1109,7 @@ def from_xml_el(xml_el: ET.Element, strict: bool = True) -> List[Union[DidlObjec # upnp_class to python type mapping -def type_by_upnp_class(upnp_class: str, strict: bool = True) -> Optional[Type[DidlObject]]: +def type_by_upnp_class(upnp_class: str, strict: bool = True) -> type[DidlObject] | None: """Get DidlObject-type by upnp_class. When strict is False, the upnp_class lookup will be done ignoring string diff --git a/didl_lite/utils.py b/didl_lite/utils.py index 0b1452c..40afba0 100644 --- a/didl_lite/utils.py +++ b/didl_lite/utils.py @@ -1,7 +1,6 @@ """Utilities.""" import re -from typing import Optional, Tuple NAMESPACES = { "didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/", @@ -27,7 +26,7 @@ def expand_namespace_tag(tag: str) -> str: return f"{{{namespace_uri}}}{tag}" -def split_namespace_tag(namespaced_tag: str) -> Tuple[Optional[str], str]: +def split_namespace_tag(namespaced_tag: str) -> tuple[str | None, str]: """ Extract namespace and tag from namespaced-tag. @@ -61,7 +60,7 @@ def didl_property_key(didl_property_name: str) -> str: return to_camel_case(didl_property_name.replace("@", "_")) -def didl_property_def_key(didl_property_def: Tuple[str, ...]) -> str: +def didl_property_def_key(didl_property_def: tuple[str, ...]) -> str: """Get Python property key for didl_property_def.""" if didl_property_def[1].startswith("@"): return to_camel_case(didl_property_def[1].replace("@", "")) diff --git a/pyproject.toml b/pyproject.toml index b026e9a..ba6dc5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", ] -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "defusedxml>=0.6.0", ] @@ -47,6 +47,7 @@ version = { attr = "didl_lite.__version__" } didl_lite = ["py.typed"] [tool.mypy] +python_version = "3.11" check_untyped_defs = true disallow_untyped_calls = true disallow_untyped_defs = true diff --git a/tox.ini b/tox.ini index 1138166..619aab3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] -envlist = py310, py311, py312, py313, py314, ruff, pylint, codespell, mypy, deptry +envlist = py311, py312, py313, py314, py315, ruff, pylint, codespell, mypy, deptry [gh-actions] python = - 3.10: py310 3.11: py311 3.12: py312, ruff, pylint, codespell, mypy, deptry 3.13: py313 3.14: py314 + 3.15: py315 [testenv] package = editable