Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
be644b0
Remove redundant "SympifyError" from SympifyError message
asmeurer Apr 10, 2017
d9e96a7
Add safe flag to sympify()
asmeurer Apr 10, 2017
3e3d5bd
Merge branch 'master' into safe-sympify
asmeurer May 28, 2018
55357d8
Start safe_parser.py
asmeurer May 28, 2018
e645cfe
Allow specifying a reason in UnsafeSympifyError
asmeurer May 28, 2018
011067c
Add check_string_for_safety to safe_parser.py
asmeurer May 28, 2018
542f520
Rename disallowed_names to blacklisted_names
asmeurer May 28, 2018
838bf8a
Add locals and globals to the list of blacklisted names
asmeurer May 28, 2018
7ff0d83
Use check_string_for_safety for sympify(safe=True)
asmeurer May 28, 2018
f31f96c
Update the safety section of the sympify docstring
asmeurer May 28, 2018
9f62bca
Fix typo
asmeurer May 28, 2018
8203c74
Check parsed string for safety after performing token transformations
asmeurer May 28, 2018
473e884
Add Lambda AST nodes to safe whitelist
asmeurer May 28, 2018
cb8f362
Add some tests for the UnsafeSympifyError
asmeurer May 29, 2018
0dcaad1
Use os.path.normcase instead of sys_normcase in runtests
asmeurer May 29, 2018
e510ef3
Raise an exception if sympify(safe=False) is called from SymPy librar…
asmeurer May 29, 2018
4b2f26c
Change the str printer of Interval to not use the class constructors
asmeurer May 29, 2018
f37a8bb
Fix sympy_parser tests for safe parsing
asmeurer May 29, 2018
ae64ae2
Print S.true and S.false as just "true" and "false"
asmeurer May 29, 2018
3bf361c
Make ordinals objects not singletons
asmeurer May 29, 2018
2fe6128
Add Naturals, Naturals0, and UniversalSet to the 'from sympy import *…
asmeurer May 29, 2018
6e0a8a8
Add a comment about why the singleton names in namespace test is there
asmeurer May 29, 2018
023fbf8
Remove unused imports
asmeurer May 29, 2018
8c2b213
Fix doctesting of methods wrapped by fastcache
asmeurer May 29, 2018
c444637
Fix docstring of Basic.has
asmeurer May 30, 2018
f006798
Remove executable bit from files in sympy/vector/
asmeurer May 30, 2018
302d28d
Fix doctests
asmeurer May 30, 2018
0b27a33
Fix test function name
asmeurer May 30, 2018
6899fea
Remove unsafe parser test
asmeurer May 30, 2018
2353f14
Reset the monkeypatched code in the safe parsing test
asmeurer May 30, 2018
7dbfb18
Use inspect.isfunction instead of inspect.isroutine in doctest
asmeurer May 31, 2018
1eed28f
Remove "S." from singleton set str printers
asmeurer May 31, 2018
b600478
Check 'register' explicitly in the Singleton tests
asmeurer May 31, 2018
9774b0f
Test against singletons that haven't been installed yet
asmeurer May 31, 2018
d0e0e14
Add an exception for the other singleton from the test above
asmeurer May 31, 2018
5463b94
Allow the singleton class to be imported with 'from sympy import *'
asmeurer May 31, 2018
449d036
Add 'Integers' to 'from sympy import *'
asmeurer May 31, 2018
889b45f
Fix the printing tests for the fancy set singletons not printing with…
asmeurer May 31, 2018
cb24c1a
Fix doctests for fancyset singletons printing without "S."
asmeurer May 31, 2018
81b3491
Allow singletons in the physics module to not be imported with 'from …
asmeurer May 31, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions sympy/calculus/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ def continuous_domain(f, symbol, domain):
>>> from sympy.calculus.util import continuous_domain
>>> x = Symbol('x')
>>> continuous_domain(1/x, x, S.Reals)
Union(Interval.open(-oo, 0), Interval.open(0, oo))
Union(Interval(-oo, 0, left_open=True, right_open=True),
Interval(0, oo, left_open=True, right_open=True))
>>> continuous_domain(tan(x), x, Interval(0, pi))
Union(Interval.Ropen(0, pi/2), Interval.Lopen(pi/2, pi))
Union(Interval(0, pi/2, right_open=True), Interval(pi/2, pi, left_open=True))
>>> continuous_domain(sqrt(x - 2), x, Interval(-5, 5))
Interval(2, 5)
>>> continuous_domain(log(2*x - 1), x, S.Reals)
Interval.open(1/2, oo)
Interval(1/2, oo, left_open=True, right_open=True)

"""
from sympy.solvers.inequalities import solve_univariate_inequality
Expand Down Expand Up @@ -103,9 +104,10 @@ def function_range(f, symbol, domain):
>>> function_range(tan(x), x, Interval(-pi/2, pi/2))
Interval(-oo, oo)
>>> function_range(1/x, x, S.Reals)
Union(Interval.open(-oo, 0), Interval.open(0, oo))
Union(Interval(-oo, 0, left_open=True, right_open=True),
Interval(0, oo, left_open=True, right_open=True))
>>> function_range(exp(x), x, S.Reals)
Interval.open(0, oo)
Interval(0, oo, left_open=True, right_open=True)
>>> function_range(log(x), x, S.Reals)
Interval(-oo, oo)
>>> function_range(sqrt(x), x , Interval(-5, 9))
Expand Down Expand Up @@ -227,7 +229,7 @@ def not_empty_in(finset_intersection, *syms):
>>> not_empty_in(FiniteSet(x, x**2).intersect(Interval(1, 2)), x)
Union(Interval(-sqrt(2), -1), Interval(1, 2))
>>> not_empty_in(FiniteSet(x**2/(x + 2)).intersect(Interval(1, oo)), x)
Union(Interval.Lopen(-2, -1), Interval(2, oo))
Union(Interval(-2, -1, left_open=True), Interval(2, oo))
"""

# TODO: handle piecewise defined functions
Expand Down
2 changes: 1 addition & 1 deletion sympy/core/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ def has(self, *patterns):

>>> from sympy.sets import Interval
>>> i = Interval.Lopen(0, 5); i
Interval.Lopen(0, 5)
Interval(0, 5, left_open=True)
>>> i.args
(0, 5, True, False)
>>> i.has(4) # there is no "4" in the arguments
Expand Down
34 changes: 34 additions & 0 deletions sympy/core/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def u_decode(x):
exec_=getattr(builtins, "exec")

range=range

from inspect import unwrap
else:
import codecs
import types
Expand Down Expand Up @@ -137,6 +139,38 @@ def exec_(_code_, _globs_=None, _locs_=None):
exec("exec _code_ in _globs_, _locs_")
range=xrange

def unwrap(func, stop=None):
"""Get the object wrapped by *func*.

Follows the chain of :attr:`__wrapped__` attributes returning the last
object in the chain.

*stop* is an optional callback accepting an object in the wrapper chain
as its sole argument that allows the unwrapping to be terminated early if
the callback returns a true value. If the callback never returns a true
value, the last object in the chain is returned as usual. For example,
:func:`signature` uses this to stop unwrapping if any object in the
chain has a ``__signature__`` attribute defined.

:exc:`ValueError` is raised if a cycle is encountered.

"""
if stop is None:
def _is_wrapper(f):
return hasattr(f, '__wrapped__')
else:
def _is_wrapper(f):
return hasattr(f, '__wrapped__') and not stop(f)
f = func # remember the original func for error reporting
memo = {id(f)} # Memoise by id to tolerate non-hashable objects
while _is_wrapper(func):
func = func.__wrapped__
id_func = id(func)
if id_func in memo:
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
memo.add(id_func)
return func

def with_metaclass(meta, *bases):
"""
Create a base class with a metaclass.
Expand Down
6 changes: 3 additions & 3 deletions sympy/core/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def nargs(self):
numbers is returned:

>>> Function('f').nargs
S.Naturals0
Naturals0

If the function was initialized to accept one or more arguments, a
corresponding set will be returned:
Expand All @@ -209,7 +209,7 @@ def nargs(self):

>>> f = Function('f')
>>> f(1).nargs
S.Naturals0
Naturals0
>>> len(f(1).args)
1
"""
Expand Down Expand Up @@ -843,7 +843,7 @@ class WildFunction(Function, AtomicExpr):
>>> F = WildFunction('F')
>>> f = Function('f')
>>> F.nargs
S.Naturals0
Naturals0
>>> x.match(F)
>>> F.match(F)
{F_: F_}
Expand Down
78 changes: 72 additions & 6 deletions sympy/core/sympify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import print_function, division

from inspect import getmro
from inspect import getmro, currentframe, getmodule

from .core import all_classes as sympy_classes
from .compatibility import iterable, string_types, range
Expand All @@ -16,12 +16,24 @@ def __init__(self, expr, base_exc=None):

def __str__(self):
if self.base_exc is None:
return "SympifyError: %r" % (self.expr,)
return "%r" % (self.expr,)

return ("Sympify of expression '%s' failed, because of exception being "
"raised:\n%s: %s" % (self.expr, self.base_exc.__class__.__name__,
str(self.base_exc)))

class UnsafeSympifyError(SympifyError):
def __init__(self, expr, base_exc=None, reason=None):
self.expr = expr
self.base_exc = base_exc
self.reason = reason

def __str__(self):
msg = "%r cannot be parsed safely" % (self.expr,)
if self.reason:
msg += '. Reason: %s.' % (self.reason,)
return msg

converter = {} # See sympify docstring.

class CantSympify(object):
Expand Down Expand Up @@ -74,8 +86,9 @@ def _convert_numpy_types(a):


def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
evaluate=None):
"""Converts an arbitrary expression to a type that can be used inside SymPy.
evaluate=None, safe=True):
"""
Converts an arbitrary expression to a type that can be used inside SymPy.

For example, it will convert Python ints into instances of sympy.Integer,
floats into instances of sympy.Float, etc. It is also able to coerce symbolic
Expand All @@ -91,7 +104,7 @@ def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,

.. warning::
Note that this function uses ``eval``, and thus shouldn't be used on
unsanitized input.
unsanitized input. See the :ref:`Safety <sympify-safety>` discussion below.

If the argument is already a type that SymPy understands, it will do
nothing but return that value. This can be used at the beginning of a
Expand Down Expand Up @@ -199,6 +212,43 @@ def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
>>> sympify('2**2 / 3 + 5', evaluate=False)
2**2/3 + 5

.. _sympify-safety:

Safety
------

When parsing a string, ``sympify`` parses it, applies some
transformations, and uses ``eval`` to convert it to a SymPy Python object.
However, ``eval`` is `known
<https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html>`_ to
be unsafe to use on untrusted arbitrary input,

If the option ``safe`` is set to ``True`` (the default), expressions that
are considered unsafe to parse raise ``UnsafeSympifyError``. The current
implementation implements an AST node whitelist and a name blacklist.
There are some important caveats here, however.

- It may still be possible to construct "safe" expressions that crash the
Python interpreter or cause it to run out of memory, for instance, a
numeric expression that when evaluated produces a number that cannot fit
in memory. The current implementation does not attempt to protect
against these scenarios.

- The safe parsing is only safe if the default locals=None is used. If
user-defined names can be added to the locals dictionary, the user could
simply redefine the ``eval`` function and bypass the name blacklisting.

- We cannot guarantee that there won't be ways to bypass the safety checks
and run arbitrary code. It is still highly recommended to properly
sandbox your code if you will be passing arbitrary untrusted input to
sympify.

>>> sympify('a.b')
Traceback (most recent call last):
...
sympy.core.sympify.UnsafeSympifyError: 'a.b' cannot be parsed safely.
Reason: Non-whitelisted AST node <_ast.Attribute object at ...> found.

Extending
---------

Expand Down Expand Up @@ -257,6 +307,21 @@ def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
-2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1

"""
if not safe:
# Make sure no library code disables the safe flag. Otherwise, it
# potentially could be used to bypass it, defeating the purpose.

# Note: by its nature, this makes it so that safe=False cannot be
# tested, so if this code is modified, be sure to test it manually.
frame = currentframe()
mod = getmodule(getattr(frame, 'f_back'))
if not mod:
# Some alt-Python implementations don't support currentframe(), in
# which case the above returns None.
pass
elif hasattr(mod, '__name__') and mod.__name__ == 'sympy' or mod.__name__.startswith('sympy.'):
raise RuntimeError("Setting sympify(safe=True) in SymPy library code is not allowed")

if evaluate is None:
if global_evaluate[0] is False:
evaluate = global_evaluate[0]
Expand Down Expand Up @@ -365,7 +430,8 @@ def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,

try:
a = a.replace('\n', '')
expr = parse_expr(a, local_dict=locals, transformations=transformations, evaluate=evaluate)
expr = parse_expr(a, local_dict=locals,
transformations=transformations, evaluate=evaluate, safe=safe)
except (TokenError, SyntaxError) as exc:
raise SympifyError('could not parse %r' % a, exc)

Expand Down
30 changes: 2 additions & 28 deletions sympy/core/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import sys

from sympy.core.basic import Basic, Atom, preorder_traversal, as_Basic
from sympy.core.singleton import S, Singleton
from sympy.core.singleton import S
from sympy.core.symbol import symbols
from sympy.core.compatibility import default_sort_key, with_metaclass
from sympy.core.compatibility import default_sort_key

from sympy import sin, Lambda, Q, cos, gamma, Tuple
from sympy.functions.elementary.exponential import exp
Expand Down Expand Up @@ -140,32 +140,6 @@ def test_xreplace():
raises(TypeError, lambda: b1.xreplace([b1, b2]))


def test_Singleton():
global instantiated
instantiated = 0

class MySingleton(with_metaclass(Singleton, Basic)):
def __new__(cls):
global instantiated
instantiated += 1
return Basic.__new__(cls)

assert instantiated == 0
MySingleton() # force instantiation
assert instantiated == 1
assert MySingleton() is not Basic()
assert MySingleton() is MySingleton()
assert S.MySingleton is MySingleton()
assert instantiated == 1

class MySingleton_sub(MySingleton):
pass
assert instantiated == 1
MySingleton_sub()
assert instantiated == 2
assert MySingleton_sub() is not MySingleton()
assert MySingleton_sub() is MySingleton_sub()


def test_preorder_traversal():
expr = Basic(b21, b3)
Expand Down
76 changes: 76 additions & 0 deletions sympy/core/tests/test_singleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from sympy.core.basic import Basic
from sympy.core.numbers import Rational
from sympy.core.singleton import S, Singleton, SingletonRegistry

from sympy.core.compatibility import with_metaclass, exec_

def test_Singleton():
global instantiated
instantiated = 0

class MySingleton(with_metaclass(Singleton, Basic)):
def __new__(cls):
global instantiated
instantiated += 1
return Basic.__new__(cls)

assert instantiated == 0
MySingleton() # force instantiation
assert instantiated == 1
assert MySingleton() is not Basic()
assert MySingleton() is MySingleton()
assert S.MySingleton is MySingleton()
assert instantiated == 1

class MySingleton_sub(MySingleton):
pass
assert instantiated == 1
MySingleton_sub()
assert instantiated == 2
assert MySingleton_sub() is not MySingleton()
assert MySingleton_sub() is MySingleton_sub()

def test_names_in_namespace():
# Every singleton name should be accessible from the 'from sympy import *'
# namespace in addition to the S object. However, it does not need to be
# by the same name (e.g., oo instead of S.Infinity).

# As a general rule, things should only be added to the singleton registry
# if they are used often enough that code can benefit either from the
# performance benefit of being able to use 'is' (this only matters in very
# tight loops), or from the memory savings of having exactly one instance
# (this matters for the numbers singletons, but very little else). The
# singleton registry is already a bit overpopulated, and things cannot be
# removed from it without breaking backwards compatibility. So if you got
# here by adding something new to the singletons, ask yourself if it
# really needs to be singletonized. Note that SymPy classes compare to one
# another just fine, so Class() == Class() will give True even if each
# Class() returns a new instance. Having unique instances is only
# necessary for the above noted performance gains. It should not be needed
# for any behavioral purposes.

# If you determine that something really should be a singleton, it must be
# accessible to sympify() without using 'S' (hence this test). Also, its
# str printer should print a form that does not use S. This is because
# sympify() disables attribute lookups by default for safety purposes.
d = {}
exec_('from sympy import *', d)

for name in dir(S) + list(S._classes_to_install):
if name.startswith('_'):
continue
if name == 'register':
continue
if isinstance(getattr(S, name), Rational):
continue
if getattr(S, name).__module__.startswith('sympy.physics'):
continue
if name in ['MySingleton', 'MySingleton_sub']:
# From the test above
continue
if name == 'NegativeInfinity':
# Accessible by -oo
continue

# Use is here because of complications with ==
assert any(getattr(S, name) is i or type(getattr(S, name)) is i for i in d.values()), name
Loading