python-didl-lite 1.5.1__tar.gz → 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-didl-lite
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: DIDL-Lite (Digital Item Declaration Language) tools for Python
5
5
  Author-email: Steven Looman <steven.looman@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -9,12 +9,12 @@ Keywords: DIDL,DIDL-lite
9
9
  Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python
12
- Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
16
15
  Classifier: Programming Language :: Python :: 3.14
17
- Requires-Python: >=3.10
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/x-rst
19
19
  Requires-Dist: defusedxml>=0.6.0
20
20
  Provides-Extra: test
@@ -2,4 +2,4 @@
2
2
 
3
3
  from didl_lite import didl_lite # noqa: F401
4
4
 
5
- __version__ = "1.5.1"
5
+ __version__ = "1.5.2"
@@ -2,19 +2,8 @@
2
2
  # pylint: disable=too-many-lines
3
3
 
4
4
  import re
5
- from typing import (
6
- Any,
7
- Dict,
8
- Iterable,
9
- List,
10
- Mapping,
11
- Optional,
12
- Sequence,
13
- Tuple,
14
- Type,
15
- TypeVar,
16
- Union,
17
- )
5
+ from collections.abc import Iterable, Mapping, Sequence
6
+ from typing import Any, Self
18
7
  from xml.etree import ElementTree as ET
19
8
 
20
9
  import defusedxml.ElementTree
@@ -28,11 +17,6 @@ from .utils import (
28
17
  to_camel_case,
29
18
  )
30
19
 
31
- TDO = TypeVar("TDO", bound="DidlObject") # pylint: disable=invalid-name
32
- TC = TypeVar("TC", bound="Container") # pylint: disable=invalid-name
33
- TD = TypeVar("TD", bound="Descriptor") # pylint: disable=invalid-name
34
- TR = TypeVar("TR", bound="Resource") # pylint: disable=invalid-name
35
-
36
20
 
37
21
  class DidlLiteException(Exception):
38
22
  """DIDL Lite Exception."""
@@ -41,16 +25,16 @@ class DidlLiteException(Exception):
41
25
  # region: DidlObjects
42
26
 
43
27
  # upnp_class to python type mapping
44
- _upnp_class_map: Dict[str, Type["DidlObject"]] = {}
45
- _upnp_class_map_lowercase: Dict[str, Type["DidlObject"]] = {}
28
+ _upnp_class_map: dict[str, type["DidlObject"]] = {}
29
+ _upnp_class_map_lowercase: dict[str, type["DidlObject"]] = {}
46
30
 
47
31
 
48
32
  class DidlObject:
49
33
  """DIDL Object."""
50
34
 
51
- tag: Optional[str] = None
35
+ tag: str | None = None
52
36
  upnp_class: str = "object"
53
- didl_properties_defs: List[Tuple[str, str, str]] = [
37
+ didl_properties_defs: list[tuple[str, str, str]] = [
54
38
  ("didl_lite", "@id", "R"),
55
39
  ("didl_lite", "@parentID", "R"),
56
40
  ("didl_lite", "@restricted", "R"),
@@ -63,12 +47,12 @@ class DidlObject:
63
47
 
64
48
  id: str
65
49
  parent_id: str
66
- res: List["Resource"]
67
- xml_el: Optional[ET.Element]
50
+ res: list["Resource"]
51
+ xml_el: ET.Element | None
68
52
  descriptors: Sequence["Descriptor"]
69
53
 
70
54
  @classmethod
71
- def __init_subclass__(cls: Type["DidlObject"], **kwargs: Any) -> None:
55
+ def __init_subclass__(cls: type["DidlObject"], **kwargs: Any) -> None:
72
56
  """Create mapping of upnp_class to Python type for fast lookup."""
73
57
  super().__init_subclass__(**kwargs)
74
58
  assert cls.upnp_class not in _upnp_class_map
@@ -80,8 +64,8 @@ class DidlObject:
80
64
  self,
81
65
  id: str = "",
82
66
  parent_id: str = "",
83
- descriptors: Optional[Sequence["Descriptor"]] = None,
84
- xml_el: Optional[ET.Element] = None,
67
+ descriptors: Sequence["Descriptor"] | None = None,
68
+ xml_el: ET.Element | None = None,
85
69
  strict: bool = True,
86
70
  **properties: Any,
87
71
  ) -> None:
@@ -124,14 +108,14 @@ class DidlObject:
124
108
  setattr(self, key, value)
125
109
 
126
110
  @classmethod
127
- def from_xml(cls: Type[TDO], xml_el: ET.Element, strict: bool = True) -> TDO:
111
+ def from_xml(cls, xml_el: ET.Element, strict: bool = True) -> Self:
128
112
  """
129
113
  Initialize from an XML node.
130
114
 
131
115
  I.e., parse XML and return instance.
132
116
  """
133
117
  # pylint: disable=too-many-locals
134
- properties = {} # type: Dict[str, Any]
118
+ properties: dict[str, Any] = {}
135
119
 
136
120
  # attributes
137
121
  for attr_key, attr_value in xml_el.attrib.items():
@@ -613,8 +597,8 @@ class Container(DidlObject, list):
613
597
  self,
614
598
  id: str = "",
615
599
  parent_id: str = "",
616
- descriptors: Optional[Sequence["Descriptor"]] = None,
617
- xml_el: Optional[ET.Element] = None,
600
+ descriptors: Sequence["Descriptor"] | None = None,
601
+ xml_el: ET.Element | None = None,
618
602
  strict: bool = True,
619
603
  children: Iterable[DidlObject] = (),
620
604
  **properties: Any,
@@ -625,7 +609,7 @@ class Container(DidlObject, list):
625
609
  self.extend(children)
626
610
 
627
611
  @classmethod
628
- def from_xml(cls: Type[TC], xml_el: ET.Element, strict: bool = True) -> TC:
612
+ def from_xml(cls, xml_el: ET.Element, strict: bool = True) -> Self:
629
613
  """
630
614
  Initialize from an XML node.
631
615
 
@@ -903,19 +887,19 @@ class Resource:
903
887
 
904
888
  def __init__(
905
889
  self,
906
- uri: Optional[str],
907
- protocol_info: Optional[str],
908
- import_uri: Optional[str] = None,
909
- size: Optional[str] = None,
910
- duration: Optional[str] = None,
911
- bitrate: Optional[str] = None,
912
- sample_frequency: Optional[str] = None,
913
- bits_per_sample: Optional[str] = None,
914
- nr_audio_channels: Optional[str] = None,
915
- resolution: Optional[str] = None,
916
- color_depth: Optional[str] = None,
917
- protection: Optional[str] = None,
918
- xml_el: Optional[ET.Element] = None,
890
+ uri: str | None,
891
+ protocol_info: str | None,
892
+ import_uri: str | None = None,
893
+ size: str | None = None,
894
+ duration: str | None = None,
895
+ bitrate: str | None = None,
896
+ sample_frequency: str | None = None,
897
+ bits_per_sample: str | None = None,
898
+ nr_audio_channels: str | None = None,
899
+ resolution: str | None = None,
900
+ color_depth: str | None = None,
901
+ protection: str | None = None,
902
+ xml_el: ET.Element | None = None,
919
903
  ) -> None:
920
904
  """Initialize."""
921
905
  # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -934,7 +918,7 @@ class Resource:
934
918
  self.xml_el = xml_el
935
919
 
936
920
  @classmethod
937
- def from_xml(cls: Type[TR], xml_el: ET.Element) -> TR:
921
+ def from_xml(cls, xml_el: ET.Element) -> Self:
938
922
  """Initialize from an XML node."""
939
923
  uri = xml_el.text
940
924
  protocol_info = xml_el.attrib.get("protocolInfo")
@@ -987,9 +971,9 @@ class Descriptor:
987
971
  self,
988
972
  id: str,
989
973
  name_space: str,
990
- type: Optional[str] = None,
991
- text: Optional[str] = None,
992
- xml_el: Optional[ET.Element] = None,
974
+ type: str | None = None,
975
+ text: str | None = None,
976
+ xml_el: ET.Element | None = None,
993
977
  ) -> None:
994
978
  """Initialize."""
995
979
  # pylint: disable=invalid-name,redefined-builtin,too-many-arguments,too-many-positional-arguments
@@ -1000,7 +984,7 @@ class Descriptor:
1000
984
  self.xml_el = xml_el
1001
985
 
1002
986
  @classmethod
1003
- def from_xml(cls: Type[TD], xml_el: ET.Element) -> TD:
987
+ def from_xml(cls, xml_el: ET.Element) -> Self:
1004
988
  """Initialize from an XML node."""
1005
989
  id_ = xml_el.attrib["id"]
1006
990
  name_space = xml_el.attrib["nameSpace"]
@@ -1051,7 +1035,7 @@ def to_xml_string(*objects: DidlObject) -> bytes:
1051
1035
  return ET.tostring(root_el)
1052
1036
 
1053
1037
 
1054
- def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObject, Descriptor]]:
1038
+ def from_xml_string(xml_string: str, strict: bool = True) -> list[DidlObject | Descriptor]:
1055
1039
  """Parse DIDL-Lite XML string."""
1056
1040
  if not strict:
1057
1041
  # Find all prefixes used in tags, e.g., <prefix:tag ...>
@@ -1087,9 +1071,9 @@ def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObje
1087
1071
  return from_xml_el(xml_el, strict)
1088
1072
 
1089
1073
 
1090
- def from_xml_el(xml_el: ET.Element, strict: bool = True) -> List[Union[DidlObject, Descriptor]]:
1074
+ def from_xml_el(xml_el: ET.Element, strict: bool = True) -> list[DidlObject | Descriptor]:
1091
1075
  """Convert XML Element to DIDL Objects."""
1092
- didl_objects = [] # type: List[Union[DidlObject, Descriptor]]
1076
+ didl_objects: list[DidlObject | Descriptor] = []
1093
1077
 
1094
1078
  # items and containers, in order
1095
1079
  for child_el in xml_el:
@@ -1125,7 +1109,7 @@ def from_xml_el(xml_el: ET.Element, strict: bool = True) -> List[Union[DidlObjec
1125
1109
 
1126
1110
 
1127
1111
  # upnp_class to python type mapping
1128
- def type_by_upnp_class(upnp_class: str, strict: bool = True) -> Optional[Type[DidlObject]]:
1112
+ def type_by_upnp_class(upnp_class: str, strict: bool = True) -> type[DidlObject] | None:
1129
1113
  """Get DidlObject-type by upnp_class.
1130
1114
 
1131
1115
  When strict is False, the upnp_class lookup will be done ignoring string
@@ -1,7 +1,7 @@
1
1
  """Utilities."""
2
2
 
3
+ import functools
3
4
  import re
4
- from typing import Optional, Tuple
5
5
 
6
6
  NAMESPACES = {
7
7
  "didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
@@ -27,7 +27,7 @@ def expand_namespace_tag(tag: str) -> str:
27
27
  return f"{{{namespace_uri}}}{tag}"
28
28
 
29
29
 
30
- def split_namespace_tag(namespaced_tag: str) -> Tuple[Optional[str], str]:
30
+ def split_namespace_tag(namespaced_tag: str) -> tuple[str | None, str]:
31
31
  """
32
32
  Extract namespace and tag from namespaced-tag.
33
33
 
@@ -43,6 +43,10 @@ def split_namespace_tag(namespaced_tag: str) -> Tuple[Optional[str], str]:
43
43
  return namespace, tag
44
44
 
45
45
 
46
+ # Cached: called for every attribute set on a DidlObject, with a small closed
47
+ # set of property names. Bounded, as from_xml also passes names read from
48
+ # documents received from other devices.
49
+ @functools.lru_cache(maxsize=256)
46
50
  def to_camel_case(name: str) -> str:
47
51
  """Get camel case of name."""
48
52
  sub1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
@@ -61,7 +65,7 @@ def didl_property_key(didl_property_name: str) -> str:
61
65
  return to_camel_case(didl_property_name.replace("@", "_"))
62
66
 
63
67
 
64
- def didl_property_def_key(didl_property_def: Tuple[str, ...]) -> str:
68
+ def didl_property_def_key(didl_property_def: tuple[str, ...]) -> str:
65
69
  """Get Python property key for didl_property_def."""
66
70
  if didl_property_def[1].startswith("@"):
67
71
  return to_camel_case(didl_property_def[1].replace("@", ""))
@@ -17,13 +17,13 @@ classifiers = [
17
17
  "Development Status :: 5 - Production/Stable",
18
18
  "Intended Audience :: Developers",
19
19
  "Programming Language :: Python",
20
- "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
22
21
  "Programming Language :: Python :: 3.12",
23
22
  "Programming Language :: Python :: 3.13",
24
23
  "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: 3.15",
25
25
  ]
26
- requires-python = ">=3.10"
26
+ requires-python = ">=3.11"
27
27
  dependencies = [
28
28
  "defusedxml>=0.6.0",
29
29
  ]
@@ -47,6 +47,7 @@ version = { attr = "didl_lite.__version__" }
47
47
  didl_lite = ["py.typed"]
48
48
 
49
49
  [tool.mypy]
50
+ python_version = "3.11"
50
51
  check_untyped_defs = true
51
52
  disallow_untyped_calls = true
52
53
  disallow_untyped_defs = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-didl-lite
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: DIDL-Lite (Digital Item Declaration Language) tools for Python
5
5
  Author-email: Steven Looman <steven.looman@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -9,12 +9,12 @@ Keywords: DIDL,DIDL-lite
9
9
  Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python
12
- Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
16
15
  Classifier: Programming Language :: Python :: 3.14
17
- Requires-Python: >=3.10
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/x-rst
19
19
  Requires-Dist: defusedxml>=0.6.0
20
20
  Provides-Extra: test
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.5.1
2
+ current_version = 1.5.2
3
3
  commit = True
4
4
  tag = False
5
5
  tag_name = {new_version}
@@ -3,7 +3,7 @@
3
3
  import pytest
4
4
  from defusedxml import ElementTree as ET
5
5
 
6
- from didl_lite import didl_lite
6
+ from didl_lite import didl_lite, utils
7
7
 
8
8
  NAMESPACES = {
9
9
  "didl_lite": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
@@ -793,3 +793,13 @@ class TestDidlLite:
793
793
 
794
794
  assert b"<upnp:artist>My Artist</upnp:artist>" in xml
795
795
  assert b"<upnp:genre>My Genre</upnp:genre>" in xml
796
+
797
+ def test_to_camel_case_is_cached(self) -> None:
798
+ """Test to_camel_case is cached, and used cached through didl_lite."""
799
+ assert hasattr(utils.to_camel_case, "cache_info")
800
+ # didl_lite imports the name; it must be the cached function too.
801
+ assert didl_lite.to_camel_case is utils.to_camel_case
802
+
803
+ hits_before = utils.to_camel_case.cache_info().hits
804
+ didl_lite.MusicTrack(id="0", parent_id="0", title="Title", restricted="1")
805
+ assert utils.to_camel_case.cache_info().hits > hits_before