Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions changes/73.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop Python 3.10 support, add Python 3.15 support
92 changes: 38 additions & 54 deletions didl_lite/didl_lite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand All @@ -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"),
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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,
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand Down Expand Up @@ -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., <prefix:tag ...>
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions didl_lite/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Utilities."""

import re
from typing import Optional, Tuple

NAMESPACES = {
"didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
Expand All @@ -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.

Expand Down Expand Up @@ -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("@", ""))
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading