Spaces:
Sleeping
Sleeping
"""Tools for manipulation of expressions using paths. """ | |
from sympy.core import Basic | |
class EPath: | |
r""" | |
Manipulate expressions using paths. | |
EPath grammar in EBNF notation:: | |
literal ::= /[A-Za-z_][A-Za-z_0-9]*/ | |
number ::= /-?\d+/ | |
type ::= literal | |
attribute ::= literal "?" | |
all ::= "*" | |
slice ::= "[" number? (":" number? (":" number?)?)? "]" | |
range ::= all | slice | |
query ::= (type | attribute) ("|" (type | attribute))* | |
selector ::= range | query range? | |
path ::= "/" selector ("/" selector)* | |
See the docstring of the epath() function. | |
""" | |
__slots__ = ("_path", "_epath") | |
def __new__(cls, path): | |
"""Construct new EPath. """ | |
if isinstance(path, EPath): | |
return path | |
if not path: | |
raise ValueError("empty EPath") | |
_path = path | |
if path[0] == '/': | |
path = path[1:] | |
else: | |
raise NotImplementedError("non-root EPath") | |
epath = [] | |
for selector in path.split('/'): | |
selector = selector.strip() | |
if not selector: | |
raise ValueError("empty selector") | |
index = 0 | |
for c in selector: | |
if c.isalnum() or c in ('_', '|', '?'): | |
index += 1 | |
else: | |
break | |
attrs = [] | |
types = [] | |
if index: | |
elements = selector[:index] | |
selector = selector[index:] | |
for element in elements.split('|'): | |
element = element.strip() | |
if not element: | |
raise ValueError("empty element") | |
if element.endswith('?'): | |
attrs.append(element[:-1]) | |
else: | |
types.append(element) | |
span = None | |
if selector == '*': | |
pass | |
else: | |
if selector.startswith('['): | |
try: | |
i = selector.index(']') | |
except ValueError: | |
raise ValueError("expected ']', got EOL") | |
_span, span = selector[1:i], [] | |
if ':' not in _span: | |
span = int(_span) | |
else: | |
for elt in _span.split(':', 3): | |
if not elt: | |
span.append(None) | |
else: | |
span.append(int(elt)) | |
span = slice(*span) | |
selector = selector[i + 1:] | |
if selector: | |
raise ValueError("trailing characters in selector") | |
epath.append((attrs, types, span)) | |
obj = object.__new__(cls) | |
obj._path = _path | |
obj._epath = epath | |
return obj | |
def __repr__(self): | |
return "%s(%r)" % (self.__class__.__name__, self._path) | |
def _get_ordered_args(self, expr): | |
"""Sort ``expr.args`` using printing order. """ | |
if expr.is_Add: | |
return expr.as_ordered_terms() | |
elif expr.is_Mul: | |
return expr.as_ordered_factors() | |
else: | |
return expr.args | |
def _hasattrs(self, expr, attrs): | |
"""Check if ``expr`` has any of ``attrs``. """ | |
for attr in attrs: | |
if not hasattr(expr, attr): | |
return False | |
return True | |
def _hastypes(self, expr, types): | |
"""Check if ``expr`` is any of ``types``. """ | |
_types = [ cls.__name__ for cls in expr.__class__.mro() ] | |
return bool(set(_types).intersection(types)) | |
def _has(self, expr, attrs, types): | |
"""Apply ``_hasattrs`` and ``_hastypes`` to ``expr``. """ | |
if not (attrs or types): | |
return True | |
if attrs and self._hasattrs(expr, attrs): | |
return True | |
if types and self._hastypes(expr, types): | |
return True | |
return False | |
def apply(self, expr, func, args=None, kwargs=None): | |
""" | |
Modify parts of an expression selected by a path. | |
Examples | |
======== | |
>>> from sympy.simplify.epathtools import EPath | |
>>> from sympy import sin, cos, E | |
>>> from sympy.abc import x, y, z, t | |
>>> path = EPath("/*/[0]/Symbol") | |
>>> expr = [((x, 1), 2), ((3, y), z)] | |
>>> path.apply(expr, lambda expr: expr**2) | |
[((x**2, 1), 2), ((3, y**2), z)] | |
>>> path = EPath("/*/*/Symbol") | |
>>> expr = t + sin(x + 1) + cos(x + y + E) | |
>>> path.apply(expr, lambda expr: 2*expr) | |
t + sin(2*x + 1) + cos(2*x + 2*y + E) | |
""" | |
def _apply(path, expr, func): | |
if not path: | |
return func(expr) | |
else: | |
selector, path = path[0], path[1:] | |
attrs, types, span = selector | |
if isinstance(expr, Basic): | |
if not expr.is_Atom: | |
args, basic = self._get_ordered_args(expr), True | |
else: | |
return expr | |
elif hasattr(expr, '__iter__'): | |
args, basic = expr, False | |
else: | |
return expr | |
args = list(args) | |
if span is not None: | |
if isinstance(span, slice): | |
indices = range(*span.indices(len(args))) | |
else: | |
indices = [span] | |
else: | |
indices = range(len(args)) | |
for i in indices: | |
try: | |
arg = args[i] | |
except IndexError: | |
continue | |
if self._has(arg, attrs, types): | |
args[i] = _apply(path, arg, func) | |
if basic: | |
return expr.func(*args) | |
else: | |
return expr.__class__(args) | |
_args, _kwargs = args or (), kwargs or {} | |
_func = lambda expr: func(expr, *_args, **_kwargs) | |
return _apply(self._epath, expr, _func) | |
def select(self, expr): | |
""" | |
Retrieve parts of an expression selected by a path. | |
Examples | |
======== | |
>>> from sympy.simplify.epathtools import EPath | |
>>> from sympy import sin, cos, E | |
>>> from sympy.abc import x, y, z, t | |
>>> path = EPath("/*/[0]/Symbol") | |
>>> expr = [((x, 1), 2), ((3, y), z)] | |
>>> path.select(expr) | |
[x, y] | |
>>> path = EPath("/*/*/Symbol") | |
>>> expr = t + sin(x + 1) + cos(x + y + E) | |
>>> path.select(expr) | |
[x, x, y] | |
""" | |
result = [] | |
def _select(path, expr): | |
if not path: | |
result.append(expr) | |
else: | |
selector, path = path[0], path[1:] | |
attrs, types, span = selector | |
if isinstance(expr, Basic): | |
args = self._get_ordered_args(expr) | |
elif hasattr(expr, '__iter__'): | |
args = expr | |
else: | |
return | |
if span is not None: | |
if isinstance(span, slice): | |
args = args[span] | |
else: | |
try: | |
args = [args[span]] | |
except IndexError: | |
return | |
for arg in args: | |
if self._has(arg, attrs, types): | |
_select(path, arg) | |
_select(self._epath, expr) | |
return result | |
def epath(path, expr=None, func=None, args=None, kwargs=None): | |
r""" | |
Manipulate parts of an expression selected by a path. | |
Explanation | |
=========== | |
This function allows to manipulate large nested expressions in single | |
line of code, utilizing techniques to those applied in XML processing | |
standards (e.g. XPath). | |
If ``func`` is ``None``, :func:`epath` retrieves elements selected by | |
the ``path``. Otherwise it applies ``func`` to each matching element. | |
Note that it is more efficient to create an EPath object and use the select | |
and apply methods of that object, since this will compile the path string | |
only once. This function should only be used as a convenient shortcut for | |
interactive use. | |
This is the supported syntax: | |
* select all: ``/*`` | |
Equivalent of ``for arg in args:``. | |
* select slice: ``/[0]`` or ``/[1:5]`` or ``/[1:5:2]`` | |
Supports standard Python's slice syntax. | |
* select by type: ``/list`` or ``/list|tuple`` | |
Emulates ``isinstance()``. | |
* select by attribute: ``/__iter__?`` | |
Emulates ``hasattr()``. | |
Parameters | |
========== | |
path : str | EPath | |
A path as a string or a compiled EPath. | |
expr : Basic | iterable | |
An expression or a container of expressions. | |
func : callable (optional) | |
A callable that will be applied to matching parts. | |
args : tuple (optional) | |
Additional positional arguments to ``func``. | |
kwargs : dict (optional) | |
Additional keyword arguments to ``func``. | |
Examples | |
======== | |
>>> from sympy.simplify.epathtools import epath | |
>>> from sympy import sin, cos, E | |
>>> from sympy.abc import x, y, z, t | |
>>> path = "/*/[0]/Symbol" | |
>>> expr = [((x, 1), 2), ((3, y), z)] | |
>>> epath(path, expr) | |
[x, y] | |
>>> epath(path, expr, lambda expr: expr**2) | |
[((x**2, 1), 2), ((3, y**2), z)] | |
>>> path = "/*/*/Symbol" | |
>>> expr = t + sin(x + 1) + cos(x + y + E) | |
>>> epath(path, expr) | |
[x, x, y] | |
>>> epath(path, expr, lambda expr: 2*expr) | |
t + sin(2*x + 1) + cos(2*x + 2*y + E) | |
""" | |
_epath = EPath(path) | |
if expr is None: | |
return _epath | |
if func is None: | |
return _epath.select(expr) | |
else: | |
return _epath.apply(expr, func, args, kwargs) | |