123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- """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)
|