You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
9.9 KiB
357 lines
9.9 KiB
5 months ago
|
"""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)
|