From cd969063086d6f11c47d390af337f90c49c68cc0 Mon Sep 17 00:00:00 2001 From: Rogdham Date: Sat, 18 Oct 2025 12:28:04 +0200 Subject: [PATCH] chore: drop support for Python 3.9 --- .github/workflows/build.yml | 2 - CHANGELOG.md | 1 + pyproject.toml | 5 +- src/bigxml/handle_mgr.py | 164 ++++++++---------- src/bigxml/handler_creator.py | 16 +- src/bigxml/handler_marker.py | 20 +-- src/bigxml/nodes.py | 4 +- src/bigxml/parser.py | 10 +- src/bigxml/stream.py | 8 +- src/bigxml/typing.py | 17 +- src/bigxml/utils.py | 8 +- tests/integration/test_maths.py | 5 +- tests/integration/test_ram_usage.py | 5 +- tests/integration/test_typing.py | 41 +++-- tests/integration/test_wikipedia_export.py | 5 +- tests/unit/test_handle_mgr.py | 11 +- tests/unit/test_handler_creator.py | 64 +++---- tests/unit/test_handler_marker.py | 3 +- tests/unit/test_nodes_element_get_text.py | 11 +- tests/unit/test_parser.py | 25 ++- tests/unit/test_stream.py | 8 +- tests/unit/test_utils_get_mandatory_params.py | 2 +- tox.ini | 2 +- 23 files changed, 197 insertions(+), 240 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e20575..178ba19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,13 +18,11 @@ jobs: strategy: matrix: python: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" - - "pypy-3.9" - "pypy-3.10" - "pypy-3.11" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f18b57..661ddb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ For the purpose of determining breaking changes: ### :house: Internal - Update license metadata as per [PEP 639](https://peps.python.org/pep-0639) +- End of Python 3.9 support - Add tests for CPython 3.14 and PyPy 3.11 - Use CPython 3.14 for misc. tests - Upgrade dev dependencies diff --git a/pyproject.toml b/pyproject.toml index d2abd91..e5a14bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -22,7 +21,7 @@ classifiers = [ "Topic :: Utilities", "Topic :: Text Processing :: Markup :: XML", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "defusedxml>=0.7.1", "typing-extensions>=4.6.0 ; python_version<'3.12'", @@ -139,7 +138,7 @@ testpaths = ["docs", "tests"] [tool.ruff] src = ["src"] -target-version = "py39" +target-version = "py310" [tool.ruff.lint] select = ["ALL"] diff --git a/src/bigxml/handle_mgr.py b/src/bigxml/handle_mgr.py index 81ddb36..48f1997 100644 --- a/src/bigxml/handle_mgr.py +++ b/src/bigxml/handle_mgr.py @@ -1,6 +1,6 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator import sys -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, overload +from typing import TYPE_CHECKING, Any, Optional, Union, overload from bigxml.handler_creator import create_handler from bigxml.typing import ( @@ -20,12 +20,12 @@ class HandleMgr: - _handle: Optional[ + _handle: ( Callable[ - [Callable[[Union["XMLElement", "XMLText"]], Iterator[Any]]], - Iterator[Any], + [Callable[[Union["XMLElement", "XMLText"]], Iterator[Any]]], Iterator[Any] ] - ] = None + | None + ) = None # iter_from @@ -37,67 +37,55 @@ def iter_from( @overload def iter_from( self, - *handlers: Union[ - str, - list[str], - tuple[str, ...], - ], + *handlers: str | list[str] | tuple[str, ...], ) -> Iterator["XMLElement"]: ... @overload def iter_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - ], + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]], ) -> Iterator[T]: ... @overload def iter_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - type[T], - ], + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | type[T], ) -> Iterator[T]: ... @overload def iter_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - str, - list[str], - tuple[str, ...], - ], + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | str + | list[str] + | tuple[str, ...], ) -> Iterator[Union["XMLElement", T]]: ... @overload def iter_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - type[T], - str, - list[str], - tuple[str, ...], - ], + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | type[T] + | str + | list[str] + | tuple[str, ...], ) -> Iterator[Union["XMLElement", T]]: ... @overload @@ -122,74 +110,62 @@ def return_from( @overload def return_from( self, - *handlers: Union[ - str, - list[str], - tuple[str, ...], - ], + *handlers: str | list[str] | tuple[str, ...], ) -> Optional["XMLElement"]: ... @overload def return_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - ], - ) -> Optional[T]: ... + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]], + ) -> T | None: ... @overload def return_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - type[T], - ], - ) -> Optional[T]: ... + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | type[T], + ) -> T | None: ... @overload def return_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - str, - list[str], - tuple[str, ...], - ], - ) -> Optional[Union["XMLElement", T]]: ... + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | str + | list[str] + | tuple[str, ...], + ) -> Union["XMLElement", T] | None: ... @overload def return_from( self, - *handlers: Union[ - Callable[[Union["XMLElement", "XMLText"]], Optional[Iterable[T]]], - ClassHandlerWithCustomWrapper0[T], - ClassHandlerWithCustomWrapper1[T], - type[ClassHandlerWithCustomWrapper0[T]], - type[ClassHandlerWithCustomWrapper1[T]], - str, - list[str], - tuple[str, ...], - type[T], - ], - ) -> Optional[Union["XMLElement", T]]: ... + *handlers: Callable[[Union["XMLElement", "XMLText"]], Iterable[T] | None] + | ClassHandlerWithCustomWrapper0[T] + | ClassHandlerWithCustomWrapper1[T] + | type[ClassHandlerWithCustomWrapper0[T]] + | type[ClassHandlerWithCustomWrapper1[T]] + | str + | list[str] + | tuple[str, ...] + | type[T], + ) -> Union["XMLElement", T] | None: ... @overload def return_from( self, *handlers: object, - ) -> Optional[object]: ... + ) -> object | None: ... - def return_from(self, *handlers: Any) -> Optional[Any]: + def return_from(self, *handlers: Any) -> Any | None: return last_item_or_none(self.iter_from(*handlers)) diff --git a/src/bigxml/handler_creator.py b/src/bigxml/handler_creator.py index 9e1bdc2..b16c678 100644 --- a/src/bigxml/handler_creator.py +++ b/src/bigxml/handler_creator.py @@ -1,7 +1,7 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator from dataclasses import is_dataclass from inspect import getmembers, isclass -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Union, cast import warnings from bigxml.marks import get_marks, has_marks @@ -27,7 +27,7 @@ def _assert_one_mandatory_param( def _assert_iterable_or_none( item: object, klass: type[Any], method_name: str -) -> Optional[Iterable[object]]: +) -> Iterable[object] | None: if item is None or isinstance(item, Iterable): return item raise TypeError( @@ -44,7 +44,7 @@ class _HandlerTree: def __init__(self, path: tuple[str, ...] = ()) -> None: self.path: tuple[str, ...] = path self.children: dict[str, _HandlerTree] = {} - self.handler: Optional[Callable[..., Iterable[object]]] = None + self.handler: Callable[..., Iterable[object]] | None = None def add_handler( self, @@ -111,15 +111,13 @@ def add_handler_callable( self.handler = handler @transform_to_iterator - def handle( - self, node: Union["XMLElement", "XMLText"] - ) -> Optional[Iterable[object]]: + def handle(self, node: Union["XMLElement", "XMLText"]) -> Iterable[object] | None: if self.handler: if isclass(self.handler): return self._handle_from_class(self.handler, node) return self.handler(node) - child: Optional[_HandlerTree] = None + child: _HandlerTree | None = None namespace = getattr(node, "namespace", None) if namespace is not None: child = self.children.get(f"{{{namespace}}}{node.name}") @@ -137,7 +135,7 @@ def handle( @staticmethod def _handle_from_class( klass: type[Any], node: Union["XMLElement", "XMLText"] - ) -> Optional[Iterable[object]]: + ) -> Iterable[object] | None: # instantiate class init_mandatory_params = get_mandatory_params(klass) try: diff --git a/src/bigxml/handler_marker.py b/src/bigxml/handler_marker.py index da37f41..2327c28 100644 --- a/src/bigxml/handler_marker.py +++ b/src/bigxml/handler_marker.py @@ -1,5 +1,5 @@ -from collections.abc import Iterable -from typing import Any, Callable, Generic, Optional, Union, cast, overload +from collections.abc import Callable, Iterable +from typing import Any, Generic, cast, overload from bigxml.marks import add_mark from bigxml.nodes import XMLElement, XMLText @@ -28,15 +28,15 @@ def __call__( @overload def __call__( self, - obj: Callable[[T_co], Optional[Iterable[T]]], - ) -> Callable[[Union[XMLElement, XMLText]], Optional[Iterable[T]]]: ... + obj: Callable[[T_co], Iterable[T] | None], + ) -> Callable[[XMLElement | XMLText], Iterable[T] | None]: ... # wrapper for methods @overload def __call__( self, - obj: Callable[[U, T_co], Optional[Iterable[T]]], - ) -> Callable[[U, Union[XMLElement, XMLText]], Optional[Iterable[T]]]: ... + obj: Callable[[U, T_co], Iterable[T] | None], + ) -> Callable[[U, XMLElement | XMLText], Iterable[T] | None]: ... def xml_handle_element(*args: str) -> ___xml_handle_xxx_wrapped[XMLElement]: @@ -69,15 +69,15 @@ def xml_handle_text(obj: K, /) -> K: ... # @xml_handle_text (for functions) @overload def xml_handle_text( - obj: Callable[[XMLText], Optional[Iterable[T]]], / -) -> Callable[[Union[XMLElement, XMLText]], Optional[Iterable[T]]]: ... + obj: Callable[[XMLText], Iterable[T] | None], / +) -> Callable[[XMLElement | XMLText], Iterable[T] | None]: ... # @xml_handle_text (for methods) @overload def xml_handle_text( - obj: Callable[[U, XMLText], Optional[Iterable[T]]], / -) -> Callable[[U, Union[XMLElement, XMLText]], Optional[Iterable[T]]]: ... + obj: Callable[[U, XMLText], Iterable[T] | None], / +) -> Callable[[U, XMLElement | XMLText], Iterable[T] | None]: ... # @xml_handle_text(...) (for functions & methods) diff --git a/src/bigxml/nodes.py b/src/bigxml/nodes.py index cfbe44c..2e17611 100644 --- a/src/bigxml/nodes.py +++ b/src/bigxml/nodes.py @@ -1,6 +1,6 @@ from collections.abc import Iterator, Mapping from dataclasses import dataclass -from typing import Optional, Union +from typing import Union import warnings from bigxml.handle_mgr import HandleMgr @@ -10,7 +10,7 @@ class XMLElementAttributes(Mapping[str, str]): def __init__(self, attributes: Mapping[str, str]) -> None: self._items: dict[ - str, tuple[Optional[int], str] + str, tuple[int | None, str] ] = {} # key -> (alternatives, value) self._len = 0 for key, value in attributes.items(): diff --git a/src/bigxml/parser.py b/src/bigxml/parser.py index bc6bbf8..a3af8da 100644 --- a/src/bigxml/parser.py +++ b/src/bigxml/parser.py @@ -1,5 +1,5 @@ -from collections.abc import Iterator -from typing import TYPE_CHECKING, Callable, Optional, Union +from collections.abc import Callable, Iterator +from typing import TYPE_CHECKING, Optional import warnings from defusedxml.ElementTree import iterparse @@ -17,7 +17,7 @@ def _parse( iterator: IterWithRollback[tuple[str, "Element"]], - handler: Callable[[Union[XMLElement, XMLText]], Iterator[T]], + handler: Callable[[XMLElement | XMLText], Iterator[T]], parents: tuple[XMLElement, ...], parent_elem: Optional["Element"], expected_iteration: int, @@ -26,7 +26,7 @@ def _parse( raise RuntimeError("Tried to access a node out of order") depth = 0 - last_child: Optional[Element] = None + last_child: Element | None = None def handle_text() -> Iterator[T]: if last_child is not None: @@ -39,7 +39,7 @@ def handle_text() -> Iterator[T]: node = XMLText(text=text, parents=parents) yield from handler(node) - def create_node(elem: "Element", iteration: int) -> Union[XMLElement, XMLText]: + def create_node(elem: "Element", iteration: int) -> XMLElement | XMLText: node = XMLElement( name=elem.tag, attributes=XMLElementAttributes(elem.attrib), parents=parents ) diff --git a/src/bigxml/stream.py b/src/bigxml/stream.py index 7584a47..387fde5 100644 --- a/src/bigxml/stream.py +++ b/src/bigxml/stream.py @@ -1,7 +1,7 @@ from collections.abc import Generator, Iterable from io import IOBase import sys -from typing import Any, Optional, cast +from typing import Any, cast from bigxml.typing import Streamable, SupportsRead from bigxml.utils import autostart_generator @@ -13,7 +13,7 @@ @autostart_generator -def _flatten_stream(stream: Streamable) -> Generator[Optional[memoryview], int, None]: +def _flatten_stream(stream: Streamable) -> Generator[memoryview | None, int, None]: yield None # buffer protocol (bytes, etc.) @@ -68,7 +68,7 @@ def _flatten_stream(stream: Streamable) -> Generator[Optional[memoryview], int, @autostart_generator def _convert_to_read( - data_stream: Generator[Optional[memoryview], int, None], + data_stream: Generator[memoryview | None, int, None], ) -> Generator[bytes, int, None]: size = yield b"" while True: @@ -90,7 +90,7 @@ def __init__(self, *streams: Streamable) -> None: super().__init__() self._read = _convert_to_read(_flatten_stream(streams)) - def read(self, size: Optional[int] = None) -> bytes: + def read(self, size: int | None = None) -> bytes: if not isinstance(size, int) or size <= 0: raise NotImplementedError("Read size must be strictly positive") return self._read.send(size) diff --git a/src/bigxml/typing.py b/src/bigxml/typing.py index 264abdb..5246752 100644 --- a/src/bigxml/typing.py +++ b/src/bigxml/typing.py @@ -1,11 +1,6 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator import sys -from typing import Any, Callable, Optional, Protocol, TypeVar, Union - -if sys.version_info < (3, 10): # pragma: no cover - from typing_extensions import ParamSpec -else: # pragma: no cover - from typing import ParamSpec +from typing import Any, ParamSpec, Protocol, TypeVar if sys.version_info < (3, 12): # pragma: no cover from typing_extensions import Buffer @@ -22,17 +17,17 @@ class SupportsRead(Protocol[T_co]): - def read(self, size: Optional[int] = None) -> T_co: ... # pragma: no cover + def read(self, size: int | None = None) -> T_co: ... # pragma: no cover -Streamable = Union[Buffer, SupportsRead[bytes], Iterable["Streamable"]] +Streamable = Buffer | SupportsRead[bytes] | Iterable["Streamable"] class ClassHandlerWithCustomWrapper0(Protocol[T_co]): - def xml_handler(self) -> Optional[Iterable[T_co]]: ... # pragma: no cover + def xml_handler(self) -> Iterable[T_co] | None: ... # pragma: no cover class ClassHandlerWithCustomWrapper1(Protocol[T_co]): def xml_handler( self, items: Iterator[Any] - ) -> Optional[Iterable[T_co]]: ... # pragma: no cover + ) -> Iterable[T_co] | None: ... # pragma: no cover diff --git a/src/bigxml/utils.py b/src/bigxml/utils.py index 0b1f3bc..565ed2c 100644 --- a/src/bigxml/utils.py +++ b/src/bigxml/utils.py @@ -1,9 +1,9 @@ from collections import deque -from collections.abc import Generator, Iterable, Iterator +from collections.abc import Callable, Generator, Iterable, Iterator from functools import wraps from inspect import Parameter, signature import re -from typing import Callable, Optional, cast +from typing import cast from bigxml.typing import P, T, U @@ -44,7 +44,7 @@ def extract_namespace_name(name: str) -> tuple[str, str]: return ("", name) -def last_item_or_none(iterable: Iterable[T]) -> Optional[T]: +def last_item_or_none(iterable: Iterable[T]) -> T | None: try: return deque(iterable, maxlen=1)[0] except IndexError: @@ -62,7 +62,7 @@ def consume(iterable: Iterable[object]) -> bool: def transform_to_iterator( - fct: Callable[P, Optional[Iterable[T]]], + fct: Callable[P, Iterable[T] | None], ) -> Callable[P, Iterator[T]]: @wraps(fct) def wrapped(*args: P.args, **kwargs: P.kwargs) -> Iterator[T]: diff --git a/tests/integration/test_maths.py b/tests/integration/test_maths.py index 5a641cb..3b80e60 100644 --- a/tests/integration/test_maths.py +++ b/tests/integration/test_maths.py @@ -1,7 +1,6 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator from functools import reduce import operator -from typing import Callable, Optional, Union from bigxml import Parser, XMLElement, XMLText, xml_handle_element, xml_handle_text @@ -29,7 +28,7 @@ def test_maths_eval_list() -> None: - handlers: list[Callable[[Union[XMLElement, XMLText]], Optional[Iterable[int]]]] = [] + handlers: list[Callable[[XMLElement | XMLText], Iterable[int] | None]] = [] @xml_handle_element("expr") def handle_expr(node: XMLElement) -> Iterator[int]: diff --git a/tests/integration/test_ram_usage.py b/tests/integration/test_ram_usage.py index 8239c72..ca50bb7 100644 --- a/tests/integration/test_ram_usage.py +++ b/tests/integration/test_ram_usage.py @@ -1,5 +1,4 @@ -from collections.abc import Iterator -from typing import Callable, Optional +from collections.abc import Callable, Iterator import pytest @@ -21,7 +20,7 @@ def ram_usage() -> Iterator[Callable[[], float]]: def big_stream(ram_used: Callable[[], float]) -> Iterator[bytes]: - ram_limit: Optional[float] = None + ram_limit: float | None = None yield b"\n" diff --git a/tests/integration/test_typing.py b/tests/integration/test_typing.py index 76ac227..fee4752 100644 --- a/tests/integration/test_typing.py +++ b/tests/integration/test_typing.py @@ -1,7 +1,6 @@ from collections.abc import Iterable, Iterator from dataclasses import dataclass import sys -from typing import Optional, Union from bigxml import ( HandlerTypeHelper, @@ -47,7 +46,7 @@ def test_no_handlers() -> None: # function -def catchall(node: Union[XMLElement, XMLText]) -> Iterator[int]: +def catchall(node: XMLElement | XMLText) -> Iterator[int]: yield len(node.text) @@ -67,7 +66,7 @@ def test_catchall() -> None: assert list(iterator) == [11] value = Parser(XML).return_from(catchall) - assert_type(value, Optional[int]) + assert_type(value, int | None) assert value == 11 @@ -77,7 +76,7 @@ def test_element_handler() -> None: assert list(iterator) == ["one", "two", "three"] value = Parser(XML).return_from(element_handler) - assert_type(value, Optional[str]) + assert_type(value, str | None) assert value == "three" @@ -87,7 +86,7 @@ def test_text_handler() -> None: assert list(iterator) == [5, 5, 5, 1] value = Parser(XML).return_from(text_handler) - assert_type(value, Optional[int]) + assert_type(value, int | None) assert value == 1 @@ -143,7 +142,7 @@ def test_class_nothing() -> None: assert isinstance(items[0], Nothing) value = Parser(XML).return_from(Nothing) - assert_type(value, Optional[Nothing]) + assert_type(value, Nothing | None) assert isinstance(value, Nothing) @@ -157,7 +156,7 @@ def test_class_init() -> None: assert items[0].text == "onetwothree" value = Parser(XML).return_from(HoldNode) - assert_type(value, Optional[HoldNode]) + assert_type(value, HoldNode | None) assert isinstance(value, HoldNode) assert value.name == "root" assert value.text == "onetwothree" @@ -173,7 +172,7 @@ def test_class_item_init() -> None: assert [item.text for item in items] == ["one", "two", "three"] value = Parser(XML).return_from(ItemHoldNode) - assert_type(value, Optional[ItemHoldNode]) + assert_type(value, ItemHoldNode | None) assert isinstance(value, ItemHoldNode) assert value.name == "item" assert value.text == "three" @@ -186,7 +185,7 @@ def test_class_with_subhandler() -> None: assert items == [WithSubHandler("")] value = Parser(XML).return_from(WithSubHandler) - assert_type(value, Optional[WithSubHandler]) + assert_type(value, WithSubHandler | None) assert value == WithSubHandler("") @@ -202,7 +201,7 @@ def test_class_with_custom_handler() -> None: ] value = Parser(XML).return_from(WithCustomHandler) - assert_type(value, Optional[str]) + assert_type(value, str | None) assert value == "total -> 3" @@ -223,7 +222,7 @@ def test_instance_subhandler() -> None: assert items == [3, 3, 5] value = Parser(XML).return_from(SubHandler()) - assert_type(value, Optional[object]) + assert_type(value, object) assert value == 5 @@ -236,7 +235,7 @@ def test_syntactic_sugar_tuple() -> None: assert [node.text for node in iterator] == ["one", "two", "three"] value = Parser(XML).return_from(("root", "item")) - assert_type(value, Optional[XMLElement]) + assert_type(value, XMLElement | None) assert value # cannot get node.text as it is consumed at that point assert isinstance(value, XMLElement) @@ -249,7 +248,7 @@ def test_syntactic_sugar_list() -> None: assert [node.text for node in iterator] == ["one", "two", "three"] value = Parser(XML).return_from(["root", "item"]) - assert_type(value, Optional[XMLElement]) + assert_type(value, XMLElement | None) assert value # cannot get node.text as it is consumed at that point assert isinstance(value, XMLElement) @@ -262,7 +261,7 @@ def test_syntactic_sugar_str() -> None: assert [node.text for node in iterator] == ["onetwothree"] value = Parser(XML).return_from("root") - assert_type(value, Optional[XMLElement]) + assert_type(value, XMLElement | None) assert value # cannot get node.text as it is consumed at that point assert isinstance(value, XMLElement) @@ -274,31 +273,31 @@ def test_syntactic_sugar_str() -> None: def test_mixed_iterator_types() -> None: iterator = Parser(XML).iter_from( - HandlerTypeHelper[Union[str, int]], # little help + HandlerTypeHelper[str | int], # little help element_handler, text_handler, ) - assert_type(iterator, Iterator[Union[str, int]]) + assert_type(iterator, Iterator[str | int]) assert list(iterator) == [5, "one", 5, "two", 5, "three", 1] value = Parser(XML).return_from( - HandlerTypeHelper[Union[str, int]], # little help + HandlerTypeHelper[str | int], # little help element_handler, text_handler, ) - assert_type(value, Optional[Union[str, int]]) + assert_type(value, str | int | None) assert value == 1 def test_mixed_syntactic_sugar() -> None: iterator = Parser(XML).iter_from(("root", "item"), text_handler) - assert_type(iterator, Iterator[Union[XMLElement, int]]) + assert_type(iterator, Iterator[XMLElement | int]) assert [ item.name if isinstance(item, XMLElement) else item for item in iterator ] == [5, "item", 5, "item", 5, "item", 1] value = Parser(XML).return_from(("root", "item"), text_handler) - assert_type(value, Optional[Union[XMLElement, int]]) + assert_type(value, XMLElement | int | None) assert value == 1 @@ -330,5 +329,5 @@ def text_handler2(node: XMLText) -> Iterator[WithSubHandler2]: ] value = Parser(XML).return_from(text_handler2, WithSubHandler2) - assert_type(value, Optional[WithSubHandler2]) + assert_type(value, WithSubHandler2 | None) assert value == WithSubHandler2("~\n~") diff --git a/tests/integration/test_wikipedia_export.py b/tests/integration/test_wikipedia_export.py index 53c45e8..6b65cfa 100644 --- a/tests/integration/test_wikipedia_export.py +++ b/tests/integration/test_wikipedia_export.py @@ -1,7 +1,6 @@ from datetime import datetime from lzma import LZMAFile from pathlib import Path -from typing import Optional from bigxml import Parser, XMLText, xml_handle_element, xml_handle_text @@ -10,8 +9,8 @@ def test_wikipedia_export() -> None: @xml_handle_element("mediawiki", "page", "revision") class Revision: def __init__(self) -> None: - self.author: Optional[str] = None - self.date: Optional[datetime] = None + self.author: str | None = None + self.date: datetime | None = None @xml_handle_text("contributor", "username") def handle_author(self, node: XMLText) -> None: diff --git a/tests/unit/test_handle_mgr.py b/tests/unit/test_handle_mgr.py index 655cc81..1d7b204 100644 --- a/tests/unit/test_handle_mgr.py +++ b/tests/unit/test_handle_mgr.py @@ -1,5 +1,4 @@ -from collections.abc import Iterator -from typing import Callable, Union +from collections.abc import Callable, Iterator from unittest.mock import Mock import pytest @@ -8,21 +7,21 @@ from bigxml.nodes import XMLElement, XMLText -def handler_a(_node: Union[XMLElement, XMLText]) -> Iterator[int]: +def handler_a(_node: XMLElement | XMLText) -> Iterator[int]: yield 13 yield 37 -def handler_b(_node: Union[XMLElement, XMLText]) -> Iterator[int]: +def handler_b(_node: XMLElement | XMLText) -> Iterator[int]: yield 42 -def handler_c(_node: Union[XMLElement, XMLText]) -> Iterator[object]: +def handler_c(_node: XMLElement | XMLText) -> Iterator[object]: yield from () def handle( - handler: Callable[[Union[XMLElement, XMLText]], Iterator[int]], + handler: Callable[[XMLElement | XMLText], Iterator[int]], ) -> Iterator[int]: node = Mock() for item in handler(node): diff --git a/tests/unit/test_handler_creator.py b/tests/unit/test_handler_creator.py index 478e45f..cc542a4 100644 --- a/tests/unit/test_handler_creator.py +++ b/tests/unit/test_handler_creator.py @@ -1,8 +1,8 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Callable, Iterable, Iterator from contextlib import suppress from dataclasses import dataclass from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast +from typing import TYPE_CHECKING, Any, cast from unittest.mock import Mock import pytest @@ -18,8 +18,8 @@ def create_nodes( - *path: str, parent: Optional[Union[XMLElement, XMLText]] = None -) -> list[Union[XMLElement, XMLText]]: + *path: str, parent: XMLElement | XMLText | None = None +) -> list[XMLElement | XMLText]: # create nodes nodes = [parent] if parent is not None else [] for node_name in path: @@ -27,7 +27,7 @@ def create_nodes( # plus that case is kind of tested in tests below parents = tuple(cast("list[XMLElement]", nodes)) if node_name == ":text:": - node: Union[XMLElement, XMLText] = XMLText(text="text", parents=parents) + node: XMLElement | XMLText = XMLText(text="text", parents=parents) else: node = XMLElement( name=node_name, @@ -43,7 +43,7 @@ def create_nodes( # get existing children try: - children: list[Union[XMLElement, XMLText]] = list( + children: list[XMLElement | XMLText] = list( node_parent.iter_from(lambda n: (n,)) ) except RuntimeError: @@ -55,8 +55,8 @@ def create_nodes( # create handle def handle( - handler: Callable[[Union[XMLElement, XMLText]], Iterator[object]], - children: list[Union[XMLElement, XMLText]], + handler: Callable[[XMLElement | XMLText], Iterator[object]], + children: list[XMLElement | XMLText], ) -> Iterable[object]: for child in children: yield from handler(child) @@ -67,7 +67,7 @@ def handle( def cases( - *args: tuple[tuple[str, ...], Optional[str], Optional[str]], + *args: tuple[tuple[str, ...], str | None, str | None], ) -> pytest.MarkDecorator: tests: list[ParameterSet] = [] for node_path, expected_text, expected_node_name in args: @@ -79,9 +79,9 @@ def cases( expected_node = nodes[node_path.index(expected_node_name)] def test_create_handler( - root: Union[XMLElement, XMLText], - expected_text: Optional[str], - expected_node: Union[XMLElement, XMLText, None], + root: XMLElement | XMLText, + expected_text: str | None, + expected_node: XMLElement | XMLText | None, *handles: object, ) -> None: handler = create_handler(*handles) @@ -130,8 +130,8 @@ def test_no_handlers() -> None: ) def test_one_catchall(test_create_handler: TEST_CREATE_HANDLER_TYPE) -> None: def catchall( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("catchall", node) test_create_handler(catchall) @@ -295,8 +295,8 @@ def handle5(node: XMLText) -> Iterator[tuple[str, XMLText]]: @staticmethod def xml_handler( - generator: Iterator[tuple[str, Union[XMLElement, XMLText]]], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + generator: Iterator[tuple[str, XMLElement | XMLText]], + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield from generator handler = Handler() if instantiate_class else Handler @@ -520,7 +520,7 @@ def test_class_init_text_node() -> None: class Handler: def __init__(self, node: XMLElement) -> None: self.root = node - self.node: Optional[XMLText] = None + self.node: XMLText | None = None @xml_handle_text def handle0(self, node: XMLText) -> None: @@ -603,7 +603,7 @@ def handle0(self, node: XMLElement) -> None: def handle1(self, node: XMLElement) -> None: self.nodes.append(("y", node)) - def xml_handler(self) -> Iterator[tuple[str, Optional[XMLElement]]]: + def xml_handler(self) -> Iterator[tuple[str, XMLElement | None]]: yield ("start", None) for txt, node in self.nodes: yield (f"_{txt}", node) @@ -677,7 +677,7 @@ def handle1(self, node: XMLElement) -> Iterable[tuple[str, XMLElement]]: def xml_handler( self, generator: Iterable[tuple[str, XMLElement]] - ) -> Iterable[tuple[str, Optional[XMLElement]]]: + ) -> Iterable[tuple[str, XMLElement | None]]: yield ("start", None) for txt, node in self.nodes: # before consuming the generator, self.nodes is empty @@ -719,7 +719,7 @@ def handle1(node: XMLElement) -> Iterable[tuple[str, XMLElement]]: @staticmethod def xml_handler( generator: Iterable[tuple[str, XMLElement]], - ) -> Iterable[tuple[str, Optional[XMLElement]]]: + ) -> Iterable[tuple[str, XMLElement | None]]: yield ("start", None) for txt, node in generator: yield (f"h{txt}", node) @@ -806,13 +806,13 @@ def handle0(self, node: XMLElement) -> None: def test_catchall_handler_not_alone() -> None: @xml_handle_element("a") def handle( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("0", node) def catchall( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("1", node) # ok alone @@ -830,13 +830,13 @@ def catchall( def test_several_catchall_handlers() -> None: def catchall0( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("0", node) def catchall1( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("1", node) with pytest.raises(TypeError) as exc_info: @@ -847,14 +847,14 @@ def catchall1( def test_concurrent_handlers() -> None: @xml_handle_element("a", "b", "c") def handle0( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("0", node) @xml_handle_element("a", "b", "c") def handle1( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield ("0", node) with pytest.raises(TypeError) as exc_info: diff --git a/tests/unit/test_handler_marker.py b/tests/unit/test_handler_marker.py index d4f340d..e5e11d4 100644 --- a/tests/unit/test_handler_marker.py +++ b/tests/unit/test_handler_marker.py @@ -1,5 +1,4 @@ from collections.abc import Iterator -from typing import Union import pytest @@ -105,7 +104,7 @@ def test_mixed_markers() -> None: @xml_handle_element("abc", "def") @xml_handle_text("ghi") @xml_handle_element("klm", "opq", "rst") - def fct(node: Union[XMLElement, XMLText]) -> Iterator[str]: + def fct(node: XMLElement | XMLText) -> Iterator[str]: yield f"<{node.text}>" assert get_marks(fct) == ( diff --git a/tests/unit/test_nodes_element_get_text.py b/tests/unit/test_nodes_element_get_text.py index 46ff8c7..376839d 100644 --- a/tests/unit/test_nodes_element_get_text.py +++ b/tests/unit/test_nodes_element_get_text.py @@ -1,5 +1,4 @@ -from collections.abc import Iterator -from typing import Callable, Union +from collections.abc import Callable, Iterator from unittest.mock import Mock import pytest @@ -11,15 +10,13 @@ def create_text(text: str) -> XMLText: return XMLText(text, ()) # don't care about parents -def create_element(*children: Union[XMLElement, XMLText]) -> XMLElement: +def create_element(*children: XMLElement | XMLText) -> XMLElement: node = XMLElement("foo", Mock(), ()) # don't care about parents handle = Mock() def side_effect( - handler: Callable[ - [Union[XMLElement, XMLText]], Iterator[Union[XMLElement, XMLText]] - ], - ) -> Iterator[Union[XMLElement, XMLText]]: + handler: Callable[[XMLElement | XMLText], Iterator[XMLElement | XMLText]], + ) -> Iterator[XMLElement | XMLText]: for child in children: yield from handler(child) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index e62aebf..ef0f4ee 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -1,6 +1,5 @@ -from collections.abc import Iterator +from collections.abc import Callable, Iterator from itertools import count -from typing import Callable, Optional, Union import pytest @@ -9,8 +8,8 @@ from bigxml.parser import Parser HANDLER_TYPE = Callable[ - [Union[XMLElement, XMLText]], - Iterator[tuple[str, Union[XMLElement, XMLText]]], + [XMLElement | XMLText], + Iterator[tuple[str, XMLElement | XMLText]], ] @@ -19,8 +18,8 @@ def handler() -> HANDLER_TYPE: return_values = count() def handler_fct( - node: Union[XMLElement, XMLText], - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + node: XMLElement | XMLText, + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield (f"handler-yield-{next(return_values)}", node) return handler_fct @@ -29,7 +28,7 @@ def handler_fct( def elem( name: str, *, - attributes: Optional[dict[str, str]] = None, + attributes: dict[str, str] | None = None, parents: tuple["XMLElement", ...] = (), namespace: str = "", ) -> XMLElement: @@ -66,8 +65,8 @@ def test_root_level( xml: bytes, node: XMLElement, handler: Callable[ - [Union[XMLElement, XMLText]], - Iterator[tuple[str, Union[XMLElement, XMLText]]], + [XMLElement | XMLText], + Iterator[tuple[str, XMLElement | XMLText]], ], ) -> None: parser = Parser(xml) @@ -133,13 +132,13 @@ def test_root_level( ) def test_content( xml_content: bytes, - nodes: list[Union[XMLElement, XMLText]], + nodes: list[XMLElement | XMLText], handler: HANDLER_TYPE, ) -> None: @xml_handle_element("root") def root_handler( node: XMLElement, - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield from node.iter_from(handler) parser = Parser(b"", xml_content, b"") @@ -178,7 +177,7 @@ def test_many_small_streams( @xml_handle_element("root") def root_handler( node: XMLElement, - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield from node.iter_from(handler) parser = Parser(*xml_parts) @@ -195,7 +194,7 @@ def test_insecurely_allow_entities( @xml_handle_element("root") def root_handler( node: XMLElement, - ) -> Iterator[tuple[str, Union[XMLElement, XMLText]]]: + ) -> Iterator[tuple[str, XMLElement | XMLText]]: yield from node.iter_from(handler) with pytest.warns( diff --git a/tests/unit/test_stream.py b/tests/unit/test_stream.py index cf2a34e..bcc584b 100644 --- a/tests/unit/test_stream.py +++ b/tests/unit/test_stream.py @@ -5,7 +5,7 @@ from mmap import mmap from string import ascii_lowercase import sys -from typing import Optional, cast +from typing import cast import pytest @@ -74,7 +74,7 @@ def abcdef_str_generator() -> Iterator[str]: class IntIO(IOBase): @staticmethod - def read(size: Optional[int]) -> int: + def read(size: int | None) -> int: assert isinstance(size, int) assert size >= 0 return 42 @@ -175,7 +175,7 @@ def test_stream_part_above_read_size() -> None: class InfiniteIO(IOBase): @staticmethod - def read(size: Optional[int]) -> bytes: + def read(size: int | None) -> bytes: assert isinstance(size, int) assert 0 <= size < 16 return (b"%x" % size) * size @@ -220,7 +220,7 @@ def test_pass_read_size(streams: tuple[Streamable, ...]) -> None: @pytest.mark.parametrize("size", [None, -1, 0]) -def test_invalid_read_sizes(size: Optional[int]) -> None: +def test_invalid_read_sizes(size: int | None) -> None: stream = StreamChain(b"Hello, world!") with pytest.raises(NotImplementedError): stream.read(size) diff --git a/tests/unit/test_utils_get_mandatory_params.py b/tests/unit/test_utils_get_mandatory_params.py index 0627d5e..0642247 100644 --- a/tests/unit/test_utils_get_mandatory_params.py +++ b/tests/unit/test_utils_get_mandatory_params.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable import pytest diff --git a/tox.ini b/tox.ini index 96cc806..11b7c77 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ [tox] envlist = - py, py39, py310, py311, py312, py313, py314, pypy3 + py, py310, py311, py312, py313, py314, pypy3 build, docs, lint, type [testenv]