Skip to content

Commit

Permalink
Remove threading lock for lazy resources
Browse files Browse the repository at this point in the history
  - It causes stale with PyPy in many tests (TODO)
  - Upgrade static typing tests with mypy==1.8.0
  - Fix undefined import with py38 and py39 (remove, not mandatory)
  • Loading branch information
brunato committed Jan 6, 2024
1 parent 5146f9f commit 345b823
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 138 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/test-xmlschema.yml
Expand Up @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions tox.ini
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions xmlschema/converters/default.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
240 changes: 111 additions & 129 deletions xmlschema/resources.py
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]]:
"""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 345b823

Please sign in to comment.