This commit is contained in:
2026-08-02 18:57:40 +02:00
parent 5e5ab8681a
commit 6fa59321c0
5759 changed files with 712133 additions and 66 deletions
@@ -0,0 +1,658 @@
########################################################################
# File name: __init__.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.xso` --- Working with XML stream contents
########################################################
This subpackage deals with **X**\\ ML **S**\\ tream **O**\\ bjects. XSOs can be
stanzas, but in general any XML.
The facilities in this subpackage are supposed to help developers of XEP
plugins, as well as the main development of :mod:`aioxmpp`. The subpackage
is split in two parts, :mod:`aioxmpp.xso.model`, which provides facilities to
allow declarative-style parsing and un-parsing of XML subtrees into XSOs and
the :mod:`aioxmpp.xso.types` module, which provides classes which implement
validators and type parsers for content represented as strings in XML.
Introduction
============
.. seealso::
For a more in-depth introduction into :mod:`aioxmpp.xso`, please refer to
the :ref:`ug-introduction-to-xso` chapter in the user guide. This document
here is a reference manual.
The :mod:`aioxmpp.xso` subpackage provides declarative-style parsing of XML
document fragments. The declarations are similar to what you might know from
declarative Object-Relational-Mappers such as :mod:`sqlalchemy`. Due to the
different data model of XML and relational databases, they are not identical
of course.
An abstract class describing the common properties of an XMPP stanza might
look like this:
.. code:: python
class Stanza(xso.XSO):
from_ = xso.Attr(tag="from", type_=xso.JID(), default=None)
to = xso.Attr(tag="to", type_=xso.JID(), default=None)
lang = xso.LangAttr(tag=(namespaces.xml, "lang"))
Instances of classes deriving from :class:`aioxmpp.xso.XSO` are called XML
stream objects, or XSOs for short. Each XSO maps to an XML element node.
The declaration of an XSO class typically has one or more
:term:`descriptors <descriptor>` describing the mapping of XML child nodes of
the element. XML nodes which can be mapped include attributes, text and
elements (processing instructions and comments are not supported; CDATA
sections are treated like text).
XSO-specific Terminology
========================
Definition of an XSO
--------------------
An XSO is an object whose class inherits from
:class:`aioxmpp.xso.XSO`.
A word on tags
--------------
Tags, as used by etree, are used throughout this module. Note that we are
representing tags as tuples of ``(namespace_uri, localname)``, where
``namespace_uri`` may be :data:`None`.
.. seealso::
The functions :func:`normalize_tag` and :func:`tag_to_str` are useful to
convert from and to ElementTree compatible strings.
XML stream events
-----------------
XSOs are parsed using SAX-like events. This allows them to be built one-by-one
in memory (and discarded) even while the XML stream is in progress.
The XSO module uses a subset of the original SAX event list, and it uses a
custom format. The reason for that is that instead of using an interface with
methods, the parsing parts are implemented using suspendable functions (see
below).
Suspendable functions
---------------------
This module uses suspendable functions, implemented as generators, at several
points. These may also be called coroutines, but have nothing to do with
coroutines as used by :mod:`asyncio`, which is why we will call them
suspendable functions here.
Suspendable functions possibly take arguments and then operate on input which
is fed to them in a push-manner step by step (using the
:meth:`~types.GeneratorType.send` method). The main usage in this module is to
process XML stream events: The SAX events are processed step-by-step by the functions,
and when the event is fully processed, it suspends itself (using ``yield``)
until the next event is sent into it.
General functions
=================
.. autofunction:: normalize_tag
.. autofunction:: tag_to_str
.. module:: aioxmpp.xso.model
.. currentmodule:: aioxmpp.xso
Object declaration with :mod:`aioxmpp.xso.model`
================================================
This module provides facilities to create classes which map to full XML stream
subtrees (for example stanzas including payload).
To create such a class, derive from :class:`XSO` and provide attributes
using the :class:`Attr`, :class:`Text`, :class:`Child` and :class:`ChildList`
descriptors.
Descriptors for XML-sourced attributes
--------------------------------------
.. autosummary::
Attr
LangAttr
Text
Child
ChildTag
ChildFlag
ChildText
ChildTextMap
ChildValue
ChildList
ChildMap
ChildLangMap
ChildValueList
ChildValueMap
ChildValueMultiMap
Collector
The following descriptors can be used to load XSO attributes from XML. There
are two fundamentally different descriptor types: *scalar* and *non-scalar*
(e.g. list) descriptors. Assignment to the descriptor attribute is
strictly type-checked for *scalar* descriptors.
Scalar descriptors
^^^^^^^^^^^^^^^^^^
Many of the arguments and attributes used for the scalar descriptors are
similar. They are described in detail on the :class:`Attr` class and not
repeated that detailed on the other classes. Refer to the documentation of the
:class:`Attr` class in those cases.
.. autoclass:: Attr(name, *[, type_=xso.String()][, validator=None][, validate=ValidateMode.FROM_RECV][, missing=None][, default][, erroneous_as_absent=False])
.. autoclass:: LangAttr(*[, validator=None][, validate=ValidateMode.FROM_RECV][, default=None])
.. autoclass:: Child(classes, *[, required=False][, strict=False])
.. autoclass:: ChildTag(tags, *[, text_policy=UnknownTextPolicy.FAIL][, child_policy=UnknownChildPolicy.FAIL][, attr_policy=UnknownAttrPolicy.FAIL][, default_ns=None][, allow_none=False])
.. autoclass:: ChildFlag(tag, *[, text_policy=UnknownTextPolicy.FAIL][, child_policy=UnknownChildPolicy.FAIL][, attr_policy=UnknownAttrPolicy.FAIL])
.. autoclass:: ChildText(tag, *[, child_policy=UnknownChildPolicy.FAIL][, attr_policy=UnknownAttrPolicy.FAIL][, type_=xso.String()][, validator=None][, validate=ValidateMode.FROM_RECV][, default][, erroneous_as_absent=False])
.. autoclass:: ChildValue(type_)
.. autoclass:: Text(*[, type_=xso.String()][, validator=None][, validate=ValidateMode.FROM_RECV][, default][, erroneous_as_absent=False])
Non-scalar descriptors
^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: ChildList(classes)
.. autoclass:: ChildMap(classes[, key=None])
.. autoclass:: ChildLangMap(classes)
.. autoclass:: ChildValueList(type_)
.. autoclass:: ChildValueMap(type_, *, mapping_type=dict)
.. autoclass:: ChildValueMultiMap(type_, *, mapping_type=multidict.MultiDict)
.. autoclass:: ChildTextMap(xso_type)
.. autoclass:: Collector()
Container for child lists
^^^^^^^^^^^^^^^^^^^^^^^^^
The child lists in :class:`ChildList`, :class:`ChildMap` and
:class:`ChildLangMap` descriptors use a specialized list-subclass which
provides advanced capabilities for filtering :class:`XSO` objects.
.. currentmodule:: aioxmpp.xso.model
.. autoclass:: XSOList
.. currentmodule:: aioxmpp.xso
Parsing XSOs
------------
To parse XSOs, an asynchronous approach which uses SAX-like events is
followed. For this, the suspendable functions explained earlier are used. The
main class to parse a XSO from events is :class:`XSOParser`. To drive
that suspendable callable from SAX events, use a :class:`SAXDriver`.
.. autoclass:: XSOParser
.. autoclass:: SAXDriver
Base and meta class
-------------------
The :class:`XSO` base class makes use of the :class:`model.XMLStreamClass`
metaclass and provides implementations for utility methods. For an object to
work with this module, it must derive from :class:`XSO` or provide an
identical interface.
.. autoclass:: XSO()
.. autoclass:: CapturingXSO()
The metaclass takes care of collecting the special descriptors in attributes
where they can be used by the SAX event interpreter to fill the class with
data. It also provides a class method for late registration of child classes.
.. currentmodule:: aioxmpp.xso.model
.. autoclass:: XMLStreamClass
.. currentmodule:: aioxmpp.xso
To create an enumeration of XSO classes, the following mixin can be used:
.. autoclass:: XSOEnumMixin
Functions, enumerations and exceptions
--------------------------------------
The values of the following enumerations are used on "magic" attributes of
:class:`XMLStreamClass` instances (i.e. classes).
.. autoclass:: UnknownChildPolicy
.. autoclass:: UnknownAttrPolicy
.. autoclass:: UnknownTextPolicy
.. autoclass:: ValidateMode
The following exceptions are generated at some places in this module:
.. autoclass:: UnknownTopLevelTag
The following special value is used to indicate that no default is used with a
descriptor:
.. data:: NO_DEFAULT
This is a special value which is used to indicate that no defaulting should
take place. It can be passed to the `default` arguments of descriptors, and
usually is the default value of these arguments.
It compares unequal to everything but itself, does not support ordering,
conversion to bool, float or integer.
.. autofunction:: capture_events
.. autofunction:: events_to_sax
Handlers for missing attributes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autofunction:: lang_attr
.. module:: aioxmpp.xso.types
.. currentmodule:: aioxmpp.xso
Types and validators from :mod:`~aioxmpp.xso.types`
===================================================
This module provides classes whose objects can be used as types and validators
in :mod:`~aioxmpp.xso.model`.
Character Data types
--------------------
.. autosummary::
String
Float
Integer
Bool
DateTime
Date
Time
Base64Binary
HexBinary
JID
ConnectionLocation
LanguageTag
JSON
EnumCDataType
These types describe character data, i.e. text in XML. Thus, they can be used
with :class:`Attr`, :class:`Text` and similar descriptors. They are used to
deserialise XML character data to python values, such as integers or dates and
vice versa. These types inherit from :class:`AbstractCDataType`.
.. autoclass:: String
.. autoclass:: Float
.. autoclass:: Integer
.. autoclass:: Bool
.. autoclass:: DateTime
.. autoclass:: Date
.. autoclass:: Time
.. autoclass:: Base64Binary
.. autoclass:: HexBinary
.. autoclass:: JID
.. autoclass:: ConnectionLocation
.. autoclass:: LanguageTag
.. autoclass:: JSON
.. autoclass:: EnumCDataType(enum_class, nested_type=xso.String(), *, allow_coerce=False, deprecate_coerce=False, allow_unknown=True, accept_unknown=True)
.. autofunction:: EnumType(enum_class[, nested_type], *, allow_coerce=False, deprecate_coerce=False, allow_unknown=True, accept_unknown=True)
.. autoclass:: Unknown
Element types
-------------
.. autosummary::
EnumElementType
TextChildMap
These types describe structured XML data, i.e. subtrees. Thus, they can be used
with the :class:`ChildValueList` and :class:`ChildValueMap` family of
descriptors (which represent XSOs as python values). These types inherit from
:class:`AbstractElementType`.
.. autoclass:: EnumElementType
.. autoclass:: TextChildMap
Defining custom types
---------------------
.. autoclass:: AbstractCDataType
.. autoclass:: AbstractElementType
Validators
----------
Validators validate the python values after they have been parsed from
XML-sourced strings or even when being assigned to a descriptor attribute
(depending on the choice in the `validate` argument).
They can be useful both for defending and rejecting incorrect input and to
avoid producing incorrect output.
The basic validator interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: AbstractValidator
Implementations
^^^^^^^^^^^^^^^
.. autoclass:: RestrictToSet
.. autoclass:: Nmtoken
.. autoclass:: IsInstance
.. autoclass:: NumericRange
.. module:: aioxmpp.xso.query
.. currentmodule:: aioxmpp.xso
Querying data from XSOs
=======================
With XML, we have XPath as query language to retrieve data from XML trees. With
XSOs, we have :mod:`aioxmpp.xso.query`, even though its not as powerful as
XPath.
Syntactically, its oriented on XPath. Consider the following XSO classes:
.. code-block:: python
class FooXSO(xso.XSO):
TAG = (None, "foo")
attr = xso.Attr(
"attr"
)
class BarXSO(xso.XSO):
TAG = (None, "bar")
child = xso.Child([
FooXSO,
])
class BazXSO(FooXSO):
TAG = (None, "baz")
attr2 = xso.Attr(
"attr2"
)
class RootXSO(xso.XSO):
TAG = (None, "root")
children = xso.ChildList([
FooXSO,
BarXSO,
])
attr = xso.Attr(
"attr"
)
To perform a query, we first need to set up a
:class:`.query.EvaluationContext`:
.. code-block:: python
root_xso = # a RootXSO instance
ec = xso.query.EvaluationContext()
ec.set_toplevel_object(root_xso)
Using the context, we can now execute queries:
.. code-block:: python
# to find all FooXSO children of the RootXSO
ec.eval(RootXSO.children / FooXSO)
# to find all BarXSO children of the RootXSO
ec.eval(RootXSO.children / BarXSO)
# to find all FooXSO children of the RootXSO, where FooXSO.attr
# is set
ec.eval(RootXSO.children / FooXSO[where(FooXSO.attr)])
# to find all FooXSO children of the RootXSO, where FooXSO.attr
# is *not* set
ec.eval(RootXSO.children / FooXSO[where(not FooXSO.attr)])
# to find all FooXSO children of the RootXSO, where FooXSO.attr
# is set to "foobar"
ec.eval(RootXSO.children / FooXSO[where(FooXSO.attr == "foobar")])
# to test whether there is a FooXSO which has attr set to
# "foobar"
ec.eval(RootXSO.children / FooXSO.attr == "foobar")
# to find the first three FooXSO children where attr is set
ec.eval(RootXSO.children / FooXSO[where(FooXSO.attr)][:3])
The following operators are available in the :mod:`aioxmpp.xso` namespace:
.. autoclass:: where
.. autofunction:: not_
The following need to be explicitly sourced from :mod:`aioxmpp.xso.query`, as
they are rarely used directly in user code.
.. currentmodule:: aioxmpp.xso.query
.. autoclass:: EvaluationContext()
.. note::
The implementation details of the query language are documented in the
source. They are not useful unless you want to implement custom query
operators, which is not possible without modifying the
:mod:`aioxmpp.xso.query` source anyways.
.. currentmodule:: aioxmpp.xso
Predefined XSO base classes
===========================
Some patterns reoccur when using this subpackage. For these, base classes are
provided which facilitate the use.
.. autoclass:: AbstractTextChild
""" # NOQA: E501
def tag_to_str(tag):
"""
`tag` must be a tuple ``(namespace_uri, localname)``. Return a tag string
conforming to the ElementTree specification. Example::
tag_to_str(("jabber:client", "iq")) == "{jabber:client}iq"
"""
return "{{{:s}}}{:s}".format(*tag) if tag[0] else tag[1]
def normalize_tag(tag):
"""
Normalize an XML element tree `tag` into the tuple format. The following
input formats are accepted:
* ElementTree namespaced string, e.g. ``{uri:bar}foo``
* Unnamespaced tags, e.g. ``foo``
* Two-tuples consisting of `namespace_uri` and `localpart`; `namespace_uri`
may be :data:`None` if the tag is supposed to be namespaceless. Otherwise
it must be, like `localpart`, a :class:`str`.
Return a two-tuple consisting the ``(namespace_uri, localpart)`` format.
"""
if isinstance(tag, str):
namespace_uri, sep, localname = tag.partition("}")
if sep:
if not namespace_uri.startswith("{"):
raise ValueError("not a valid etree-format tag")
namespace_uri = namespace_uri[1:]
else:
localname = namespace_uri
namespace_uri = None
return (namespace_uri, localname)
elif len(tag) != 2:
raise ValueError("not a valid tuple-format tag")
else:
if any(part is not None and not isinstance(part, str) for part in tag):
raise TypeError("tuple-format tags must only contain str and None")
if tag[1] is None:
raise ValueError("tuple-format localname must not be None")
return tag
from .types import ( # NOQA: F401
Unknown,
AbstractCDataType,
AbstractElementType,
String,
Integer,
Float,
Bool,
DateTime,
Date,
Time,
Base64Binary,
HexBinary,
JID,
ConnectionLocation,
LanguageTag,
JSON,
TextChildMap,
EnumType,
EnumCDataType,
EnumElementType,
AbstractValidator,
RestrictToSet,
Nmtoken,
IsInstance,
NumericRange,
)
from .model import ( # NOQA: F401
UnknownChildPolicy,
UnknownAttrPolicy,
UnknownTextPolicy,
ValidateMode,
UnknownTopLevelTag,
Attr,
LangAttr,
ChildValue,
Child,
ChildFlag,
ChildList,
ChildLangMap,
ChildMap,
ChildTag,
ChildText,
Collector,
Text,
ChildValueList,
ChildValueMap,
ChildValueMultiMap,
ChildTextMap,
XSOParser,
SAXDriver,
XSO,
XSOEnumMixin,
CapturingXSO,
lang_attr,
capture_events,
events_to_sax,
AbstractTextChild,
)
from .model import _PropBase # NOQA: E402
NO_DEFAULT = _PropBase.NO_DEFAULT
del _PropBase
from .query import ( # NOQA: F401
where,
not_,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,447 @@
########################################################################
# File name: query.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
import abc
import copy
import itertools
import inspect
import operator
class _SoftExprMixin:
"""
This mixin is used for metaclasses and descriptors.
It defines the operators ``/`` and ``[]``, which are rarely used for either
classes or descriptors.
.. seealso::
:class:`_ExprMixin`
which inherits from this class and defines more operators, some of
which would be unsafe to implement on classes or descriptors, such as
``==``.
"""
def __truediv__(self, other):
if isinstance(other, PreExpr):
return as_expr(other, lhs=self)
elif isinstance(other, Expr):
return as_expr(other, lhs=self)
return NotImplemented
def __getitem__(self, index):
if isinstance(index, where):
return ExprFilter(self, as_expr(index.expr))
return Nth(self, as_expr(index))
class _ExprMixin(_SoftExprMixin):
"""
This mixin defines operators which are only "safe" to overload in
constrained situations. These operators often have meanings and may be
implicitly used by the python language; thus, they are only defined on
:class:`Expr` subclasses and some :class:`PreExpr` subclasses.
The defined operators currently are:
* Comparison: ``==``, ``<``, ``<=``, ``>=``, ``>``, ``!=``
"""
def __eq__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.eq,
)
def __ne__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.ne,
)
def __lt__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.lt,
)
def __gt__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.gt,
)
def __ge__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.ge,
)
def __le__(self, other):
return CmpOp(
as_expr(self),
as_expr(other),
operator.le,
)
class EvaluationContext:
"""
The evaluation context holds contextual information for the evaluation of a
query expression.
Most notably, it provides the methods for acquiring and replacing the
toplevel objects of classes:
.. automethod:: get_toplevel_object()
.. automethod:: set_toplevel_object()
In addition, it provides shortcuts for evaluating expressions:
.. automethod:: eval
.. automethod:: eval_bool
"""
def __init__(self, *args, **kwargs):
super().__init__()
self._toplevels = {}
def __copy__(self):
result = type(self).__new__(type(self))
result._toplevels = dict(self._toplevels)
return result
def get_toplevel_object(self, class_):
"""
Return the toplevel object for the given `class_`. Only exact matches
are returned.
"""
return self._toplevels[class_]
def set_toplevel_object(self, instance, class_=None):
"""
Set the toplevel object to return from :meth:`get_toplevel_object` when
asked for `class_` to `instance`.
If `class_` is :data:`None`, the :func:`type` of the `instance` is
used.
"""
if class_ is None:
class_ = type(instance)
self._toplevels[class_] = instance
def eval(self, expr):
"""
Evaluate the expression `expr` and return the result.
The result of an expression is always an iterable.
"""
return expr.eval(self)
def eval_bool(self, expr):
"""
Evaluate the expression `expr` and return the truthness of its result.
A result of an expression is said to be true if it contains at least
one value. It has the same semantics as :func:`bool` on sequences.s
"""
result = expr.eval(self)
iterator = iter(result)
try:
next(iterator)
except StopIteration:
return False
else:
return True
finally:
if hasattr(iterator, "close"):
iterator.close()
class Expr(_ExprMixin, metaclass=abc.ABCMeta):
"""
Base class for things which are solely expressions and nothing else.
"""
@abc.abstractmethod
def eval(self, ec):
pass
def eval_leaf(self, ec):
result = self.eval(ec)
if inspect.isgenerator(result):
return list(result)
return result
def __repr__(self):
return "<{}.{} {!r}>".format(
type(self).__module__,
type(self).__qualname__,
self.__dict__,
)
class ContextInstance(Expr):
def __init__(self, class_, **kwargs):
super().__init__(**kwargs)
self.class_ = class_
def eval(self, ec):
"""
Retrieve the current toplevel instance of `class_` from the
:class:`EvaluationContext`. `
"""
try:
return [ec.get_toplevel_object(self.class_)]
except KeyError:
return []
class GetDescriptor(Expr):
"""
Represents a descriptor bound to a class.
As an expression, it represents the query for all values of the
`descriptor` on an all instances of `class_` in the result set of `expr`.
"""
def __init__(self, expr, descriptor):
super().__init__()
self.expr = expr
self.descriptor = descriptor
def new_values(self):
return []
def update_values(self, v, vnew):
v.append(vnew)
def eval(self, ec):
vs = self.new_values()
for instance in self.expr.eval(ec):
try:
vnew = self.descriptor.__get__(instance, type(instance))
except AttributeError:
continue
self.update_values(
vs,
vnew
)
return vs
class GetMappingDescriptor(GetDescriptor):
def __init__(self, expr, descriptor, mapping_factory=dict, **kwargs):
super().__init__(expr, descriptor, **kwargs)
self.mapping_factory = mapping_factory
def new_values(self):
return self.mapping_factory()
def update_values(self, v, vnew):
v.update(vnew)
class GetSequenceDescriptor(GetDescriptor):
def __init__(self, expr, descriptor, sequence_factory=list, **kwargs):
super().__init__(expr, descriptor, **kwargs)
self.sequence_factory = sequence_factory
def new_values(self):
return self.sequence_factory()
def update_values(self, v, vnew):
v.extend(vnew)
class GetInstances(Expr):
def __init__(self, expr, class_):
super().__init__()
self.expr = expr
self.class_ = class_
def eval(self, ec):
for obj in self.expr.eval(ec):
if isinstance(obj, self.class_):
yield obj
class Nth(Expr):
def __init__(self, expr, nth_expr):
super().__init__()
self.expr = expr
self.nth_expr = nth_expr
def eval(self, ec):
n, = self.nth_expr.eval(ec)
iterable = self.expr.eval(ec)
if isinstance(n, slice):
return itertools.islice(
iterable,
n.start, n.stop, n.step,
)
return itertools.islice(
self.expr.eval(ec),
n, n+1,
)
class ExprFilter(Expr):
def __init__(self, expr, filter_expr):
super().__init__()
self.expr = expr
self.filter_expr = filter_expr
def eval(self, ec):
for value in self.expr.eval(ec):
sub_ec = copy.copy(ec)
sub_ec.set_toplevel_object(value)
filter_result = sub_ec.eval_bool(self.filter_expr)
if filter_result:
yield value
class where:
"""
Wrap the expression `expr` so that it can be used as a filter in ``[]``.
"""
def __init__(self, expr):
self.expr = expr
class _BoolOpMixin:
def eval(self, ec):
if self.eval_leaf(ec):
yield True
class CmpOp(_BoolOpMixin, Expr):
def __init__(self, operand1, operand2, operator):
super().__init__()
self.operand1 = operand1
self.operand2 = operand2
self.operator = operator
def eval_leaf(self, ec):
vs1 = self.operand1.eval_leaf(ec)
vs2 = self.operand2.eval_leaf(ec)
for v1 in vs1:
for v2 in vs2:
if self.operator(v1, v2):
return True
return False
class NotOp(_BoolOpMixin, Expr):
def __init__(self, operand):
super().__init__()
self.operand = operand
def eval_leaf(self, ec):
return not ec.eval_bool(self.operand)
def not_(expr):
"""
Return the boolean-not of the value of `expr`. A expression value is true
if it contains at least one element and false otherwise.
.. seealso::
:meth:`EvaluationContext.eval_bool`
which is used behind the scenes to calculate the boolean value of
`expr`.
:class:`NotOp`
which actually implements the operator.
"""
return NotOp(as_expr(expr))
class Constant(Expr):
def __init__(self, value):
super().__init__()
self.value = value
def eval(self, ec):
return [self.value]
# Here be dragons: if you use metaclass=abc.ABCMeta with this class, very
# interesting things will blow up
class PreExpr(_SoftExprMixin):
@abc.abstractmethod
def xq_instantiate(self, expr=None):
pass
class Class(PreExpr):
def xq_instantiate(self, expr=None):
if expr is None:
return ContextInstance(self)
return GetInstances(expr, self)
class BoundDescriptor(_ExprMixin, PreExpr):
def __init__(self, class_, descriptor, expr_class, expr_kwargs={},
**kwargs):
super().__init__(**kwargs)
self.xq_xso_class = class_
self.xq_descriptor = descriptor
self.xq_expr_class = expr_class
self.xq_expr_kwargs = expr_kwargs
def xq_instantiate(self, expr=None):
return self.xq_expr_class(
self.xq_xso_class.xq_instantiate(expr),
self.xq_descriptor,
**self.xq_expr_kwargs
)
def __getattr__(self, name):
try:
return super().__getattr__(name)
except AttributeError:
if not name.startswith("xq_"):
return getattr(self.xq_descriptor, name)
raise
def as_expr(thing, lhs=None):
if isinstance(thing, Expr):
if hasattr(thing, "expr"):
thing.expr = as_expr(thing.expr, lhs=lhs)
return thing
if isinstance(thing, PreExpr):
return thing.xq_instantiate(lhs)
return Constant(thing)
File diff suppressed because it is too large Load Diff