Skip to content

Commit

Permalink
typing: Let the linter have its will
Browse files Browse the repository at this point in the history
  • Loading branch information
funkyfuture committed Jan 8, 2025
1 parent 2557d19 commit f46745f
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 64 deletions.
14 changes: 14 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ Changes
The listed updates resemble rather a Best Of than a full record of changes.
Intentionally.

0.5.1 (2025-01-08)
------------------

News
~~~~

- Further deprecations that emit messages with hints to alternatives if
available:
- :meth:`NodeBase.new_tag_node`
- Empty / null namespaces will generally be represented as empty strings in
the future.
- :attr:`TagNode.parse`



0.5 (2025-01-01)
----------------
Expand Down
115 changes: 76 additions & 39 deletions _delb/nodes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2018-'24 Frank Sachsenheim
# Copyright (C) 2018-'25 Frank Sachsenheim
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
Expand Down Expand Up @@ -352,7 +352,7 @@ def new_tag_node(
"""

result = _wrapper_cache(
etree.Element(QName(namespace, local_name).text),
etree.Element(QName(namespace or None, local_name).text),
)
assert isinstance(result, TagNode)
if attributes is not None:
Expand Down Expand Up @@ -431,8 +431,7 @@ def tag(*args): # noqa: C901
want to) of :class:`TagNode` instances as:
- ``node`` argument to methods that add nodes to a tree
- items in the ``children`` argument of :func:`new_tag_node` and
:meth:`NodeBase.new_tag_node`
- items in the ``children`` argument of :func:`new_tag_node`
The first argument to the function is always the local name of the tag node.
Optionally, the second argument can be a :term:`mapping` that specifies attributes
Expand Down Expand Up @@ -468,7 +467,7 @@ def prepare_attributes(

for key, value in attributes.items():
if isinstance(value, Attribute):
result[(value.namespace, value.local_name)] = value.value
result[value._qualified_name] = value.value
elif isinstance(key, slice):
result[(key.start, key.stop)] = value
elif isinstance(key, (str, tuple)):
Expand Down Expand Up @@ -599,7 +598,7 @@ def _set_new_key(self, namespace: str, name: str):
return

attributes = self._attributes
current = self.namespace or "", self.local_name
current = self._namespace, self.local_name
assert attributes is not None
attributes[(namespace, name)] = self.value
self._qualified_name = (namespace, name)
Expand All @@ -613,26 +612,46 @@ def local_name(self) -> str:

@local_name.setter
def local_name(self, name: str):
self._set_new_key(self.namespace or "", name)
self._set_new_key(self._namespace, name)

@property
def namespace(self) -> Optional[str]:
"""The attribute's namespace"""
return self._qualified_name[0] or None
if result := self._qualified_name[0] or None:
return result
else: # pragma: nocover
warnings.warn(
"With delb 0.6 this will return an empty string to represent an "
"empty/null namespace. You can use the the intermediate "
"`Attribute._namespace` with that future behaviour.",
category=DeprecationWarning,
)
return None

@namespace.setter
def namespace(self, namespace: Optional[str]):
if namespace is None:
warnings.warn(
"With delb 0.6 empty/null namespaces will have to be expressed as "
"empty strings"
)
self._set_new_key(namespace or "", self.local_name)

@property
def _namespace(self) -> str:
return self._qualified_name[0]

@property
def universal_name(self) -> str:
"""
The attribute's namespace and local name in `Clark notation`_.
.. _Clark notation: http://www.jclark.com/xml/xmlns.htm
"""
namespace = self.namespace
return f"{{{namespace}}}{self.local_name}" if namespace else self.local_name
if namespace := self._namespace:
return f"{{{namespace}}}{self.local_name}"
else:
return self.local_name

@property
def value(self) -> str:
Expand Down Expand Up @@ -706,9 +725,7 @@ def __eq__(self, other: Any) -> bool:
if isinstance(other, TagAttributes):
for key, attribute in self.items():
assert isinstance(attribute, Attribute)
other_value = other.get(
(attribute.namespace or "", attribute.local_name)
)
other_value = other.get((attribute._namespace, attribute.local_name))
if (other_value is None) or (attribute != other_value):
return False
return True
Expand Down Expand Up @@ -780,9 +797,7 @@ def __resolve_accessor(self, item: AttributeAccessor) -> tuple[str, str]:
raise TypeError(ATTRIBUTE_ACCESSOR_MSG)

if namespace is None:
namespace = self.__node.namespace
if namespace is None:
namespace = ""
namespace = self.__node._namespace

return namespace, name

Expand Down Expand Up @@ -1232,7 +1247,7 @@ def _new_tag_node_from(
context_namespace = QName(context).namespace

if namespace:
tag = QName(namespace, local_name)
tag = QName(namespace or None, local_name)
elif context_namespace:
tag = QName(context_namespace, local_name)
else:
Expand Down Expand Up @@ -1435,6 +1450,11 @@ def new_tag_node(
namespace: Optional[str] = None,
children: Sequence[str | NodeBase | _TagDefinition] = (),
) -> TagNode:
warnings.warn(
"The node methods `new_tag_node` will be removed. Use :func:`new_tag_node`"
"instead.",
category=DeprecationWarning,
)
parent = self.parent
if parent is None:
return new_tag_node(
Expand Down Expand Up @@ -1733,9 +1753,7 @@ class TagNode(_ElementWrappingNode, NodeBase):
The instances of this class represent :term:`tag node` s of a tree, the equivalent
of DOM's elements.
To instantiate new nodes use :class:`Document.new_tag_node`,
:class:`TagNode.new_tag_node`, :class:`TextNode.new_tag_node` or
:func:`new_tag_node`.
To instantiate new nodes use :func:`new_tag_node`.
Some syntactic sugar is baked in:
Expand Down Expand Up @@ -2009,7 +2027,7 @@ def clone(
return new_tag_node(
local_name=self.local_name,
attributes=self.attributes,
namespace=self.namespace,
namespace=self._namespace,
children=(
[n.clone(deep=True) for n in self.iterate_children()] if deep else ()
),
Expand Down Expand Up @@ -2203,7 +2221,7 @@ def _create_by_xpath(
else:
namespace = namespaces[prefix]

new_node = node.new_tag_node(
new_node = new_tag_node(
local_name=node_test.local_name,
attributes=None,
namespace=namespace,
Expand Down Expand Up @@ -2400,7 +2418,7 @@ def local_name(self) -> str:

@local_name.setter
def local_name(self, value: str):
self._etree_obj.tag = QName(self.namespace, value).text
self._etree_obj.tag = QName(self._namespace or None, value).text

@property
def location_path(self) -> str:
Expand Down Expand Up @@ -2445,12 +2463,24 @@ def namespace(self) -> Optional[str]:
:meta category: Node properties
"""
# weirdly QName fails in some cases when called with an etree._Element
return QName(self._etree_obj.tag).namespace # type: ignore
if result := QName(self._etree_obj.tag).namespace:
return result
else: # pragma: nocover
warnings.warn(
"With the delb 0.6 this will return an empty string to represent an "
"empty/null namespace. You can use the the intermediate "
"`TagNode._namespace` with that future behaviour.",
category=DeprecationWarning,
)
return result

@namespace.setter
def namespace(self, value: Optional[str]):
self._etree_obj.tag = QName(value, self.local_name).text
self._etree_obj.tag = QName(value or None, self.local_name).text

@property
def _namespace(self) -> str:
return QName(self._etree_obj.tag).namespace or ""

def new_tag_node(
self,
Expand All @@ -2459,6 +2489,11 @@ def new_tag_node(
namespace: Optional[str] = None,
children: Sequence[str | NodeBase | _TagDefinition] = (),
) -> TagNode:
warnings.warn(
"The node methods `new_tag_node` will be removed. Use :func:`new_tag_node`"
"instead.",
category=DeprecationWarning,
)
return self._new_tag_node_from(
self._etree_obj, local_name, attributes, namespace, children
)
Expand All @@ -2468,7 +2503,7 @@ def _new_tag_node_from_definition(self, definition: _TagDefinition) -> TagNode:
context=self._etree_obj,
local_name=definition.local_name,
attributes=definition.attributes,
namespace=self.namespace,
namespace=self._namespace,
children=definition.children,
)

Expand All @@ -2486,7 +2521,7 @@ def parse(
"""
warnings.warn(
"This method will be replaced by another interface in a future version.",
category=PendingDeprecationWarning,
category=DeprecationWarning,
)
parser_options = parser_options or ParserOptions()
parser = parser_options._make_parser()
Expand All @@ -2498,12 +2533,14 @@ def parse(
return result

@property
def prefix(self) -> Optional[str]:
def prefix(self) -> Optional[str]: # pragma: nocover
"""
The prefix that the node's namespace is currently mapped to.
:meta category: Node properties
"""
warnings.warn("This attribute will be removed.", category=DeprecationWarning)

target = QName(self._etree_obj).namespace

if target is None:
Expand Down Expand Up @@ -3305,28 +3342,28 @@ def __init__(
self._namespaces = (
Namespaces({}) if namespaces is None else Namespaces(namespaces)
)
self._prefixes: dict[str | None, str] = {}
self._prefixes: dict[str, str] = {}
self.writer = writer

def _collect_prefixes(self, root: TagNode): # noqa: C901 REMOVE eventually
for node in traverse_bf_ltr_ttb(root, is_tag_node):
assert isinstance(node, TagNode)
for namespace in {node.namespace} | {
a.namespace for a in node.attributes.values()
for namespace in {node._namespace} | {
a._namespace for a in node.attributes.values()
}:
if namespace in self._prefixes:
continue

prefix: str | None
if namespace is None:
if not namespace:
for other_namespace, prefix in self._prefixes.items():
if prefix == "":
# hence there's a default namespace in use, it needs to use
# a prefix though as an empty namespace can't be declared
assert other_namespace
self._new_namespace_declaration(other_namespace)
break
self._prefixes[None] = ""
self._prefixes[""] = ""
continue

# TODO unlike lxml, delb will probably not keep the individual
Expand Down Expand Up @@ -3364,7 +3401,7 @@ def _generate_attributes_data(self, node: TagNode) -> dict[str, str]:

for attribute in (node.attributes[a] for a in sorted(node.attributes)):
assert isinstance(attribute, Attribute)
data[self._prefixes[attribute.namespace] + attribute.local_name] = (
data[self._prefixes[attribute._namespace] + attribute.local_name] = (
f'"{attribute.value.translate(CCE_TABLE_FOR_ATTRIBUTES)}"'
)

Expand All @@ -3374,7 +3411,7 @@ def _handle_child_nodes(self, child_nodes: tuple[NodeBase, ...]):
for child_node in child_nodes:
self.serialize_node(child_node)

def _new_namespace_declaration(self, namespace: None | str):
def _new_namespace_declaration(self, namespace: str):
for i in range(2**16):
prefix = f"ns{i}:"
if prefix not in self._prefixes.values():
Expand Down Expand Up @@ -3423,7 +3460,7 @@ def _serialize_tag(
attributes_data: dict[str, str],
):
child_nodes = tuple(node.iterate_children())
prefixed_name = self._prefixes[node.namespace] + node.local_name
prefixed_name = self._prefixes[node._namespace] + node.local_name

self.writer(f"<{prefixed_name}")
if attributes_data:
Expand Down Expand Up @@ -3696,7 +3733,7 @@ def _required_space(self, node: NodeBase, up_to: int) -> None | int:

assert isinstance(node, TagNode)

name_length = len(node.local_name) + len(self._prefixes[node.namespace])
name_length = len(node.local_name) + len(self._prefixes[node._namespace])
if len(node) == 0:
used_space = 3 + name_length # <N/>
else:
Expand Down Expand Up @@ -3739,7 +3776,7 @@ def _required_space_for_attributes(self, node: TagNode, up_to: int) -> None | in
result += (
4 # preceding space and »="…"«
+ len(attribute.local_name)
+ len(self._prefixes[attribute.namespace])
+ len(self._prefixes[attribute._namespace])
+ len(attribute.value.translate(CCE_TABLE_FOR_ATTRIBUTES))
)
if result > up_to:
Expand Down
8 changes: 4 additions & 4 deletions _delb/xpath/ast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2018-'24 Frank Sachsenheim
# Copyright (C) 2018-'25 Frank Sachsenheim
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
Expand Down Expand Up @@ -79,7 +79,7 @@ def wrapper(self, node, **kwargs):

if prefix is not None and prefix not in namespaces:
raise XPathEvaluationError(
f"The namespace prefix `{prefix}` is unknown in the the evaluation "
f"The namespace prefix `{prefix}` is unknown in the evaluation "
"context."
)
return func(self, node, **kwargs)
Expand Down Expand Up @@ -404,7 +404,7 @@ def evaluate(self, node: NodeBase, namespaces) -> bool:
if self.prefix is None:
return True

return cast("TagNode", node).namespace == namespaces.get(self.prefix)
return cast("TagNode", node)._namespace == namespaces.get(self.prefix)


class NameMatchTest(NodeTestNode):
Expand All @@ -419,7 +419,7 @@ def evaluate(self, node: NodeBase, namespaces) -> bool:
if not _is_node_of_type(node, "TagNode"):
return False
node = cast("TagNode", node)
if (self.prefix or None in namespaces) and node.namespace != namespaces.get(
if (self.prefix or None in namespaces) and node._namespace != namespaces.get(
self.prefix
):
return False
Expand Down
4 changes: 4 additions & 0 deletions delb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,10 @@ def new_tag_node(
This method proxies to the :meth:`TagNode.new_tag_node` method of the
document's root node.
"""
warnings.warn(
"The this method will be removed. Use :func:`new_tag_node` instead.",
category=DeprecationWarning,
)
return self.root.new_tag_node(
local_name=local_name, attributes=attributes, namespace=namespace
)
Expand Down
Loading

0 comments on commit f46745f

Please sign in to comment.