Repository: sympy/sympy
Pull request: allow for economical specification of `parse_expr` transformations
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->


#### Brief description of what is fixed or changed

fixes #21226

#### Other comments


#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* parsing
    * strings 'all' and 'implicit' can be passed to select commonly used transformations; `T` can be sliced to give desired transformations for `parse_expr`
<!-- END RELEASE NOTES -->


Problem statement:
Make parse_expr with transformations easier to type
Right now to parse an expression with non-standard transformations like implicit multiplication, you have to do

```py
>>> from sympy.parsing.sympy_parser import standard_transformations, implicit_multiplication_application
>>> transformations = (standard_transformations + (implicit_multiplication_application,))
>>> parse_expr("2x + 1", transformations=transformations)
2*x + 1
```

This is too much typing. We should add some kind of shortcut, like `transformations='all'`, to do this common case.

Hints from issue discussion:
I would like to implement this in order to practice the workflow.
Now you can just do this:

x = parse_expr("2x + 1", transformations="all")
print(x)

@ebab, you can still improve the answer.
@NtemKenyor I do not think that it is enough to check if a string was passed or not. Hence I decided to make my own pull request which specifically checks for `all` and has added documentation.
I agree. I thought about it but I thought it would make people free as d
can pass any string and it still works...

Perhaps your idea is better🥺...

On Fri, Apr 2, 2021, 13:21 Erik Babuschkin ***@***.***> wrote:

> @NtemKenyor <https://github.com/NtemKenyor> I do not think that it is
> enough to check if a string was passed or not. Hence I decided to make my
> own pull request which specifically checks for all and has added
> documentation.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/sympy/sympy/issues/21226#issuecomment-812507077>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AOKWIBNK5Q3Y4FVNSKSY45TTGWZFDANCNFSM42H2OU4Q>
> .
>

What about assigning an int to each transformation that corresponds to a power of 2 so `transformation=3` would be the 0th and 1th element in the logical list of transformations?

The `ibin` function could be used to get the elements corresponding to the int:
```python
>>> from sympy.utilities.iterables import ibin
>>> ibin(11)
[1, 0, 1, 1]
transformation = [transformation_list[i] for i,j in enumerate(_[::-1]) if j]  # transformation_list is the canonical list of all transformations and would only grow in length, never have any element removed
```
I totally agree👍 and it's a great approach.
I did not think this way before now🤯🤯🤯

On Sat, Apr 3, 2021, 21:51 Christopher Smith ***@***.***>
wrote:

> What about assigning an int to each transformation that corresponds to a
> power of 2 so transformation=3 would be the 0th and 1th element in the
> logical list of transformations?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/sympy/sympy/issues/21226#issuecomment-812923621>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AOKWIBPQPPH2SASK4JRLKPLTG55WVANCNFSM42H2OU4Q>
> .
>

> I did not think this way before now

Perhaps something like the following could be added to iterables and then used:
```python
def int_select(i, L):
    """select items from L based on which bits are 1 in the binary representation of i. e.g.
    i = 1 selects item 0 from L, i = -1 selects all items, i = 0 selects none. Any other i must
    not be larger than 2**n - 1 where n is the number of elements in L

    Examples
    ========

    >>> from sympy.utilities.iterables import ibin
    >>> ibin(13)[::-1]
    [1, 0, 1, 1]
    >>> int_select(13, range(4))
    [0, 2, 3]
    """
    if i == 0:
        return []
    if i == -1:
        return L[:]
    if i < 0 or i >= 2**len(L):
        raise ValueError
    return [L[j] for j, b in enumerate(ibin(i)[::-1]) if b]
```
Does anyone have preferences for any or all of these:
```python
def parse_expr(..., xforms=None):
    """
    ...
    xforms : int or iterable, options
        When provided, selects the transformations to use by either selecting
        them according to the position of 1s in the binary representation of
        i or by index provided in the iterable, or as the integers given in a
        comma/dash separated string.

        i    transform
        --  --------------------
        0   lambda_notation,
        1   auto_symbol,
        2   repeated_decimals,
        3   auto_number,
        4   rationalize,
        5   factorial_notation,
        6   implicit_multiplication,
        7   implicit_application,
        8   implicit_multiplication_application,
        9   convert_equals_signs,
        10  function_exponentiation,
        11  convert_xor,

    Examples
    ========

    >>> from sympy.parsing.sympy_parser import parse_expr
    >>> parse_expr("1/2")
    1/2
    >>> type(_)
    <class 'sympy.core.numbers.Half'>
    >>> from sympy.parsing.sympy_parser import standard_transformations,\\
    ... implicit_multiplication_application
    >>> transformations = (standard_transformations +
    ...     (implicit_multiplication_application,))
    >>> parse_expr("2x", transformations=transformations)
    2*x
    >>> parse_expr("2x", xforms=31+2**8)
    2*x
    >>> parse_expr("2x", xforms=(1,2,3,4,5,8))
    2*x
    >>> parse_expr("2x", xforms='1-5, 8')
    2*x
```
I would at use strings or named constants or something rather than integer codes.
> I would at use strings or named constants

What strings would be nice to type? First letters from each of the hyphenated words? `ima -> implicit_multiplication_application`, `st -> standard_transformations`, `st, ima -> standard_transformations + (implicit_multiplication_application,)`. If I ever used these I would nearly always have to look up the names to make sure I get the single or plural form right (e.g. `convert_equals_signs` has double plural) and I would prefer to just type their index number as in the `1-5, 8` case.

Indirect addressing by binary digit is not my preference anymore.

Commit message: add implicit and all keywords for parsing

Patch to review:
diff --git a/sympy/matrices/common.py b/sympy/matrices/common.py
index e6e6da3fe095..8ebf90bc96f4 100644
--- a/sympy/matrices/common.py
+++ b/sympy/matrices/common.py
@@ -76,7 +76,7 @@ def __getitem__(self, key):
         """Implementations of __getitem__ should accept ints, in which
         case the matrix is indexed as a flat list, tuples (i,j) in which
         case the (i,j) entry is returned, slices, or mixed tuples (a,b)
-        where a and b are any combintion of slices and integers."""
+        where a and b are any combination of slices and integers."""
         raise NotImplementedError("Subclasses must implement this.")
 
     def __len__(self):
diff --git a/sympy/parsing/sympy_parser.py b/sympy/parsing/sympy_parser.py
index 9b4c721167a1..daee583bccf6 100644
--- a/sympy/parsing/sympy_parser.py
+++ b/sympy/parsing/sympy_parser.py
@@ -914,7 +914,7 @@ def eval_expr(code, local_dict, global_dict):
 
 
 def parse_expr(s, local_dict=None, transformations=standard_transformations,
-               global_dict=None, evaluate=True):
+               global_dict=None, evaluate=True, all=None, implicit=None):
     """Converts the string ``s`` to a SymPy expression, in ``local_dict``
 
     Parameters
@@ -975,6 +975,62 @@ def parse_expr(s, local_dict=None, transformations=standard_transformations,
     >>> b.args
     (x, 1)
 
+    Note, however, that when these expressions are printed they will
+    appear the same:
+
+    >>> assert str(a) == str(b)
+
+    As a convenience, transformations can be seen by printing ``transformations``:
+
+    >>> from sympy.parsing.sympy_parser import transformations
+
+    >>> print(transformations)
+    0: lambda_notation
+    1: auto_symbol
+    2: repeated_decimals
+    3: auto_number
+    4: factorial_notation
+    5: implicit_multiplication_application
+    6: convert_xor
+    7: implicit_application
+    8: implicit_multiplication
+    9: convert_equals_signs
+    10: function_exponentiation
+    11: rationalize
+
+    The ``T`` object provides a way to select these transformations:
+
+    >>> from sympy.parsing.sympy_parser import T
+
+    If you print it, you will see the same list as shown above.
+
+    >>> str(T) == str(transformations)
+    True
+
+    Standard slicing will return a tuple of transformations:
+
+    >>> T[:5] == standard_transformations
+    True
+
+    So ``T`` can be used to specify the parsing transformations:
+
+    >>> parse_expr("2x", transformations=T[:5])
+    Traceback (most recent call last):
+    ...
+    SyntaxError: invalid syntax
+    >>> parse_expr("2x", transformations=T[:6])
+    2*x
+    >>> parse_expr('.3', transformations=T[3, 11])
+    3/10
+    >>> parse_expr('.3x', transformations=T[:])
+    3*x/10
+
+    As a further convenience, keywords `implicit` and `all`, when set
+    to True, will select 0-5 and all the transformations, respectively.
+
+    >>> parse_expr('.3x', all=True)
+    3*x/10
+
     See Also
     ========
 
@@ -994,7 +1050,18 @@ def parse_expr(s, local_dict=None, transformations=standard_transformations,
     elif not isinstance(global_dict, dict):
         raise TypeError('expecting global_dict to be a dict')
 
-    transformations = transformations or ()
+    if all:
+        if not (implicit is None and
+                transformations == standard_transformations):
+            raise ValueError('give transformations or use single selector keyword')
+        transformations =  T[:]
+    elif implicit:
+        if not (all is None and
+                transformations == standard_transformations):
+            raise ValueError('give transformations or use single selector keyword')
+        transformations = T[:6]
+    elif not transformations:
+        transformations = ()
     if transformations:
         if not iterable(transformations):
             raise TypeError(
@@ -1130,3 +1197,51 @@ def visit_Call(self, node):
         if isinstance(node.func, ast.Name) and node.func.id in self.functions:
             new_node.keywords.append(ast.keyword(arg='evaluate', value=ast.NameConstant(value=False, ctx=ast.Load())))
         return new_node
+
+
+_transformation = {  # items can be added but never re-ordered
+0: lambda_notation,
+1: auto_symbol,
+2: repeated_decimals,
+3: auto_number,
+4: factorial_notation,
+5: implicit_multiplication_application,
+6: convert_xor,
+7: implicit_application,
+8: implicit_multiplication,
+9: convert_equals_signs,
+10: function_exponentiation,
+11: rationalize}
+
+transformations = '\n'.join('%s: %s' % (i, func_name(f)) for i, f in _transformation.items())
+
+
+class _T():
+    """class to retrieve transformations from a given slice
+
+    EXAMPLES
+    ========
+
+    >>> from sympy.parsing.sympy_parser import T, standard_transformations
+    >>> assert T[:5] == standard_transformations
+    """
+    def __init__(self):
+        self.N = len(_transformation)
+
+    def __str__(self):
+        return transformations
+
+    def __getitem__(self, t):
+        if not type(t) is tuple:
+            t = (t,)
+        i = []
+        for ti in t:
+            if type(ti) is int:
+                i.append(range(self.N)[ti])
+            elif type(ti) is slice:
+                i.extend(list(range(*ti.indices(self.N))))
+            else:
+                raise TypeError('unexpected slice arg')
+        return tuple([_transformation[_] for _ in i])
+
+T = _T()
diff --git a/sympy/parsing/tests/test_sympy_parser.py b/sympy/parsing/tests/test_sympy_parser.py
index 3a0dea6eb394..b60e121818b9 100644
--- a/sympy/parsing/tests/test_sympy_parser.py
+++ b/sympy/parsing/tests/test_sympy_parser.py
@@ -13,8 +13,10 @@
 from sympy.parsing.sympy_parser import (
     parse_expr, standard_transformations, rationalize, TokenError,
     split_symbols, implicit_multiplication, convert_equals_signs,
-    convert_xor, function_exponentiation,
-    implicit_multiplication_application,
+    convert_xor, function_exponentiation, lambda_notation, auto_symbol,
+    repeated_decimals, implicit_multiplication_application,
+    auto_number, factorial_notation, implicit_application,
+    _transformation, T
     )
 
 
@@ -293,3 +295,26 @@ def test_issue_19501():
         standard_transformations +
         (implicit_multiplication_application,)))
     assert eq.free_symbols == {x}
+
+
+def test_parsing_definitions():
+    from sympy.abc import x
+    assert len(_transformation) == 12  # if this changes, extend below
+    assert _transformation[0] == lambda_notation
+    assert _transformation[1] == auto_symbol
+    assert _transformation[2] == repeated_decimals
+    assert _transformation[3] == auto_number
+    assert _transformation[4] == factorial_notation
+    assert _transformation[5] == implicit_multiplication_application
+    assert _transformation[6] == convert_xor
+    assert _transformation[7] == implicit_application
+    assert _transformation[8] == implicit_multiplication
+    assert _transformation[9] == convert_equals_signs
+    assert _transformation[10] == function_exponentiation
+    assert _transformation[11] == rationalize
+    assert T[:5] == T[0,1,2,3,4] == standard_transformations
+    t = _transformation
+    assert T[-1, 0] == (t[len(t) - 1], t[0])
+    assert T[:5, 8] == standard_transformations + (t[8],)
+    assert parse_expr('0.3x^2', all=True) == 3*x**2/10
+    assert parse_expr('sin 3x', implicit=True) == sin(3*x)


Code review comments:
diff_hunk: @@ -975,6 +975,62 @@ def parse_expr(s, local_dict=None, transformations=standard_transformations,
     >>> b.args
     (x, 1)
 
+    Note, however, that when these expressions are printed they will
+    appear the same:
+
+    >>> assert str(a) == str(b)
+
+    As a convenience, transformations can be seen by printing ``transformations``:
+
+    >>> from sympy.parsing.sympy_parser import transformations
+
+    >>> print(transformations)
+    0: lambda_notation
+    1: auto_symbol
+    2: repeated_decimals
+    3: auto_number
+    4: factorial_notation
+    5: implicit_multiplication_application
+    6: convert_xor
+    7: implicit_application
+    8: implicit_multiplication
+    9: convert_equals_signs
+    10: function_exponentiation
+    11: rationalize
+
+    The ``T`` object provides a way to select these transformations:
+
+    >>> from sympy.parsing.sympy_parser import T
+
+    If you print it, you will see the same list as shown above.
+
+    >>> str(T) == str(transformations)
+    True
+
+    Standard slicing will return a tuple of transformations:
+
+    >>> T[:5] == standard_transformations
+    True
+
+    So ``T`` can be used to specify the parsing transformations:
+
+    >>> parse_expr("2x", transformations=T[:5])
+    Traceback (most recent call last):
+    ...
+    SyntaxError: invalid syntax
+    >>> parse_expr("2x", transformations=T[:6])
+    2*x
+    >>> parse_expr('.3', transformations=T[3, 11])
+    3/10
+    >>> parse_expr('.3x', transformations=T[:])
+    3*x/10
+
+    As a further convenience, keywords `implicit` and `all`, when set
+    to True, will select 0-5 and all the transformations, respectively.
+
+    >>> parse_expr('.3x', all=True)
+    3*x/10
path: sympy/parsing/sympy_parser.py
text: @user1:
What about `parse_expr(..., transformations='all')`?