diff --git a/.github/workflows/test-xmlschema.yml b/.github/workflows/test-xmlschema.yml index bd5cec0f..b7c3c8e6 100644 --- a/.github/workflows/test-xmlschema.yml +++ b/.github/workflows/test-xmlschema.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12.0-rc.2, pypy-3.9] + python-version: [3.8, 3.9, "3.10", 3.11, 3.12.0, pypy-3.10] exclude: - os: macos-latest python-version: 3.8 @@ -40,12 +40,11 @@ jobs: pip install lxml jinja2 pip install . python -m unittest - - name: Lint with flake8 if Python version != 3.12rc2 - if: ${{ matrix.python-version != '3.12.0-rc.2' }} + - name: Lint with flake8 run: | pip install flake8 flake8 xmlschema --max-line-length=100 --statistics - name: Lint with mypy run: | - pip install mypy==1.7.1 elementpath==4.1.5 lxml-stubs + pip install mypy==1.8.0 elementpath==4.1.5 lxml-stubs mypy --show-error-codes --strict xmlschema diff --git a/tox.ini b/tox.ini index c8e2b05e..591bb422 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ commands = [testenv:mypy-py{38,39,310,311,312,py3}] deps = - mypy==1.7.1 + mypy==1.8.0 elementpath==4.1.5 lxml-stubs jinja2 @@ -64,7 +64,7 @@ deps = elementpath>=4.1.5, <5.0.0 lxml jinja2 - mypy==1.7.1 + mypy==1.8.0 lxml-stubs commands = pytest tests -ra diff --git a/xmlschema/converters/default.py b/xmlschema/converters/default.py index dd5c719e..16567827 100644 --- a/xmlschema/converters/default.py +++ b/xmlschema/converters/default.py @@ -10,7 +10,7 @@ from collections import namedtuple from collections.abc import MutableMapping, MutableSequence from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Iterable, \ - List, Optional, ParamSpec, Tuple, Type, TypeVar, Union + List, Optional, Tuple, Type, TypeVar, Union from xml.etree.ElementTree import Element from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError @@ -37,11 +37,10 @@ declarations. """ -P = ParamSpec('P') T = TypeVar('T') -def stackable(method: Callable[P, T]) -> Callable[P, T]: +def stackable(method: Callable[..., T]) -> Callable[..., T]: """Mark if a converter object method supports 'stacked' xmlns processing mode.""" method.stackable = True # type: ignore[attr-defined] return method diff --git a/xmlschema/resources.py b/xmlschema/resources.py index eacd3bfa..d57e0854 100644 --- a/xmlschema/resources.py +++ b/xmlschema/resources.py @@ -9,7 +9,6 @@ # import sys import os.path -import threading from collections import deque from io import StringIO, BytesIO from itertools import zip_longest @@ -198,7 +197,6 @@ class XMLResource: _base_url: Optional[str] = None _parent_map: Optional[ParentMapType] = None _lazy: Union[bool, int] = False - _lazy_lock: Optional[threading.Lock] = None def __init__(self, source: XMLSourceType, base_url: Union[None, str, Path, bytes] = None, @@ -259,15 +257,6 @@ def __init__(self, source: XMLSourceType, self.parse(source, lazy) - def __getstate__(self) -> Dict[str, Any]: - state = self.__dict__.copy() - state.pop('_lazy_lock', None) - return state - - def __setstate__(self, state: Dict[str, Any]) -> None: - self.__dict__.update(state) - self._lazy_lock = threading.Lock() if self._lazy else None - def __repr__(self) -> str: return '%s(root=%r)' % (self.__class__.__name__, self._root) @@ -725,7 +714,6 @@ def parse(self, source: XMLSourceType, lazy: Union[bool, int] = False) -> None: if ns_declarations: self._xmlns[elem] = ns_declarations - self._lazy_lock = threading.Lock() if self._lazy else None self._parent_map = None self._source = source @@ -939,46 +927,44 @@ def iter(self, tag: Optional[str] = None) -> Iterator[ElementType]: yield from self._root.iter(tag) return - assert self._lazy_lock is not None - with self._lazy_lock: - resource = self.open() - tag = '*' if tag is None else tag.strip() - lazy_depth = int(self._lazy) - subtree_elements: Deque[ElementType] = deque() - ancestors = [] - level = 0 - - try: - for event, node in self._lazy_iterparse(resource): - if event == "start": - if level < lazy_depth: - if level: - ancestors.append(node) - if tag == '*' or node.tag == tag: - yield node # an incomplete element - level += 1 - else: - level -= 1 - if level < lazy_depth: - if level: - ancestors.pop() - continue # pragma: no cover - elif level > lazy_depth: - if tag == '*' or node.tag == tag: - subtree_elements.appendleft(node) - continue # pragma: no cover + resource = self.open() + tag = '*' if tag is None else tag.strip() + lazy_depth = int(self._lazy) + subtree_elements: Deque[ElementType] = deque() + ancestors = [] + level = 0 + try: + for event, node in self._lazy_iterparse(resource): + if event == "start": + if level < lazy_depth: + if level: + ancestors.append(node) if tag == '*' or node.tag == tag: - yield node # a full element + yield node # an incomplete element + level += 1 + else: + level -= 1 + if level < lazy_depth: + if level: + ancestors.pop() + continue # pragma: no cover + elif level > lazy_depth: + if tag == '*' or node.tag == tag: + subtree_elements.appendleft(node) + continue # pragma: no cover - yield from subtree_elements - subtree_elements.clear() + if tag == '*' or node.tag == tag: + yield node # a full element - self._lazy_clear(node, ancestors) - finally: - # Close the resource only if it was originally opened by XMLResource - if resource is not self._source: - resource.close() + yield from subtree_elements + subtree_elements.clear() + + self._lazy_clear(node, ancestors) + finally: + # Close the resource only if it was originally opened by XMLResource + if resource is not self._source: + resource.close() def iter_location_hints(self, tag: Optional[str] = None) -> Iterator[Tuple[str, str]]: """ @@ -1018,47 +1004,45 @@ def iter_depth(self, mode: int = 1, ancestors: Optional[List[ElementType]] = Non yield self._root return - assert self._lazy_lock is not None - with self._lazy_lock: - resource = self.open() - level = 0 - lazy_depth = int(self._lazy) + resource = self.open() + level = 0 + lazy_depth = int(self._lazy) - # boolean flags - incomplete_root = mode == 5 - pruned_root = mode > 2 - depth_level_elements = mode != 3 - thin_lazy = mode <= 2 + # boolean flags + incomplete_root = mode == 5 + pruned_root = mode > 2 + depth_level_elements = mode != 3 + thin_lazy = mode <= 2 - try: - for event, elem in self._lazy_iterparse(resource): - if event == "start": - if not level: - if incomplete_root: - yield elem - if ancestors is not None and level < lazy_depth: - ancestors.append(elem) - level += 1 - else: - level -= 1 - if not level: - if pruned_root: - yield elem - continue - elif level != lazy_depth: - if ancestors is not None and level < lazy_depth: - ancestors.pop() - continue # pragma: no cover - elif depth_level_elements: + try: + for event, elem in self._lazy_iterparse(resource): + if event == "start": + if not level: + if incomplete_root: + yield elem + if ancestors is not None and level < lazy_depth: + ancestors.append(elem) + level += 1 + else: + level -= 1 + if not level: + if pruned_root: yield elem + continue + elif level != lazy_depth: + if ancestors is not None and level < lazy_depth: + ancestors.pop() + continue # pragma: no cover + elif depth_level_elements: + yield elem - if thin_lazy: - self._lazy_clear(elem, ancestors) - else: - self._lazy_clear(elem) - finally: - if self._source is not resource: - resource.close() + if thin_lazy: + self._lazy_clear(elem, ancestors) + else: + self._lazy_clear(elem) + finally: + if self._source is not resource: + resource.close() def _select_elements(self, token: XPathToken, node: ResourceNodeType, @@ -1103,53 +1087,51 @@ def iterfind(self, path: str, yield from self._select_elements(token, self.xpath_root, ancestors) return - assert self._lazy_lock is not None - with self._lazy_lock: - resource = self.open() - lazy_depth = int(self._lazy) - level = 0 - - path = path.replace(' ', '').replace('./', '') - select_all = '*' in path and set(path).issubset({'*', '/'}) - if path == '.': - path_depth = 0 - elif path.startswith('/'): - path_depth = path.count('/') - 1 - else: - path_depth = path.count('/') + 1 + resource = self.open() + lazy_depth = int(self._lazy) + level = 0 - if not path_depth: - raise XMLResourceError(f"cannot use path {path!r} on a lazy resource") - elif path_depth < lazy_depth: - raise XMLResourceError(f"cannot use path {path!r} on a lazy resource " - f"with lazy_depth=={lazy_depth}") + path = path.replace(' ', '').replace('./', '') + select_all = '*' in path and set(path).issubset({'*', '/'}) + if path == '.': + path_depth = 0 + elif path.startswith('/'): + path_depth = path.count('/') - 1 + else: + path_depth = path.count('/') + 1 - if ancestors is not None: - ancestors.clear() - elif self._thin_lazy: - ancestors = [] + if not path_depth: + raise XMLResourceError(f"cannot use path {path!r} on a lazy resource") + elif path_depth < lazy_depth: + raise XMLResourceError(f"cannot use path {path!r} on a lazy resource " + f"with lazy_depth=={lazy_depth}") - try: - for event, node in self._lazy_iterparse(resource): - if event == "start": - if ancestors is not None and level < path_depth: - ancestors.append(node) - level += 1 - else: - level -= 1 - if level < path_depth: - if ancestors is not None: - ancestors.pop() - continue - elif level == path_depth: - if select_all or \ - node in self._select_elements(token, self.xpath_root): - yield node - if level == lazy_depth: - self._lazy_clear(node, ancestors) - finally: - if self._source is not resource: - resource.close() + if ancestors is not None: + ancestors.clear() + elif self._thin_lazy: + ancestors = [] + + try: + for event, node in self._lazy_iterparse(resource): + if event == "start": + if ancestors is not None and level < path_depth: + ancestors.append(node) + level += 1 + else: + level -= 1 + if level < path_depth: + if ancestors is not None: + ancestors.pop() + continue + elif level == path_depth: + if select_all or \ + node in self._select_elements(token, self.xpath_root): + yield node + if level == lazy_depth: + self._lazy_clear(node, ancestors) + finally: + if self._source is not resource: + resource.close() def find(self, path: str, namespaces: Optional[NamespacesType] = None,