Skip to content

Commit

Permalink
Add support for threads
Browse files Browse the repository at this point in the history
  - Added thread locks for meta-schema classes and XPath selectors
  - Fix for issue #147
  • Loading branch information
brunato committed Dec 17, 2019
1 parent 10112c4 commit a6b9781
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 42 deletions.
1 change: 1 addition & 0 deletions xmlschema/validators/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class XsdAssert(XsdComponent, ElementPathMixin):
def __init__(self, elem, schema, parent, base_type):
self.base_type = base_type
super(XsdAssert, self).__init__(elem, schema, parent)
ElementPathMixin.__init__(self)

def __repr__(self):
return '%s(test=%r)' % (self.__class__.__name__, self.path)
Expand Down
1 change: 1 addition & 0 deletions xmlschema/validators/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class XsdElement(XsdComponent, ValidationMixin, ParticleMixin, ElementPathMixin)

def __init__(self, elem, schema, parent):
super(XsdElement, self).__init__(elem, schema, parent)
ElementPathMixin.__init__(self)
if self.type is None:
raise XMLSchemaAttributeError("undefined 'type' attribute for %r." % self)
if self.qualified is None:
Expand Down
2 changes: 1 addition & 1 deletion xmlschema/validators/globals_.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def clear(self, remove_schemas=False, only_unbuilt=False):
:param only_unbuilt: removes only not built objects/schemas.
"""
if only_unbuilt:
not_built_schemas = {schema for schema in self.iter_schemas() if not schema.built}
not_built_schemas = {s for s in self.iter_schemas() if not s.built}
if not not_built_schemas:
return

Expand Down
44 changes: 24 additions & 20 deletions xmlschema/validators/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from collections import namedtuple, Counter
from abc import ABCMeta
import logging
import threading
import warnings
import re

Expand Down Expand Up @@ -90,6 +91,7 @@ def get_attribute(attr, *args):
# Defining a subclass without a meta-schema (eg. XMLSchemaBase)
return super(XMLSchemaMeta, mcs).__new__(mcs, name, bases, dict_)
dict_['meta_schema'] = None
dict_['lock'] = threading.Lock() # Lock instance for shared meta-schemas

xsd_version = dict_.get('XSD_VERSION') or get_attribute('XSD_VERSION', *bases)
if xsd_version not in ('1.0', '1.1'):
Expand Down Expand Up @@ -118,10 +120,11 @@ def get_attribute(attr, *args):
meta_schema_class.__qualname__ = meta_schema_class_name
globals()[meta_schema_class_name] = meta_schema_class

# Build the new meta-schema instance
# Build the shared meta-schema instance
schema_location = meta_schema.url if isinstance(meta_schema, XMLSchemaBase) else meta_schema
meta_schema = meta_schema_class.create_meta_schema(schema_location)
dict_['meta_schema'] = meta_schema
dict_.pop('lock')

return super(XMLSchemaMeta, mcs).__new__(mcs, name, bases, dict_)

Expand Down Expand Up @@ -200,6 +203,10 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
:vartype final_default: str
:cvar default_attributes: the XSD 1.1 schema's *defaultAttributes* attribute, defaults to ``None``.
:vartype default_attributes: XsdAttributeGroup
:cvar xpath_tokens: symbol table for schema bound XPath 2.0 parsers. Initially set to \
``None`` it's redefined at instance level with a dictionary at first use of the XPath \
selector. The parser symbol table is extended with schema types constructors.
:vartype xpath_tokens: dict
:ivar target_namespace: is the *targetNamespace* of the schema, the namespace to which \
belong the declarations/definitions of the schema. If it's empty no namespace is associated \
Expand Down Expand Up @@ -258,11 +265,14 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin):
default_attributes = None
default_open_content = None
override = None
xpath_tokens = None

def __init__(self, source, namespace=None, validation='strict', global_maps=None,
converter=None, locations=None, base_url=None, defuse='remote',
timeout=300, build=True, use_meta=True, loglevel=None):
super(XMLSchemaBase, self).__init__(validation)
ElementPathMixin.__init__(self)

if loglevel is not None:
logger.setLevel(loglevel)
elif build and global_maps is None:
Expand Down Expand Up @@ -335,21 +345,17 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None

self.locations = NamespaceResourcesMap(self.source.get_locations(locations))
self.converter = self.get_converter(converter)
self.xpath_tokens = {}

if self.meta_schema is None:
# Meta-schema creation phase (MetaXMLSchema class)
self.maps = global_maps or XsdGlobals(self)
for child in filter(lambda x: x.tag == XSD_OVERRIDE, self.root):
self.include_schema(child.attrib['schemaLocation'], self.base_url)
return # Meta-schemas don't need to be checked and don't process imports
elif not self.meta_schema.maps.types:
self.meta_schema.maps.build()

# Validate the schema document (transforming validation errors to parse errors)
if validation != 'skip':
for e in self.meta_schema.iter_errors(root, namespaces=self.namespaces):
self.parse_error(e.reason, elem=e.elem)
with self.meta_schema.lock:
if not self.meta_schema.maps.types:
self.meta_schema.maps.build()

# Create or set the XSD global maps instance
if global_maps is None:
Expand All @@ -372,6 +378,11 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None
if k not in {'targetNamespace', VC_MIN_VERSION, VC_MAX_VERSION}:
del root.attrib[k]

# Validate the schema document (transforming validation errors to parse errors)
if validation != 'skip':
for e in self.meta_schema.iter_errors(root, namespaces=self.namespaces):
self.parse_error(e.reason, elem=e.elem)

self._parse_inclusions()
self._parse_imports()

Expand Down Expand Up @@ -403,16 +414,6 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None
if loglevel is not None:
logger.setLevel(logging.WARNING) # Restore default logging

def __getstate__(self):
state = self.__dict__.copy()
del state['xpath_tokens']
state.pop('_xpath_parser', None)
return state

def __setstate__(self, state):
self.__dict__.update(state)
self.xpath_tokens = {}

def __repr__(self):
if self.url:
basename = os.path.basename(self.url)
Expand Down Expand Up @@ -595,6 +596,8 @@ def constraints(self):
"""
Old reference to identity constraints, for backward compatibility. Will be removed in v1.1.0.
"""
warnings.warn("'constraints' property has been replaced by 'identities' "
"and will be removed in 1.1 version.", DeprecationWarning)
return self.identities

@classmethod
Expand Down Expand Up @@ -641,6 +644,7 @@ def create_meta_schema(cls, source=None, base_schemas=None, global_maps=None):
@classmethod
def create_schema(cls, *args, **kwargs):
"""Creates a new schema instance of the same class of the caller."""
warnings.warn("'create_schema()' method will be removed in 1.1 version.", DeprecationWarning)
return cls(*args, **kwargs)

def create_any_content_group(self, parent, any_element=None):
Expand Down Expand Up @@ -913,7 +917,7 @@ def include_schema(self, location, base_url=None):
if schema_url == schema.url:
break
else:
schema = self.create_schema(
schema = type(self)(
source=schema_url,
namespace=self.target_namespace,
validation=self.validation,
Expand Down Expand Up @@ -1045,7 +1049,7 @@ def import_schema(self, namespace, location, base_url=None, force=False, build=F
self.imports[namespace] = schema
return schema

schema = self.create_schema(
schema = type(self)(
source=schema_url,
validation=self.validation,
global_maps=self.maps,
Expand Down
9 changes: 4 additions & 5 deletions xmlschema/validators/wildcards.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ class XsdWildcard(XsdComponent, ValidationMixin):
not_qname = ()
process_contents = 'strict'

def __init__(self, elem, schema, parent):
if parent is None:
raise XMLSchemaValueError("'parent' attribute is None but %r cannot be global!" % self)
super(XsdWildcard, self).__init__(elem, schema, parent)

def __repr__(self):
if self.not_namespace:
return '%s(not_namespace=%r, process_contents=%r)' % (
Expand Down Expand Up @@ -368,6 +363,10 @@ class XsdAnyElement(XsdWildcard, ParticleMixin, ElementPathMixin):
_ADMITTED_TAGS = {XSD_ANY}
precedences = ()

def __init__(self, elem, schema, parent):
super(XsdAnyElement, self).__init__(elem, schema, parent)
ElementPathMixin.__init__(self)

def __repr__(self):
if self.namespace:
return '%s(namespace=%r, process_contents=%r, occurs=%r)' % (
Expand Down
38 changes: 22 additions & 16 deletions xmlschema/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from __future__ import unicode_literals
from abc import abstractmethod
from elementpath import XPath2Parser, XPathSchemaContext, AbstractSchemaProxy
import threading

from .compat import Sequence
from .qnames import XSD_SCHEMA
Expand Down Expand Up @@ -97,14 +98,13 @@ def bind_parser(self, parser):
if parser.schema is not self:
parser.schema = self

try:
parser.symbol_table = self._schema.xpath_tokens[parser.__class__]
except KeyError:
if self._schema.xpath_tokens is None:
parser.symbol_table = parser.__class__.symbol_table.copy()
self._schema.xpath_tokens[parser.__class__] = parser.symbol_table
for xsd_type in self.iter_atomic_types():
parser.schema_constructor(xsd_type.name)

self._schema.xpath_tokens = parser.symbol_table
else:
parser.symbol_table = self._schema.xpath_tokens
parser.tokenizer = parser.create_tokenizer(parser.symbol_table)

def get_context(self):
Expand Down Expand Up @@ -183,11 +183,20 @@ class ElementPathMixin(Sequence):

_xpath_parser = None # Internal XPath 2.0 parser, instantiated at first use.

def __init__(self):
self._xpath_lock = threading.Lock() # Lock for XPath operations

def __getstate__(self):
state = self.__dict__.copy()
state.pop('_xpath_lock', None)
state.pop('_xpath_parser', None)
state.pop('xpath_tokens', None) # For schema objects
return state

def __setstate__(self, state):
self.__dict__.update(state)
self._xpath_lock = threading.Lock()

@abstractmethod
def __iter__(self):
pass
Expand Down Expand Up @@ -223,11 +232,6 @@ def xpath_proxy(self):
"""Returns an XPath proxy instance bound with the schema."""
raise NotImplementedError

def _rebind_xpath_parser(self):
"""Rebind XPath 2 parser with schema component."""
if self._xpath_parser is not None:
self._xpath_parser.schema.bind_parser(self._xpath_parser)

def _get_xpath_namespaces(self, namespaces=None):
"""
Returns a dictionary with namespaces for XPath selection.
Expand All @@ -251,12 +255,14 @@ def _xpath_parse(self, path, namespaces=None):
path = ''.join(['/', XSD_SCHEMA, path])

namespaces = self._get_xpath_namespaces(namespaces)
if self._xpath_parser is None:
self._xpath_parser = XPath2Parser(namespaces, strict=False, schema=self.xpath_proxy)
else:
self._xpath_parser.namespaces = namespaces

return self._xpath_parser.parse(path)
with self._xpath_lock:
parser = self._xpath_parser
if parser is None:
parser = XPath2Parser(namespaces, strict=False, schema=self.xpath_proxy)
self._xpath_parser = parser
else:
parser.namespaces = namespaces
return parser.parse(path)

def find(self, path, namespaces=None):
"""
Expand Down

0 comments on commit a6b9781

Please sign in to comment.