Skip to content

Commit

Permalink
Merge pull request #44 from OliverKillane/dependabot/pip/python-packa…
Browse files Browse the repository at this point in the history
…ges-85c47425a1

update deps(deps): bump the python-packages group across 1 directory with 5 updates
  • Loading branch information
OliverKillane authored Jan 12, 2025
2 parents 1dea982 + cd48c6d commit 65d05a8
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 54 deletions.
16 changes: 8 additions & 8 deletions examples/complex_application/config.xsd
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" id="MyPythonApp" elementFormDefault="qualified">
<xs:complexType name="Inspect">
<xs:sequence>
<xs:element name="DebugLogs" type="xs:boolean"/>
<xs:element name="MetricsUrl" type="xs:string"/>
<xs:element name="ControlTimeout" type="xs:integer"/>
<xs:element name="ControlPort" type="xs:integer"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="IPv4Conn">
<xs:restriction base="xs:string">
<xs:pattern value="(.*):(\d*)\.(\d*)\.(\d*)\.(\d*):(\d*)"/>
Expand All @@ -12,14 +20,6 @@
<xs:element name="Conn" type="IPv4Conn"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Inspect">
<xs:sequence>
<xs:element name="DebugLogs" type="xs:boolean"/>
<xs:element name="MetricsUrl" type="xs:string"/>
<xs:element name="ControlTimeout" type="xs:integer"/>
<xs:element name="ControlPort" type="xs:integer"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="UserConfig">
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
Expand Down
4 changes: 2 additions & 2 deletions examples/complex_application/ipconn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
4 changes: 2 additions & 2 deletions examples/userdefined/pgconn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand All @@ -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"
Expand Down
22 changes: 12 additions & 10 deletions src/xmlable/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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(
Expand All @@ -241,15 +243,15 @@ 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))}",
why="The XSDs for classes are written to the .xsd file in dependency order",
)

@staticmethod
def NotXmlified(cls: type) -> XError:
def NotXmlified(cls: AnyType) -> XError:
cls_name: str = typename(cls)
return XError(
short="Not Xmlified",
Expand Down
22 changes: 12 additions & 10 deletions src/xmlable/_manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@
from lxml.etree import _Element, Element, _ElementTree, ElementTree
from lxml.objectify import ObjectifiedElement

from xmlable._utils import typename
from xmlable._utils import typename, AnyType, ordered_iter
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]
visit_dep(dep) for dep in ordered_iter(curr.xsd_dependencies()) # type: ignore[attr-defined]
):
cycle.append(curr)
return True
Expand Down Expand Up @@ -71,15 +71,17 @@ 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)
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)
Expand Down
3 changes: 2 additions & 1 deletion src/xmlable/_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions src/xmlable/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
- typenames
"""

from typing import Any, Callable, TypeVar
from typing import Any, Callable, TypeVar, TypeAlias, Type, Iterable
from types import GenericAlias

AnyType: TypeAlias = Type | GenericAlias

T = TypeVar("T")

Expand Down Expand Up @@ -47,8 +49,15 @@ 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:
return t.__name__


Z = TypeVar("Z")


def ordered_iter(types: Iterable[Z]) -> list[Z]:
return sorted(list(types), key=str)
18 changes: 11 additions & 7 deletions src/xmlable/_xmlify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
]

Expand Down Expand Up @@ -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():
Expand Down
22 changes: 13 additions & 9 deletions src/xmlable/_xobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand All @@ -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!)
Expand Down

0 comments on commit 65d05a8

Please sign in to comment.