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.
566 lines
16 KiB
566 lines
16 KiB
"""Miscellaneous stuff that does not really fit anywhere else."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import operator
|
|
import sys
|
|
import os
|
|
import re as _re
|
|
import struct
|
|
from textwrap import fill, dedent
|
|
|
|
|
|
class Undecidable(ValueError):
|
|
# an error to be raised when a decision cannot be made definitively
|
|
# where a definitive answer is needed
|
|
pass
|
|
|
|
|
|
def filldedent(s, w=70, **kwargs):
|
|
"""
|
|
Strips leading and trailing empty lines from a copy of ``s``, then dedents,
|
|
fills and returns it.
|
|
|
|
Empty line stripping serves to deal with docstrings like this one that
|
|
start with a newline after the initial triple quote, inserting an empty
|
|
line at the beginning of the string.
|
|
|
|
Additional keyword arguments will be passed to ``textwrap.fill()``.
|
|
|
|
See Also
|
|
========
|
|
strlines, rawlines
|
|
|
|
"""
|
|
return '\n' + fill(dedent(str(s)).strip('\n'), width=w, **kwargs)
|
|
|
|
|
|
def strlines(s, c=64, short=False):
|
|
"""Return a cut-and-pastable string that, when printed, is
|
|
equivalent to the input. The lines will be surrounded by
|
|
parentheses and no line will be longer than c (default 64)
|
|
characters. If the line contains newlines characters, the
|
|
`rawlines` result will be returned. If ``short`` is True
|
|
(default is False) then if there is one line it will be
|
|
returned without bounding parentheses.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import strlines
|
|
>>> q = 'this is a long string that should be broken into shorter lines'
|
|
>>> print(strlines(q, 40))
|
|
(
|
|
'this is a long string that should be b'
|
|
'roken into shorter lines'
|
|
)
|
|
>>> q == (
|
|
... 'this is a long string that should be b'
|
|
... 'roken into shorter lines'
|
|
... )
|
|
True
|
|
|
|
See Also
|
|
========
|
|
filldedent, rawlines
|
|
"""
|
|
if not isinstance(s, str):
|
|
raise ValueError('expecting string input')
|
|
if '\n' in s:
|
|
return rawlines(s)
|
|
q = '"' if repr(s).startswith('"') else "'"
|
|
q = (q,)*2
|
|
if '\\' in s: # use r-string
|
|
m = '(\nr%s%%s%s\n)' % q
|
|
j = '%s\nr%s' % q
|
|
c -= 3
|
|
else:
|
|
m = '(\n%s%%s%s\n)' % q
|
|
j = '%s\n%s' % q
|
|
c -= 2
|
|
out = []
|
|
while s:
|
|
out.append(s[:c])
|
|
s=s[c:]
|
|
if short and len(out) == 1:
|
|
return (m % out[0]).splitlines()[1] # strip bounding (\n...\n)
|
|
return m % j.join(out)
|
|
|
|
|
|
def rawlines(s):
|
|
"""Return a cut-and-pastable string that, when printed, is equivalent
|
|
to the input. Use this when there is more than one line in the
|
|
string. The string returned is formatted so it can be indented
|
|
nicely within tests; in some cases it is wrapped in the dedent
|
|
function which has to be imported from textwrap.
|
|
|
|
Examples
|
|
========
|
|
|
|
Note: because there are characters in the examples below that need
|
|
to be escaped because they are themselves within a triple quoted
|
|
docstring, expressions below look more complicated than they would
|
|
be if they were printed in an interpreter window.
|
|
|
|
>>> from sympy.utilities.misc import rawlines
|
|
>>> from sympy import TableForm
|
|
>>> s = str(TableForm([[1, 10]], headings=(None, ['a', 'bee'])))
|
|
>>> print(rawlines(s))
|
|
(
|
|
'a bee\\n'
|
|
'-----\\n'
|
|
'1 10 '
|
|
)
|
|
>>> print(rawlines('''this
|
|
... that'''))
|
|
dedent('''\\
|
|
this
|
|
that''')
|
|
|
|
>>> print(rawlines('''this
|
|
... that
|
|
... '''))
|
|
dedent('''\\
|
|
this
|
|
that
|
|
''')
|
|
|
|
>>> s = \"\"\"this
|
|
... is a triple '''
|
|
... \"\"\"
|
|
>>> print(rawlines(s))
|
|
dedent(\"\"\"\\
|
|
this
|
|
is a triple '''
|
|
\"\"\")
|
|
|
|
>>> print(rawlines('''this
|
|
... that
|
|
... '''))
|
|
(
|
|
'this\\n'
|
|
'that\\n'
|
|
' '
|
|
)
|
|
|
|
See Also
|
|
========
|
|
filldedent, strlines
|
|
"""
|
|
lines = s.split('\n')
|
|
if len(lines) == 1:
|
|
return repr(lines[0])
|
|
triple = ["'''" in s, '"""' in s]
|
|
if any(li.endswith(' ') for li in lines) or '\\' in s or all(triple):
|
|
rv = []
|
|
# add on the newlines
|
|
trailing = s.endswith('\n')
|
|
last = len(lines) - 1
|
|
for i, li in enumerate(lines):
|
|
if i != last or trailing:
|
|
rv.append(repr(li + '\n'))
|
|
else:
|
|
rv.append(repr(li))
|
|
return '(\n %s\n)' % '\n '.join(rv)
|
|
else:
|
|
rv = '\n '.join(lines)
|
|
if triple[0]:
|
|
return 'dedent("""\\\n %s""")' % rv
|
|
else:
|
|
return "dedent('''\\\n %s''')" % rv
|
|
|
|
ARCH = str(struct.calcsize('P') * 8) + "-bit"
|
|
|
|
|
|
# XXX: PyPy does not support hash randomization
|
|
HASH_RANDOMIZATION = getattr(sys.flags, 'hash_randomization', False)
|
|
|
|
_debug_tmp: list[str] = []
|
|
_debug_iter = 0
|
|
|
|
def debug_decorator(func):
|
|
"""If SYMPY_DEBUG is True, it will print a nice execution tree with
|
|
arguments and results of all decorated functions, else do nothing.
|
|
"""
|
|
from sympy import SYMPY_DEBUG
|
|
|
|
if not SYMPY_DEBUG:
|
|
return func
|
|
|
|
def maketree(f, *args, **kw):
|
|
global _debug_tmp
|
|
global _debug_iter
|
|
oldtmp = _debug_tmp
|
|
_debug_tmp = []
|
|
_debug_iter += 1
|
|
|
|
def tree(subtrees):
|
|
def indent(s, variant=1):
|
|
x = s.split("\n")
|
|
r = "+-%s\n" % x[0]
|
|
for a in x[1:]:
|
|
if a == "":
|
|
continue
|
|
if variant == 1:
|
|
r += "| %s\n" % a
|
|
else:
|
|
r += " %s\n" % a
|
|
return r
|
|
if len(subtrees) == 0:
|
|
return ""
|
|
f = []
|
|
for a in subtrees[:-1]:
|
|
f.append(indent(a))
|
|
f.append(indent(subtrees[-1], 2))
|
|
return ''.join(f)
|
|
|
|
# If there is a bug and the algorithm enters an infinite loop, enable the
|
|
# following lines. It will print the names and parameters of all major functions
|
|
# that are called, *before* they are called
|
|
#from functools import reduce
|
|
#print("%s%s %s%s" % (_debug_iter, reduce(lambda x, y: x + y, \
|
|
# map(lambda x: '-', range(1, 2 + _debug_iter))), f.__name__, args))
|
|
|
|
r = f(*args, **kw)
|
|
|
|
_debug_iter -= 1
|
|
s = "%s%s = %s\n" % (f.__name__, args, r)
|
|
if _debug_tmp != []:
|
|
s += tree(_debug_tmp)
|
|
_debug_tmp = oldtmp
|
|
_debug_tmp.append(s)
|
|
if _debug_iter == 0:
|
|
print(_debug_tmp[0])
|
|
_debug_tmp = []
|
|
return r
|
|
|
|
def decorated(*args, **kwargs):
|
|
return maketree(func, *args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
|
|
def debug(*args):
|
|
"""
|
|
Print ``*args`` if SYMPY_DEBUG is True, else do nothing.
|
|
"""
|
|
from sympy import SYMPY_DEBUG
|
|
if SYMPY_DEBUG:
|
|
print(*args, file=sys.stderr)
|
|
|
|
|
|
def debugf(string, args):
|
|
"""
|
|
Print ``string%args`` if SYMPY_DEBUG is True, else do nothing. This is
|
|
intended for debug messages using formatted strings.
|
|
"""
|
|
from sympy import SYMPY_DEBUG
|
|
if SYMPY_DEBUG:
|
|
print(string%args, file=sys.stderr)
|
|
|
|
|
|
def find_executable(executable, path=None):
|
|
"""Try to find 'executable' in the directories listed in 'path' (a
|
|
string listing directories separated by 'os.pathsep'; defaults to
|
|
os.environ['PATH']). Returns the complete filename or None if not
|
|
found
|
|
"""
|
|
from .exceptions import sympy_deprecation_warning
|
|
sympy_deprecation_warning(
|
|
"""
|
|
sympy.utilities.misc.find_executable() is deprecated. Use the standard
|
|
library shutil.which() function instead.
|
|
""",
|
|
deprecated_since_version="1.7",
|
|
active_deprecations_target="deprecated-find-executable",
|
|
)
|
|
if path is None:
|
|
path = os.environ['PATH']
|
|
paths = path.split(os.pathsep)
|
|
extlist = ['']
|
|
if os.name == 'os2':
|
|
(base, ext) = os.path.splitext(executable)
|
|
# executable files on OS/2 can have an arbitrary extension, but
|
|
# .exe is automatically appended if no dot is present in the name
|
|
if not ext:
|
|
executable = executable + ".exe"
|
|
elif sys.platform == 'win32':
|
|
pathext = os.environ['PATHEXT'].lower().split(os.pathsep)
|
|
(base, ext) = os.path.splitext(executable)
|
|
if ext.lower() not in pathext:
|
|
extlist = pathext
|
|
for ext in extlist:
|
|
execname = executable + ext
|
|
if os.path.isfile(execname):
|
|
return execname
|
|
else:
|
|
for p in paths:
|
|
f = os.path.join(p, execname)
|
|
if os.path.isfile(f):
|
|
return f
|
|
|
|
return None
|
|
|
|
|
|
def func_name(x, short=False):
|
|
"""Return function name of `x` (if defined) else the `type(x)`.
|
|
If short is True and there is a shorter alias for the result,
|
|
return the alias.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import func_name
|
|
>>> from sympy import Matrix
|
|
>>> from sympy.abc import x
|
|
>>> func_name(Matrix.eye(3))
|
|
'MutableDenseMatrix'
|
|
>>> func_name(x < 1)
|
|
'StrictLessThan'
|
|
>>> func_name(x < 1, short=True)
|
|
'Lt'
|
|
"""
|
|
alias = {
|
|
'GreaterThan': 'Ge',
|
|
'StrictGreaterThan': 'Gt',
|
|
'LessThan': 'Le',
|
|
'StrictLessThan': 'Lt',
|
|
'Equality': 'Eq',
|
|
'Unequality': 'Ne',
|
|
}
|
|
typ = type(x)
|
|
if str(typ).startswith("<type '"):
|
|
typ = str(typ).split("'")[1].split("'")[0]
|
|
elif str(typ).startswith("<class '"):
|
|
typ = str(typ).split("'")[1].split("'")[0]
|
|
rv = getattr(getattr(x, 'func', x), '__name__', typ)
|
|
if '.' in rv:
|
|
rv = rv.split('.')[-1]
|
|
if short:
|
|
rv = alias.get(rv, rv)
|
|
return rv
|
|
|
|
|
|
def _replace(reps):
|
|
"""Return a function that can make the replacements, given in
|
|
``reps``, on a string. The replacements should be given as mapping.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import _replace
|
|
>>> f = _replace(dict(foo='bar', d='t'))
|
|
>>> f('food')
|
|
'bart'
|
|
>>> f = _replace({})
|
|
>>> f('food')
|
|
'food'
|
|
"""
|
|
if not reps:
|
|
return lambda x: x
|
|
D = lambda match: reps[match.group(0)]
|
|
pattern = _re.compile("|".join(
|
|
[_re.escape(k) for k, v in reps.items()]), _re.M)
|
|
return lambda string: pattern.sub(D, string)
|
|
|
|
|
|
def replace(string, *reps):
|
|
"""Return ``string`` with all keys in ``reps`` replaced with
|
|
their corresponding values, longer strings first, irrespective
|
|
of the order they are given. ``reps`` may be passed as tuples
|
|
or a single mapping.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import replace
|
|
>>> replace('foo', {'oo': 'ar', 'f': 'b'})
|
|
'bar'
|
|
>>> replace("spamham sha", ("spam", "eggs"), ("sha","md5"))
|
|
'eggsham md5'
|
|
|
|
There is no guarantee that a unique answer will be
|
|
obtained if keys in a mapping overlap (i.e. are the same
|
|
length and have some identical sequence at the
|
|
beginning/end):
|
|
|
|
>>> reps = [
|
|
... ('ab', 'x'),
|
|
... ('bc', 'y')]
|
|
>>> replace('abc', *reps) in ('xc', 'ay')
|
|
True
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string
|
|
"""
|
|
if len(reps) == 1:
|
|
kv = reps[0]
|
|
if isinstance(kv, dict):
|
|
reps = kv
|
|
else:
|
|
return string.replace(*kv)
|
|
else:
|
|
reps = dict(reps)
|
|
return _replace(reps)(string)
|
|
|
|
|
|
def translate(s, a, b=None, c=None):
|
|
"""Return ``s`` where characters have been replaced or deleted.
|
|
|
|
SYNTAX
|
|
======
|
|
|
|
translate(s, None, deletechars):
|
|
all characters in ``deletechars`` are deleted
|
|
translate(s, map [,deletechars]):
|
|
all characters in ``deletechars`` (if provided) are deleted
|
|
then the replacements defined by map are made; if the keys
|
|
of map are strings then the longer ones are handled first.
|
|
Multicharacter deletions should have a value of ''.
|
|
translate(s, oldchars, newchars, deletechars)
|
|
all characters in ``deletechars`` are deleted
|
|
then each character in ``oldchars`` is replaced with the
|
|
corresponding character in ``newchars``
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import translate
|
|
>>> abc = 'abc'
|
|
>>> translate(abc, None, 'a')
|
|
'bc'
|
|
>>> translate(abc, {'a': 'x'}, 'c')
|
|
'xb'
|
|
>>> translate(abc, {'abc': 'x', 'a': 'y'})
|
|
'x'
|
|
|
|
>>> translate('abcd', 'ac', 'AC', 'd')
|
|
'AbC'
|
|
|
|
There is no guarantee that a unique answer will be
|
|
obtained if keys in a mapping overlap are the same
|
|
length and have some identical sequences at the
|
|
beginning/end:
|
|
|
|
>>> translate(abc, {'ab': 'x', 'bc': 'y'}) in ('xc', 'ay')
|
|
True
|
|
"""
|
|
|
|
mr = {}
|
|
if a is None:
|
|
if c is not None:
|
|
raise ValueError('c should be None when a=None is passed, instead got %s' % c)
|
|
if b is None:
|
|
return s
|
|
c = b
|
|
a = b = ''
|
|
else:
|
|
if isinstance(a, dict):
|
|
short = {}
|
|
for k in list(a.keys()):
|
|
if len(k) == 1 and len(a[k]) == 1:
|
|
short[k] = a.pop(k)
|
|
mr = a
|
|
c = b
|
|
if short:
|
|
a, b = [''.join(i) for i in list(zip(*short.items()))]
|
|
else:
|
|
a = b = ''
|
|
elif len(a) != len(b):
|
|
raise ValueError('oldchars and newchars have different lengths')
|
|
|
|
if c:
|
|
val = str.maketrans('', '', c)
|
|
s = s.translate(val)
|
|
s = replace(s, mr)
|
|
n = str.maketrans(a, b)
|
|
return s.translate(n)
|
|
|
|
|
|
def ordinal(num):
|
|
"""Return ordinal number string of num, e.g. 1 becomes 1st.
|
|
"""
|
|
# modified from https://codereview.stackexchange.com/questions/41298/producing-ordinal-numbers
|
|
n = as_int(num)
|
|
k = abs(n) % 100
|
|
if 11 <= k <= 13:
|
|
suffix = 'th'
|
|
elif k % 10 == 1:
|
|
suffix = 'st'
|
|
elif k % 10 == 2:
|
|
suffix = 'nd'
|
|
elif k % 10 == 3:
|
|
suffix = 'rd'
|
|
else:
|
|
suffix = 'th'
|
|
return str(n) + suffix
|
|
|
|
|
|
def as_int(n, strict=True):
|
|
"""
|
|
Convert the argument to a builtin integer.
|
|
|
|
The return value is guaranteed to be equal to the input. ValueError is
|
|
raised if the input has a non-integral value. When ``strict`` is True, this
|
|
uses `__index__ <https://docs.python.org/3/reference/datamodel.html#object.__index__>`_
|
|
and when it is False it uses ``int``.
|
|
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.utilities.misc import as_int
|
|
>>> from sympy import sqrt, S
|
|
|
|
The function is primarily concerned with sanitizing input for
|
|
functions that need to work with builtin integers, so anything that
|
|
is unambiguously an integer should be returned as an int:
|
|
|
|
>>> as_int(S(3))
|
|
3
|
|
|
|
Floats, being of limited precision, are not assumed to be exact and
|
|
will raise an error unless the ``strict`` flag is False. This
|
|
precision issue becomes apparent for large floating point numbers:
|
|
|
|
>>> big = 1e23
|
|
>>> type(big) is float
|
|
True
|
|
>>> big == int(big)
|
|
True
|
|
>>> as_int(big)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: ... is not an integer
|
|
>>> as_int(big, strict=False)
|
|
99999999999999991611392
|
|
|
|
Input that might be a complex representation of an integer value is
|
|
also rejected by default:
|
|
|
|
>>> one = sqrt(3 + 2*sqrt(2)) - sqrt(2)
|
|
>>> int(one) == 1
|
|
True
|
|
>>> as_int(one)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: ... is not an integer
|
|
"""
|
|
if strict:
|
|
try:
|
|
if isinstance(n, bool):
|
|
raise TypeError
|
|
return operator.index(n)
|
|
except TypeError:
|
|
raise ValueError('%s is not an integer' % (n,))
|
|
else:
|
|
try:
|
|
result = int(n)
|
|
except TypeError:
|
|
raise ValueError('%s is not an integer' % (n,))
|
|
if n != result:
|
|
raise ValueError('%s is not an integer' % (n,))
|
|
return result
|