From b915cde3d9968c38c09294e758f676c4a253c1b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:05:46 +0000 Subject: [PATCH 1/3] update deps(deps): bump the python-packages group across 1 directory with 5 updates Bumps the python-packages group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [lxml](https://github.com/lxml/lxml) | `5.2.1` | `5.3.0` | | [termcolor](https://github.com/termcolor/termcolor) | `2.4.0` | `2.5.0` | | [black](https://github.com/psf/black) | `24.4.0` | `24.10.0` | | [mypy](https://github.com/python/mypy) | `1.9.0` | `1.14.1` | | [pytest](https://github.com/pytest-dev/pytest) | `8.1.1` | `8.3.4` | Updates `lxml` from 5.2.1 to 5.3.0 - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-5.2.1...lxml-5.3.0) Updates `termcolor` from 2.4.0 to 2.5.0 - [Release notes](https://github.com/termcolor/termcolor/releases) - [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md) - [Commits](https://github.com/termcolor/termcolor/compare/2.4.0...2.5.0) Updates `black` from 24.4.0 to 24.10.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.4.0...24.10.0) Updates `mypy` from 1.9.0 to 1.14.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/1.9.0...v1.14.1) Updates `pytest` from 8.1.1 to 8.3.4 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.3.4) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: termcolor dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages ... Signed-off-by: dependabot[bot] --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 700d3b0..9e2f47a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,9 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "lxml==5.2.1", + "lxml==5.3.0", "lxml-stubs==0.5.1", - "termcolor==2.4.0", + "termcolor==2.5.0", "pyhumps==3.8.0", ] @@ -31,7 +31,7 @@ dependencies = [ "Source" = "https://github.com/OliverKillane/xmlable" [project.optional-dependencies] -dev = ["black==24.4.0", "mypy==1.9.0", "pytest==8.1.1"] +dev = ["black==24.10.0", "mypy==1.14.1", "pytest==8.3.4"] [tool.pytest.ini_options] minversion = "6.0" From 00967af033ae179c038b9da753ed7fd9f68aae4e Mon Sep 17 00:00:00 2001 From: Oliver Killane Date: Sun, 12 Jan 2025 16:54:31 +0100 Subject: [PATCH 2/3] Fixed mypy issues --- examples/complex_application/config.xsd | 16 ++++++++-------- examples/complex_application/ipconn.py | 4 ++-- examples/maps/config.xsd | 12 ++++++------ examples/userdefined/pgconn.py | 4 ++-- src/xmlable/_errors.py | 22 ++++++++++++---------- src/xmlable/_manual.py | 18 ++++++++++-------- src/xmlable/_user.py | 3 ++- src/xmlable/_utils.py | 6 ++++-- src/xmlable/_xmlify.py | 18 +++++++++++------- src/xmlable/_xobject.py | 22 +++++++++++++--------- 10 files changed, 70 insertions(+), 55 deletions(-) diff --git a/examples/complex_application/config.xsd b/examples/complex_application/config.xsd index 4afd03d..7762d02 100644 --- a/examples/complex_application/config.xsd +++ b/examples/complex_application/config.xsd @@ -1,5 +1,13 @@ + + + + + + + + @@ -12,14 +20,6 @@ - - - - - - - - diff --git a/examples/complex_application/ipconn.py b/examples/complex_application/ipconn.py index 9edb483..0997d5e 100644 --- a/examples/complex_application/ipconn.py +++ b/examples/complex_application/ipconn.py @@ -10,7 +10,7 @@ from xmlable._xobject import XObject from xmlable._user import IXmlify from xmlable._manual import manual_xmlify -from xmlable._utils import firstkey +from xmlable._utils import firstkey, AnyType import re @@ -109,5 +109,5 @@ def xsd_forward(add_ns: dict[str, str]) -> _Element: ) @staticmethod - def xsd_dependencies() -> set[type]: + def xsd_dependencies() -> set[AnyType]: return {IPv4Conn} diff --git a/examples/maps/config.xsd b/examples/maps/config.xsd index 45260da..a2fadb3 100644 --- a/examples/maps/config.xsd +++ b/examples/maps/config.xsd @@ -1,5 +1,11 @@ + + + + + + @@ -17,12 +23,6 @@ - - - - - - diff --git a/examples/userdefined/pgconn.py b/examples/userdefined/pgconn.py index c1e4b6f..7b23ce1 100644 --- a/examples/userdefined/pgconn.py +++ b/examples/userdefined/pgconn.py @@ -10,7 +10,7 @@ from xmlable._xobject import XObject from xmlable._user import IXmlify from xmlable._manual import manual_xmlify -from xmlable._utils import firstkey +from xmlable._utils import firstkey, AnyType import re @@ -127,5 +127,5 @@ def xsd_forward(add_ns: dict[str, str]) -> _Element: ) @staticmethod - def xsd_dependencies() -> set[type]: + def xsd_dependencies() -> set[AnyType]: return {PostgresConn} diff --git a/src/xmlable/_errors.py b/src/xmlable/_errors.py index 677228f..867607a 100644 --- a/src/xmlable/_errors.py +++ b/src/xmlable/_errors.py @@ -9,7 +9,7 @@ from termcolor import colored from termcolor.termcolor import Color -from xmlable._utils import typename +from xmlable._utils import typename, AnyType def trace_note(trace: list[str], arrow_c: Color, node_c: Color): @@ -146,8 +146,8 @@ def InvalidDictionaryItem( def InvalidVariant( ctx: XErrorCtx, name: str, - expected_types: list[type], - found_type: type | None, + expected_types: list[AnyType], + found_type: AnyType | None, found_value: Any, ) -> XError: types = " | ".join(map(str, expected_types)) @@ -188,7 +188,7 @@ def NoneIsSome(ctx: XErrorCtx, name: str, val: Any) -> XError: ) @staticmethod - def NotADataclass(cls: type) -> XError: + def NotADataclass(cls: AnyType) -> XError: cls_name: str = typename(cls) return XError( short="Non-Dataclass", @@ -199,7 +199,7 @@ def NotADataclass(cls: type) -> XError: ) @staticmethod - def ReservedAttribute(cls: type, attr_name: str) -> XError: + def ReservedAttribute(cls: AnyType, attr_name: str) -> XError: cls_name: str = typename(cls) return XError( short=f"Reserved Attribute", @@ -209,7 +209,7 @@ def ReservedAttribute(cls: type, attr_name: str) -> XError: ) @staticmethod - def CommentAttribute(cls: type) -> XError: + def CommentAttribute(cls: AnyType) -> XError: cls_name: str = typename(cls) return XError( short=f"Comment Attribute", @@ -219,7 +219,9 @@ def CommentAttribute(cls: type) -> XError: ) @staticmethod - def NonMemberTag(ctx: XErrorCtx, cls: type, tag: str, name: str) -> XError: + def NonMemberTag( + ctx: XErrorCtx, cls: AnyType, tag: str, name: str + ) -> XError: cls_name: str = typename(cls) return XError( short="Non member tag", @@ -230,7 +232,7 @@ def NonMemberTag(ctx: XErrorCtx, cls: type, tag: str, name: str) -> XError: @staticmethod def MissingAttribute( - cls: type, required_attrs: set[str], missing_attr: str + cls: AnyType, required_attrs: set[str], missing_attr: str ) -> XError: cls_name: str = typename(cls) return XError( @@ -241,7 +243,7 @@ def MissingAttribute( ) @staticmethod - def DependencyCycle(cycle: list[type]) -> XError: + def DependencyCycle(cycle: list[AnyType]) -> XError: return XError( short="Dependency Cycle in XSD", what=f"There is a cycle: {'<-'.join(map(str, cycle))}", @@ -249,7 +251,7 @@ def DependencyCycle(cycle: list[type]) -> XError: ) @staticmethod - def NotXmlified(cls: type) -> XError: + def NotXmlified(cls: AnyType) -> XError: cls_name: str = typename(cls) return XError( short="Not Xmlified", diff --git a/src/xmlable/_manual.py b/src/xmlable/_manual.py index a7c880e..8bbb848 100644 --- a/src/xmlable/_manual.py +++ b/src/xmlable/_manual.py @@ -8,23 +8,23 @@ from lxml.etree import _Element, Element, _ElementTree, ElementTree from lxml.objectify import ObjectifiedElement -from xmlable._utils import typename +from xmlable._utils import typename, AnyType from xmlable._lxml_helpers import with_children, XMLSchema from xmlable._errors import XError, XErrorCtx, ErrorTypes -def validate_manual_class(cls: type): +def validate_manual_class(cls: AnyType): attrs = {"get_xobject", "xsd_forward", "xsd_dependencies"} for attr in attrs: if not hasattr(cls, attr): raise ErrorTypes.MissingAttribute(cls, attrs, attr) -def type_cycle(from_type: type) -> list[type]: +def type_cycle(from_type: AnyType) -> list[AnyType]: # INV: it is an xmlified type for a user define structure - cycle: list[type] = [] + cycle: list[AnyType] = [] - def visit_dep(curr: type) -> bool: + def visit_dep(curr: AnyType) -> bool: if curr == from_type or any( visit_dep(dep) for dep in curr.xsd_dependencies() # type: ignore[attr-defined] ): @@ -71,10 +71,12 @@ def xsd( imports: dict[str, str] = {}, ) -> _ElementTree: # Get dependencies (user classes that need to be declared before) - visited: set[type] = set() - dec_order: list[type] = [] + visited: set[AnyType] = set() + dec_order: list[AnyType] = [] - def toposort(curr: type, visited: set[type], dec_order: list[type]): + def toposort( + curr: AnyType, visited: set[AnyType], dec_order: list[AnyType] + ): if curr in visited: raise ErrorTypes.DependencyCycle(type_cycle(curr)) visited.add(curr) diff --git a/src/xmlable/_user.py b/src/xmlable/_user.py index 85b675d..6822ae2 100644 --- a/src/xmlable/_user.py +++ b/src/xmlable/_user.py @@ -7,6 +7,7 @@ from abc import ABC, abstractmethod from lxml.etree import _Element from xmlable._xobject import XObject +from xmlable._utils import AnyType class IXmlify(ABC): @@ -54,7 +55,7 @@ def xsd_forward(add_ns: dict[str, str]) -> _Element: @staticmethod @abstractmethod - def xsd_dependencies() -> set[type]: + def xsd_dependencies() -> set[AnyType]: """ The user classes that need to be before this first diff --git a/src/xmlable/_utils.py b/src/xmlable/_utils.py index 2e8beb9..ff821a8 100644 --- a/src/xmlable/_utils.py +++ b/src/xmlable/_utils.py @@ -6,8 +6,10 @@ - typenames """ -from typing import Any, Callable, TypeVar +from typing import Any, Callable, TypeVar, TypeAlias, Type +from types import GenericAlias +AnyType: TypeAlias = Type | GenericAlias T = TypeVar("T") @@ -47,7 +49,7 @@ def firstkey(d: dict[X, Y], val: Y) -> X | None: return None -def typename(t: type) -> str: +def typename(t: AnyType) -> str: if t is None: return "None" else: diff --git a/src/xmlable/_xmlify.py b/src/xmlable/_xmlify.py index 88c6ef3..e793bb9 100644 --- a/src/xmlable/_xmlify.py +++ b/src/xmlable/_xmlify.py @@ -11,18 +11,18 @@ from humps import pascalize from dataclasses import fields, is_dataclass -from typing import Any, dataclass_transform +from typing import Any, dataclass_transform, cast from lxml.objectify import ObjectifiedElement from lxml.etree import Element, _Element -from xmlable._utils import get, typename +from xmlable._utils import get, typename, AnyType from xmlable._errors import XError, XErrorCtx, ErrorTypes from xmlable._manual import manual_xmlify from xmlable._lxml_helpers import with_children, with_child, XMLSchema from xmlable._xobject import XObject, gen_xobject -def validate_class(cls: type): +def validate_class(cls: AnyType): """ Validate tha the class can be xmlified - Must be a dataclass @@ -52,14 +52,18 @@ def validate_class(cls: type): @dataclass_transform() -def xmlify(cls: type) -> type: +def xmlify(cls: type) -> AnyType: try: validate_class(cls) cls_name = typename(cls) - forward_decs = {cls} + forward_decs = cast(set[AnyType], {cls}) meta_xobjects = [ - (pascalize(f.name), f, gen_xobject(f.type, forward_decs)) + ( + pascalize(f.name), + f, + gen_xobject(cast(AnyType, f.type), forward_decs), + ) for f in fields(cls) ] @@ -132,7 +136,7 @@ def xsd_forward(add_ns: dict[str, str]) -> _Element: ), ) - def xsd_dependencies() -> set[type]: + def xsd_dependencies() -> set[AnyType]: return forward_decs def get_xobject(): diff --git a/src/xmlable/_xobject.py b/src/xmlable/_xobject.py index 8d46e90..7675492 100644 --- a/src/xmlable/_xobject.py +++ b/src/xmlable/_xobject.py @@ -10,9 +10,10 @@ from lxml.objectify import ObjectifiedElement from lxml.etree import Element, Comment, _Element from abc import ABC, abstractmethod -from typing import Any, Callable, get_args +from typing import Any, Callable, Type, get_args, TypeAlias, cast +from types import GenericAlias -from xmlable._utils import get, typename, firstkey +from xmlable._utils import get, typename, firstkey, AnyType from xmlable._errors import XErrorCtx, ErrorTypes from xmlable._lxml_helpers import ( with_text, @@ -437,7 +438,7 @@ def xml_in(self, obj: ObjectifiedElement, ctx: XErrorCtx) -> dict[Any, Any]: return parsed -def resolve_type(v: Any) -> type | None: +def resolve_type(v: Any) -> AnyType: """Determine the type of some value, using primitive types - If empty container, only provide top container type INV: only generic types for v are {tuple, list, dict, set} @@ -463,8 +464,8 @@ def resolve_type(v: Any) -> type | None: class UnionObj(XObject): """A variant, can be one of several different types""" - xobjects: dict[type, XObject] - elem_gen: Callable[[type], str] = lambda t: pascalize(typename(t)) + xobjects: dict[AnyType, XObject] + elem_gen: Callable[[AnyType], str] = lambda t: pascalize(typename(t)) def xsd_out( self, @@ -508,7 +509,7 @@ def xml_temp(self, name: str) -> _Element: def xml_out(self, name: str, val: Any, ctx: XErrorCtx) -> _Element: t = resolve_type(val) - if t is not None and (val_xobj := self.xobjects.get(t)) is not None: + if (val_xobj := self.xobjects.get(t)) is not None: variant_name = self.elem_gen(t) return with_child( Element(name), @@ -577,9 +578,9 @@ def is_xmlified(cls): ) -def gen_xobject(data_type: type, forward_dec: set[type]) -> XObject: +def gen_xobject(data_type: AnyType, forward_dec: set[AnyType]) -> XObject: basic_types: dict[ - type, tuple[str, Callable[[Any], str], Callable[[Any], bool]] + AnyType, tuple[str, Callable[[Any], str], Callable[[Any], bool]] ] = { int: ("integer", str, lambda d: type(d) == int), str: ("string", str, lambda d: type(d) == str), @@ -593,7 +594,10 @@ def gen_xobject(data_type: type, forward_dec: set[type]) -> XObject: if (basic_entry := basic_types.get(data_type)) is not None: type_str, convert_fn, validate_fn = basic_entry - return BasicObj(type_str, convert_fn, validate_fn, data_type) + # NOTE: here was can pass the parse_fn as the data type, as the name is + # also a constructor. (e.g. `int` -> `int("23") == 32`) + parse_fn = cast(Callable[[ObjectifiedElement], Any], data_type) + return BasicObj(type_str, convert_fn, validate_fn, parse_fn) elif isinstance(data_type, NoneType) or data_type == NoneType: # NOTE: Python typing cringe: None can be both a type and a value # (even when within a type hint!) From cd48c6dde8db81623c146c192cb235064d715986 Mon Sep 17 00:00:00 2001 From: Oliver Killane Date: Sun, 12 Jan 2025 17:09:23 +0100 Subject: [PATCH 3/3] Using ordered iteration to ensure xsds are deterministically generated --- examples/maps/config.xsd | 12 ++++++------ src/xmlable/_manual.py | 6 +++--- src/xmlable/_utils.py | 9 ++++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/maps/config.xsd b/examples/maps/config.xsd index a2fadb3..45260da 100644 --- a/examples/maps/config.xsd +++ b/examples/maps/config.xsd @@ -1,11 +1,5 @@ - - - - - - @@ -23,6 +17,12 @@ + + + + + + diff --git a/src/xmlable/_manual.py b/src/xmlable/_manual.py index 8bbb848..1f042d0 100644 --- a/src/xmlable/_manual.py +++ b/src/xmlable/_manual.py @@ -8,7 +8,7 @@ from lxml.etree import _Element, Element, _ElementTree, ElementTree from lxml.objectify import ObjectifiedElement -from xmlable._utils import typename, AnyType +from xmlable._utils import typename, AnyType, ordered_iter from xmlable._lxml_helpers import with_children, XMLSchema from xmlable._errors import XError, XErrorCtx, ErrorTypes @@ -26,7 +26,7 @@ def type_cycle(from_type: AnyType) -> list[AnyType]: def visit_dep(curr: AnyType) -> bool: if curr == from_type or any( - visit_dep(dep) for dep in curr.xsd_dependencies() # type: ignore[attr-defined] + visit_dep(dep) for dep in ordered_iter(curr.xsd_dependencies()) # type: ignore[attr-defined] ): cycle.append(curr) return True @@ -81,7 +81,7 @@ def toposort( raise ErrorTypes.DependencyCycle(type_cycle(curr)) visited.add(curr) deps = curr.xsd_dependencies() # type: ignore[attr-defined] - for d in deps: + for d in ordered_iter(deps): if d not in visited: toposort(d, visited, dec_order) dec_order.append(curr) diff --git a/src/xmlable/_utils.py b/src/xmlable/_utils.py index ff821a8..1878229 100644 --- a/src/xmlable/_utils.py +++ b/src/xmlable/_utils.py @@ -6,7 +6,7 @@ - typenames """ -from typing import Any, Callable, TypeVar, TypeAlias, Type +from typing import Any, Callable, TypeVar, TypeAlias, Type, Iterable from types import GenericAlias AnyType: TypeAlias = Type | GenericAlias @@ -54,3 +54,10 @@ def typename(t: AnyType) -> str: return "None" else: return t.__name__ + + +Z = TypeVar("Z") + + +def ordered_iter(types: Iterable[Z]) -> list[Z]: + return sorted(list(types), key=str)