Carlos muniz/symmath removal unrevert (#29912)

* Revert "Merge pull request #29909 from openedx/revert-29869-Carlos-Muniz/symmath-removal"

This reverts commit 8c0db8ddff, reversing
changes made to 1156c62014.

* fix: Remove misplaced `-e`

`-e` was wrongfully placed in front of `common/lib/sandbox-packages`,
which may have most likely been causing the edxAPP to break.

* fix: Change regex to apply to right dirs

`py38.txt` does not include `common/lib/xmodule` so it doesn't match the
regex. Therefore, it never got its lines in `common/lib/sandbox-packages`
fixed. If we change the regex to match any `common/lib/<packagename>` it
should work correctly.
This commit is contained in:
Carlos Muniz
2022-02-14 10:26:40 -05:00
committed by GitHub
parent fea9625e0f
commit 8e8d8404be
18 changed files with 37 additions and 1216 deletions

View File

@@ -1,11 +0,0 @@
# lint-amnesty, pylint: disable=missing-module-docstring
from setuptools import setup
setup(
name="symmath",
version="0.3",
packages=["symmath"],
install_requires=[
"sympy",
],
)

View File

@@ -1,30 +0,0 @@
(Originally written by Ike.)
At a high level, the main challenges of checking symbolic math expressions are
(1) making sure the expression is mathematically legal, and (2) simplifying the
expression for comparison with what is expected.
(1) Generation (and testing) of legal input is done by using MathJax to provide
input math in an XML format known as Presentation MathML (PMathML). Such
expressions typeset correctly, but may not be mathematically legal, like "5 /
(1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is
by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module
in SnuggleTeX. CMathML is then converted into a sympy expression. This work is
all done in `symmath/formula.py`.
(2) Simplifying the expression and checking against what is expected is done by
using sympy, and a set of heuristics based on options flags provided by the
problem author. For example, the problem author may specify that the expected
expression is a matrix, in which case the dimensionality of the input
expression is checked. Other options include specifying that the comparison be
checked numerically in addition to symbolically. The checking is done in
stages, first with no simplification, then with increasing levels of testing;
if a match is found at any stage, then an "ok" is returned. Helpful messages
are also returned, eg if the input expression is of a different type than the
expected. This work is all done in `symmath/symmath_check.py`.
Links:
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
MathML: http://www.w3.org/TR/MathML2/overview.html
SymPy: http://sympy.org/en/index.html

View File

@@ -1,3 +0,0 @@
# lint-amnesty, pylint: disable=missing-module-docstring
from .formula import *
from .symmath_check import *

View File

@@ -1,588 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Flexible python representation of a symbolic mathematical formula.
Acceptes Presentation MathML, Content MathML (and could also do OpenMath).
Provides sympy representation.
"""
#
# File: formula.py
# Date: 04-May-12 (creation)
# Author: I. Chuang <ichuang@mit.edu>
#
import logging
import operator
import os
import re
import string
import unicodedata
#import subprocess
from copy import deepcopy
from functools import reduce
import six # lint-amnesty, pylint: disable=unused-import
import sympy
from lxml import etree
from sympy import latex, sympify
from sympy.physics.quantum.qubit import Qubit
from sympy.physics.quantum.state import Ket
from sympy.printing.latex import LatexPrinter
from sympy.printing.str import StrPrinter
from openedx.core.djangolib.markup import HTML
log = logging.getLogger(__name__)
log.warning("Dark code. Needs review before enabling in prod.")
os.environ['PYTHONIOENCODING'] = 'utf-8'
#-----------------------------------------------------------------------------
class dot(sympy.operations.LatticeOp): # pylint: disable=invalid-name, no-member
"""my dot product"""
zero = sympy.Symbol('dotzero')
identity = sympy.Symbol('dotidentity')
def _print_dot(_self, expr):
"""Print statement used for LatexPrinter"""
return r'{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1])
LatexPrinter._print_dot = _print_dot # pylint: disable=protected-access
#-----------------------------------------------------------------------------
# unit vectors (for 8.02)
def _print_hat(_self, expr):
"""Print statement used for LatexPrinter"""
return '\\hat{%s}' % str(expr.args[0]).lower()
LatexPrinter._print_hat = _print_hat # pylint: disable=protected-access
StrPrinter._print_hat = _print_hat # pylint: disable=protected-access
#-----------------------------------------------------------------------------
# helper routines
def to_latex(expr):
"""
Convert expression to latex mathjax format
"""
if expr is None:
return ''
expr_s = latex(expr)
expr_s = expr_s.replace(r'\XI', 'XI') # workaround for strange greek
# substitute back into latex form for scripts
# literally something of the form
# 'scriptN' becomes '\\mathcal{N}'
# note: can't use something akin to the _print_hat method above because we
# sometimes get 'script(N)__B' or more complicated terms
expr_s = re.sub(
r'script([a-zA-Z0-9]+)',
r'\\mathcal{\\1}',
expr_s
)
#return '<math>%s{}{}</math>' % (xs[1:-1])
if expr_s[0] == '$':
return HTML('[mathjax]{expression}[/mathjax]<br>').format(expression=expr_s[1:-1]) # for sympy v6
return HTML('[mathjax]{expression}[/mathjax]<br>').format(expression=expr_s) # for sympy v7
def my_evalf(expr, chop=False):
"""
Enhanced sympy evalf to handle lists of expressions
and catch eval failures without dropping out.
"""
if isinstance(expr, list):
try:
return [x.evalf(chop=chop) for x in expr]
except Exception: # pylint: disable=broad-except
return expr
try:
return expr.evalf(chop=chop)
except Exception: # pylint: disable=broad-except
return expr
def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False, symtab=None):
"""
Version of sympify to import expression into sympy
"""
# make all lowercase real?
if symtab:
varset = symtab
else:
varset = {
'p': sympy.Symbol('p'),
'g': sympy.Symbol('g'),
'e': sympy.E, # for exp
'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
'N': sympy.Symbol('N'), # or it is some kind of sympy function
'ZZ': sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
'XI': sympy.Symbol('XI'), # otherwise it is the capital \XI
'hat': sympy.Function('hat'), # for unit vectors (8.02)
}
if do_qubit: # turn qubit(...) into Qubit instance
varset.update({
'qubit': Qubit,
'Ket': Ket,
'dot': dot,
'bit': sympy.Function('bit'),
})
if abcsym: # consider all lowercase letters as real symbols, in the parsing
for letter in string.ascii_lowercase:
if letter in varset: # exclude those already done
continue
varset.update({letter: sympy.Symbol(letter, real=True)})
sexpr = sympify(expr, locals=varset)
if normphase: # remove overall phase if sexpr is a list
if isinstance(sexpr, list):
if sexpr[0].is_number:
ophase = sympy.sympify('exp(-I*arg(%s))' % sexpr[0])
sexpr = [sympy.Mul(x, ophase) for x in sexpr]
def to_matrix(expr):
"""
Convert a list, or list of lists to a matrix.
"""
# if expr is a list of lists, and is rectangular, then return Matrix(expr)
if not isinstance(expr, list):
return expr
for row in expr:
if not isinstance(row, list):
return expr
rdim = len(expr[0])
for row in expr:
if not len(row) == rdim:
return expr
return sympy.Matrix(expr)
if matrix:
sexpr = to_matrix(sexpr)
return sexpr
#-----------------------------------------------------------------------------
# class for symbolic mathematical formulas
class formula(object):
"""
Representation of a mathematical formula object. Accepts mathml math expression
for constructing, and can produce sympy translation. The formula may or may not
include an assignment (=).
"""
def __init__(self, expr, asciimath='', options=None):
self.expr = expr.strip()
self.asciimath = asciimath
self.the_cmathml = None
self.the_sympy = None
self.options = options
def is_presentation_mathml(self):
"""
Check if formula is in mathml presentation format.
"""
return '<mstyle' in self.expr
def is_mathml(self):
"""
Check if formula is in mathml format.
"""
return '<math ' in self.expr
def fix_greek_in_mathml(self, xml):
"""
Recursively fix greek letters in passed in xml.
"""
def gettag(expr):
return re.sub('{http://[^}]+}', '', expr.tag)
for k in xml:
tag = gettag(k)
if tag == 'mi' or tag == 'ci': # lint-amnesty, pylint: disable=consider-using-in
usym = six.text_type(k.text)
try:
udata = unicodedata.name(usym)
except Exception: # pylint: disable=broad-except
udata = None
# print "usym = %s, udata=%s" % (usym,udata)
if udata: # eg "GREEK SMALL LETTER BETA"
if 'GREEK' in udata:
usym = udata.split(' ')[-1]
if 'SMALL' in udata:
usym = usym.lower()
#print "greek: ",usym
k.text = usym
self.fix_greek_in_mathml(k)
return xml
def preprocess_pmathml(self, xml): # lint-amnesty, pylint: disable=too-many-statements
r"""
Pre-process presentation MathML from ASCIIMathML to make it more
acceptable for SnuggleTeX, and also to accomodate some sympy
conventions (eg hat(i) for \hat{i}).
This method would be a good spot to look for an integral and convert
it, if possible...
"""
if isinstance(xml, (str, six.text_type)):
xml = etree.fromstring(xml) # TODO: wrap in try
xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii
def gettag(expr):
return re.sub('{http://[^}]+}', '', expr.tag)
def fix_pmathml(xml):
"""
f and g are processed as functions by asciimathml, eg "f-2" turns
into "<mrow><mi>f</mi><mo>-</mo></mrow><mn>2</mn>" this is
really terrible for turning into cmathml. undo this here.
"""
for k in xml:
tag = gettag(k)
if tag == 'mrow':
if len(k) == 2:
if gettag(k[0]) == 'mi' and k[0].text in ['f', 'g'] and gettag(k[1]) == 'mo':
idx = xml.index(k)
xml.insert(idx, deepcopy(k[0])) # drop the <mrow> container
xml.insert(idx + 1, deepcopy(k[1]))
xml.remove(k)
fix_pmathml(k)
fix_pmathml(xml)
def fix_hat(xml):
"""
hat i is turned into <mover><mi>i</mi><mo>^</mo></mover> ; mangle
this into <mi>hat(f)</mi> hat i also somtimes turned into
<mover><mrow> <mi>j</mi> </mrow><mo>^</mo></mover>
"""
for k in xml:
tag = gettag(k)
if tag == 'mover':
if len(k) == 2:
if gettag(k[0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0].text
xml.replace(k, newk)
if gettag(k[0]) == 'mrow' and gettag(k[0][0]) == 'mi' and \
gettag(k[1]) == 'mo' and str(k[1].text) == '^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0][0].text
xml.replace(k, newk)
fix_hat(k)
fix_hat(xml)
def flatten_pmathml(xml):
"""
Give the text version of certain PMathML elements
Sometimes MathML will be given with each letter separated (it
doesn't know if its implicit multiplication or what). From an xml
node, find the (text only) variable name it represents. So it takes
<mrow>
<mi>m</mi>
<mi>a</mi>
<mi>x</mi>
</mrow>
and returns 'max', for easier use later on.
"""
tag = gettag(xml)
if tag == 'mn':
return xml.text
elif tag == 'mi':
return xml.text
elif tag == 'mrow':
return ''.join([flatten_pmathml(y) for y in xml])
raise Exception('[flatten_pmathml] unknown tag %s' % tag)
def fix_mathvariant(parent):
"""
Fix certain kinds of math variants
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
with 'scriptN'. There have been problems using script_N or script(N)
"""
for child in parent:
if gettag(child) == 'mstyle' and child.get('mathvariant') == 'script':
newchild = etree.Element('mi')
newchild.text = 'script%s' % flatten_pmathml(child[0])
parent.replace(child, newchild)
fix_mathvariant(child)
fix_mathvariant(xml)
# find "tagged" superscripts
# they have the character \u200b in the superscript
# replace them with a__b so snuggle doesn't get confused
def fix_superscripts(xml):
""" Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
In the javascript, variables with '__X' in them had an invisible
character inserted into the sup (to distinguish from powers)
E.g. normal:
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mi>c</mi>
</msubsup>
to be interpreted '(a_b)^c' (nothing done by this method)
And modified:
<msubsup>
<mi>b</mi>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>d</mi>
</mrow>
</msubsup>
to be interpreted 'a_b__c'
also:
<msup>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>B</mi>
</mrow>
</msup>
to be 'x__B'
"""
for k in xml:
tag = gettag(k)
# match things like the last example--
# the second item in msub is an mrow with the first
# character equal to \u200b
if (
tag == 'msup' and
len(k) == 2 and gettag(k[1]) == 'mrow' and
gettag(k[1][0]) == 'mo' and k[1][0].text == '\u200b' # whew
):
# replace the msup with 'X__Y'
k[1].remove(k[1][0])
newk = etree.Element('mi')
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
xml.replace(k, newk)
# match things like the middle example-
# the third item in msubsup is an mrow with the first
# character equal to \u200b
if (
tag == 'msubsup' and
len(k) == 3 and gettag(k[2]) == 'mrow' and
gettag(k[2][0]) == 'mo' and k[2][0].text == '\u200b' # whew
):
# replace the msubsup with 'X_Y__Z'
k[2].remove(k[2][0])
newk = etree.Element('mi')
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
xml.replace(k, newk)
fix_superscripts(k)
fix_superscripts(xml)
def fix_msubsup(parent):
"""
Snuggle returns an error when it sees an <msubsup> replace such
elements with an <msup>, except the first element is of
the form a_b. I.e. map a_b^c => (a_b)^c
"""
for child in parent:
# fix msubsup
if gettag(child) == 'msubsup' and len(child) == 3:
newchild = etree.Element('msup')
newbase = etree.Element('mi')
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
newexp = child[2]
newchild.append(newbase)
newchild.append(newexp)
parent.replace(child, newchild)
fix_msubsup(child)
fix_msubsup(xml)
self.xml = xml # pylint: disable=attribute-defined-outside-init
return self.xml
def get_content_mathml(self): # lint-amnesty, pylint: disable=missing-function-docstring
if self.the_cmathml:
return self.the_cmathml
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
try:
xml = self.preprocess_pmathml(self.expr).decode('utf-8')
except Exception as err: # pylint: disable=broad-except
log.warning('Err %s while preprocessing; expr=%s', err, self.expr)
return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml, pretty_print=True)
self.the_pmathml = pmathml # pylint: disable=attribute-defined-outside-init
return self.the_pmathml
cmathml = property(get_content_mathml, None, None, 'content MathML representation')
def make_sympy(self, xml=None): # lint-amnesty, pylint: disable=too-many-statements
"""
Return sympy expression for the math formula.
The math formula is converted to Content MathML then that is parsed.
This is a recursive function, called on every CMML node. Support for
more functions can be added by modifying opdict, abould halfway down
"""
if self.the_sympy:
return self.the_sympy
if xml is None: # root
if not self.is_mathml():
return my_sympify(self.expr)
if self.is_presentation_mathml():
cmml = None
try:
cmml = self.cmathml
xml = etree.fromstring(str(cmml))
except Exception as err:
if 'conversion from Presentation MathML to Content MathML was not successful' in cmml: # lint-amnesty, pylint: disable=unsupported-membership-test
msg = "Illegal math expression"
else:
msg = 'Err %s while converting cmathml to xml; cmml=%s' % (err, cmml)
raise Exception(msg) # lint-amnesty, pylint: disable=raise-missing-from
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
else:
xml = etree.fromstring(self.expr)
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
return self.the_sympy
def gettag(expr):
return re.sub('{http://[^}]+}', '', expr.tag)
def op_plus(*args):
return args[0] if len(args) == 1 else op_plus(*args[:-1]) + args[-1]
def op_times(*args):
return reduce(operator.mul, args)
def op_minus(*args):
if len(args) == 1:
return -args[0]
if not len(args) == 2: # lint-amnesty, pylint: disable=unneeded-not
raise Exception('minus given wrong number of arguments!')
#return sympy.Add(args[0],-args[1])
return args[0] - args[1]
opdict = {
'plus': op_plus,
'divide': operator.div, # lint-amnesty, pylint: disable=no-member
'times': op_times,
'minus': op_minus,
'root': sympy.sqrt,
'power': sympy.Pow,
'sin': sympy.sin,
'cos': sympy.cos,
'tan': sympy.tan,
'cot': sympy.cot,
'sinh': sympy.sinh,
'cosh': sympy.cosh,
'coth': sympy.coth,
'tanh': sympy.tanh,
'asin': sympy.asin,
'acos': sympy.acos,
'atan': sympy.atan,
'atan2': sympy.atan2,
'acot': sympy.acot,
'asinh': sympy.asinh,
'acosh': sympy.acosh,
'atanh': sympy.atanh,
'acoth': sympy.acoth,
'exp': sympy.exp,
'log': sympy.log,
'ln': sympy.ln,
}
def parse_presentation_symbol(xml):
"""
Parse <msub>, <msup>, <mi>, and <mn>
"""
tag = gettag(xml)
if tag == 'mn':
return xml.text
elif tag == 'mi':
return xml.text
elif tag == 'msub':
return '_'.join([parse_presentation_symbol(y) for y in xml])
elif tag == 'msup':
return '^'.join([parse_presentation_symbol(y) for y in xml])
raise Exception('[parse_presentation_symbol] unknown tag %s' % tag)
# parser tree for Content MathML
tag = gettag(xml)
# first do compound objects
if tag == 'apply': # apply operator
opstr = gettag(xml[0])
if opstr in opdict:
op = opdict[opstr] # pylint: disable=invalid-name
args = [self.make_sympy(expr) for expr in xml[1:]]
try:
res = op(*args)
except Exception as err:
self.args = args # pylint: disable=attribute-defined-outside-init
self.op = op # pylint: disable=attribute-defined-outside-init, invalid-name
raise Exception('[formula] error=%s failed to apply %s to args=%s' % (err, opstr, args)) # lint-amnesty, pylint: disable=raise-missing-from
return res
else:
raise Exception('[formula]: unknown operator tag %s' % (opstr))
elif tag == 'list': # square bracket list
if gettag(xml[0]) == 'matrix':
return self.make_sympy(xml[0])
else:
return [self.make_sympy(expr) for expr in xml]
elif tag == 'matrix':
return sympy.Matrix([self.make_sympy(expr) for expr in xml])
elif tag == 'vector':
return [self.make_sympy(expr) for expr in xml]
# atoms are below
elif tag == 'cn': # number
return sympy.sympify(xml.text)
elif tag == 'ci': # variable (symbol)
if len(xml) > 0 and (gettag(xml[0]) == 'msub' or gettag(xml[0]) == 'msup'): # subscript or superscript
usym = parse_presentation_symbol(xml[0])
sym = sympy.Symbol(str(usym))
else:
usym = six.text_type(xml.text)
if 'hat' in usym:
sym = my_sympify(usym)
else:
if usym == 'i' and self.options is not None and 'imaginary' in self.options: # i = sqrt(-1)
sym = sympy.I
else:
sym = sympy.Symbol(str(usym))
return sym
else: # unknown tag
raise Exception('[formula] unknown tag %s' % tag)
sympy = property(make_sympy, None, None, 'sympy representation')

View File

@@ -1,337 +0,0 @@
# lint-amnesty, pylint: disable=missing-module-docstring
# !/usr/bin/python
# -*- coding: utf-8 -*-
#
# File: symmath_check.py
# Date: 02-May-12 (creation)
#
# Symbolic mathematical expression checker for edX. Uses sympy to check for expression equality.
#
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
import logging
import traceback
from markupsafe import escape
from openedx.core.djangolib.markup import HTML
from .formula import * # lint-amnesty, pylint: disable=wildcard-import
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# check function interface
#
# This is one of the main entry points to call.
def symmath_check_simple(expect, ans, adict={}, symtab=None, extra_options=None): # lint-amnesty, pylint: disable=dangerous-default-value, unused-argument
"""
Check a symbolic mathematical expression using sympy.
The input is an ascii string (not MathML) converted to math using sympy.sympify.
"""
options = {'__MATRIX__': False, '__ABC__': False, '__LOWER__': False}
if extra_options:
options.update(extra_options)
for op in options: # find options in expect string
if op in expect:
expect = expect.replace(op, '')
options[op] = True
expect = expect.replace('__OR__', '__or__') # backwards compatibility
if options['__LOWER__']:
expect = expect.lower()
ans = ans.lower()
try:
ret = check(expect, ans,
matrix=options['__MATRIX__'],
abcsym=options['__ABC__'],
symtab=symtab,
)
except Exception as err: # lint-amnesty, pylint: disable=broad-except
return {'ok': False,
'msg': HTML('Error {err}<br/>Failed in evaluating check({expect},{ans})').format(
err=err, expect=expect, ans=ans
)}
return ret
#-----------------------------------------------------------------------------
# pretty generic checking function
def check(expect, given, numerical=False, matrix=False, normphase=False, abcsym=False, do_qubit=True, symtab=None, dosimplify=False): # lint-amnesty, pylint: disable=line-too-long
"""
Returns dict with
'ok': True if check is good, False otherwise
'msg': response message (in HTML)
"expect" may have multiple possible acceptable answers, separated by "__OR__"
"""
if "__or__" in expect: # if multiple acceptable answers
eset = expect.split('__or__') # then see if any match
for eone in eset:
ret = check(eone, given, numerical, matrix, normphase, abcsym, do_qubit, symtab, dosimplify)
if ret['ok']:
return ret
return ret
flags = {}
if "__autonorm__" in expect:
flags['autonorm'] = True
expect = expect.replace('__autonorm__', '')
matrix = True
threshold = 1.0e-3
if "__threshold__" in expect:
(expect, st) = expect.split('__threshold__')
threshold = float(st)
numerical = True
if str(given) == '' and not str(expect) == '': # lint-amnesty, pylint: disable=unneeded-not
return {'ok': False, 'msg': ''}
try:
xgiven = my_sympify(given, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
except Exception as err: # lint-amnesty, pylint: disable=broad-except
return {'ok': False, 'msg': HTML('Error {err}<br/> in evaluating your expression "{given}"').format(
err=err, given=given
)}
try:
xexpect = my_sympify(expect, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
except Exception as err: # lint-amnesty, pylint: disable=broad-except
return {'ok': False, 'msg': HTML('Error {err}<br/> in evaluating OUR expression "{expect}"').format(
err=err, expect=expect
)}
if 'autonorm' in flags: # normalize trace of matrices
try:
xgiven /= xgiven.trace()
except Exception as err: # lint-amnesty, pylint: disable=broad-except
return {'ok': False, 'msg': HTML('Error {err}<br/> in normalizing trace of your expression {xgiven}').
format(err=err, xgiven=to_latex(xgiven))}
try:
xexpect /= xexpect.trace()
except Exception as err: # lint-amnesty, pylint: disable=broad-except
return {'ok': False, 'msg': HTML('Error {err}<br/> in normalizing trace of OUR expression {xexpect}').
format(err=err, xexpect=to_latex(xexpect))}
msg = 'Your expression was evaluated as ' + to_latex(xgiven)
# msg += '<br/>Expected ' + to_latex(xexpect)
# msg += "<br/>flags=%s" % flags
if matrix and numerical:
xgiven = my_evalf(xgiven, chop=True)
dm = my_evalf(sympy.Matrix(xexpect) - sympy.Matrix(xgiven), chop=True)
msg += " = " + to_latex(xgiven)
if abs(dm.vec().norm().evalf()) < threshold:
return {'ok': True, 'msg': msg}
else:
pass
#msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf()))
#msg += "expect = " + to_latex(xexpect)
elif dosimplify:
if sympy.simplify(xexpect) == sympy.simplify(xgiven):
return {'ok': True, 'msg': msg}
elif numerical:
if abs((xexpect - xgiven).evalf(chop=True)) < threshold:
return {'ok': True, 'msg': msg}
elif xexpect == xgiven:
return {'ok': True, 'msg': msg}
#msg += "<p/>expect='%s', given='%s'" % (expect,given) # debugging
# msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
return {'ok': False, 'msg': msg}
#-----------------------------------------------------------------------------
# helper function to convert all <p> to <span class='inline-error'>
def make_error_message(msg):
# msg = msg.replace('<p>','<p><span class="inline-error">').replace('</p>','</span></p>')
msg = HTML('<div class="capa_alert">{msg}</div>').format(msg=msg)
return msg
def is_within_tolerance(expected, actual, tolerance):
if expected == 0:
return abs(actual) < tolerance
else:
return abs(abs(actual - expected) / expected) < tolerance
#-----------------------------------------------------------------------------
# Check function interface, which takes pmathml input
#
# This is one of the main entry points to call.
def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None): # lint-amnesty, pylint: disable=too-many-statements
"""
Check a symbolic mathematical expression using sympy.
The input may be presentation MathML. Uses formula.
This is the default Symbolic Response checking function
Desc of args:
expect is a sympy string representing the correct answer. It is interpreted
using my_sympify (from formula.py), which reads strings as sympy input
(e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5)
ans is student-typed answer. It is expected to be ascii math, but the code
below would support a sympy string.
dynamath is the PMathML string converted by MathJax. It is used if
evaluation with ans is not sufficient.
options is a string with these possible substrings, set as an xml property
of the problem:
-matrix - make a sympy matrix, rather than a list of lists, if possible
-qubit - passed to my_sympify
-imaginary - used in formla, presumably to signal to use i as sqrt(-1)?
-numerical - force numerical comparison.
"""
msg = ''
# msg += '<p/>abname=%s' % abname
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
threshold = 1.0e-3 # for numerical comparison (also with matrices)
DEBUG = debug
if xml is not None:
DEBUG = xml.get('debug', False) # override debug flag using attribute in symbolicmath xml
if DEBUG in ['0', 'False']:
DEBUG = False
# options
if options is None:
options = ''
do_matrix = 'matrix' in options
do_qubit = 'qubit' in options
do_numerical = 'numerical' in options
# parse expected answer
try:
fexpect = my_sympify(str(expect), matrix=do_matrix, do_qubit=do_qubit)
except Exception as err: # lint-amnesty, pylint: disable=broad-except
msg += HTML('<p>Error {err} in parsing OUR expected answer "{expect}"</p>').format(err=err, expect=expect)
return {'ok': False, 'msg': make_error_message(msg)}
###### Sympy input #######
# if expected answer is a number, try parsing provided answer as a number also
try:
fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
except Exception as err: # lint-amnesty, pylint: disable=broad-except, unused-variable
fans = None
# do a numerical comparison if both expected and answer are numbers
if hasattr(fexpect, 'is_number') and fexpect.is_number \
and hasattr(fans, 'is_number') and fans.is_number:
if is_within_tolerance(fexpect, fans, threshold):
return {'ok': True, 'msg': msg}
else:
msg += HTML('<p>You entered: {fans}</p>').format(fans=to_latex(fans))
return {'ok': False, 'msg': msg}
if do_numerical: # numerical answer expected - force numerical comparison
if is_within_tolerance(fexpect, fans, threshold):
return {'ok': True, 'msg': msg}
else:
msg += HTML('<p>You entered: {fans} (note that a numerical answer is expected)</p>').\
format(fans=to_latex(fans))
return {'ok': False, 'msg': msg}
if fexpect == fans:
msg += HTML('<p>You entered: {fans}</p>').format(fans=to_latex(fans))
return {'ok': True, 'msg': msg}
###### PMathML input ######
# convert mathml answer to formula
try:
mmlans = dynamath[0] if dynamath else None
except Exception as err: # lint-amnesty, pylint: disable=broad-except
mmlans = None
if not mmlans:
return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
f = formula(mmlans, options=options)
# get sympy representation of the formula
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
try:
fsym = f.sympy
msg += HTML('<p>You entered: {sympy}</p>').format(sympy=to_latex(f.sympy))
except Exception as err: # lint-amnesty, pylint: disable=broad-except
log.exception("Error evaluating expression '%s' as a valid equation", ans)
msg += HTML("<p>Error in evaluating your expression '{ans}' as a valid equation</p>").format(ans=ans)
if "Illegal math" in str(err):
msg += HTML("<p>Illegal math expression</p>")
if DEBUG:
msg += HTML('Error: {err}<hr><p><font color="blue">DEBUG messages:</p><p><pre>{format_exc}</pre></p>'
'<p>cmathml=<pre>{cmathml}</pre></p><p>pmathml=<pre>{pmathml}</pre></p><hr>').format(
err=escape(str(err)), format_exc=traceback.format_exc(), cmathml=escape(f.cmathml),
pmathml=escape(mmlans)
)
return {'ok': False, 'msg': make_error_message(msg)}
# do numerical comparison with expected
if hasattr(fexpect, 'is_number') and fexpect.is_number:
if hasattr(fsym, 'is_number') and fsym.is_number:
if abs(abs(fsym - fexpect) / fexpect) < threshold:
return {'ok': True, 'msg': msg}
return {'ok': False, 'msg': msg}
msg += HTML("<p>Expecting a numerical answer!</p><p>given = {ans}</p><p>fsym = {fsym}</p>").format(
ans=repr(ans), fsym=repr(fsym)
)
# msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','&lt;')
return {'ok': False, 'msg': make_error_message(msg)}
# Here is a good spot for adding calls to X.simplify() or X.expand(),
# allowing equivalence over binomial expansion or trig identities
# exactly the same?
if fexpect == fsym:
return {'ok': True, 'msg': msg}
if isinstance(fexpect, list):
try:
xgiven = my_evalf(fsym, chop=True)
dm = my_evalf(sympy.Matrix(fexpect) - sympy.Matrix(xgiven), chop=True)
if abs(dm.vec().norm().evalf()) < threshold:
return {'ok': True, 'msg': msg}
except sympy.ShapeError:
msg += HTML("<p>Error - your input vector or matrix has the wrong dimensions")
return {'ok': False, 'msg': make_error_message(msg)}
except Exception as err: # lint-amnesty, pylint: disable=broad-except
msg += HTML("<p>Error %s in comparing expected (a list) and your answer</p>").format(escape(str(err)))
if DEBUG:
msg += HTML("<p/><pre>{format_exc}</pre>").format(format_exc=traceback.format_exc())
return {'ok': False, 'msg': make_error_message(msg)}
#diff = (fexpect-fsym).simplify()
#fsym = fsym.simplify()
#fexpect = fexpect.simplify()
try:
diff = (fexpect - fsym)
except Exception as err: # lint-amnesty, pylint: disable=broad-except
diff = None
if DEBUG:
msg += HTML('<hr><p><font color="blue">DEBUG messages:</p><p>Got: {fsym}</p><p>Expecting: {fexpect}</p>')\
.format(fsym=repr(fsym), fexpect=repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)'))
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff:
msg += HTML("<p>Difference: {diff}</p>").format(diff=to_latex(diff))
msg += HTML('<hr>')
# Used to return more keys: 'ex': fexpect, 'got': fsym
return {'ok': False, 'msg': msg}

View File

@@ -1,117 +0,0 @@
"""
Tests of symbolic math
"""
import re
import unittest
from lxml import etree
from . import formula
def stripXML(xml):
xml = xml.replace('\n', '')
xml = re.sub(r'\> +\<', '><', xml)
return xml
class FormulaTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
# for readability later
mathml_start = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mstyle displaystyle="true">'
mathml_end = '</mstyle></math>'
def setUp(self):
super(FormulaTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
self.formulaInstance = formula('')
def test_replace_mathvariants(self):
expr = '''
<mstyle mathvariant="script">
<mi>N</mi>
</mstyle>'''
expected = '<mi>scriptN</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
assert test.decode('utf-8') == expected
def test_fix_simple_superscripts(self):
expr = '''
<msup>
<mi>a</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>b</mi>
</mrow>
</msup>'''
expected = '<mi>a__b</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
assert test.decode('utf-8') == expected
def test_fix_complex_superscripts(self):
expr = '''
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>c</mi>
</mrow>
</msubsup>'''
expected = '<mi>a_b__c</mi>'
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
assert test.decode('utf-8') == expected
def test_fix_msubsup(self):
expr = '''
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mi>c</mi>
</msubsup>'''
expected = '<msup><mi>a_b</mi><mi>c</mi></msup>' # which is (a_b)^c
# wrap
expr = stripXML(self.mathml_start + expr + self.mathml_end)
expected = stripXML(self.mathml_start + expected + self.mathml_end)
# process the expression
xml = etree.fromstring(expr)
xml = self.formulaInstance.preprocess_pmathml(xml)
test = etree.tostring(xml)
# success?
assert test.decode('utf-8') == expected

View File

@@ -1,89 +0,0 @@
# lint-amnesty, pylint: disable=missing-module-docstring
from unittest import TestCase
from six.moves import range
from .symmath_check import symmath_check
class SymmathCheckTest(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def test_symmath_check_integers(self):
number_list = range(-100, 100)
self._symmath_check_numbers(number_list)
def test_symmath_check_floats(self):
number_list = [i + 0.01 for i in range(-100, 100)]
self._symmath_check_numbers(number_list)
def test_symmath_check_same_symbols(self):
expected_str = "x+2*y"
dynamath = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>x</mi>
<mo>+</mo>
<mn>2</mn>
<mo>*</mo>
<mi>y</mi>
</mrow>
</mstyle>
</math>'''.strip()
# Expect that the exact same symbolic string is marked correct
result = symmath_check(expected_str, expected_str, dynamath=[dynamath])
assert (('ok' in result) and result['ok'])
def test_symmath_check_equivalent_symbols(self):
expected_str = "x+2*y"
input_str = "x+y+y"
dynamath = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>x</mi>
<mo>+</mo>
<mi>y</mi>
<mo>+</mo>
<mi>y</mi>
</mrow>
</mstyle>
</math>'''.strip()
# Expect that equivalent symbolic strings are marked correct
result = symmath_check(expected_str, input_str, dynamath=[dynamath])
assert (('ok' in result) and result['ok'])
def test_symmath_check_different_symbols(self):
expected_str = "0"
input_str = "x+y"
dynamath = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>x</mi>
<mo>+</mo>
<mi>y</mi>
</mrow>
</mstyle>
</math>'''.strip()
# Expect that an incorrect response is marked incorrect
result = symmath_check(expected_str, input_str, dynamath=[dynamath])
assert (('ok' in result) and (not result['ok']))
assert 'fail' not in result['msg']
def _symmath_check_numbers(self, number_list): # lint-amnesty, pylint: disable=missing-function-docstring
for n in number_list:
# expect = ans, so should say the answer is correct
expect = n
ans = n
result = symmath_check(str(expect), str(ans))
assert (('ok' in result) and result['ok']), ('%f should == %f' % (expect, ans))
# Change expect so that it != ans
expect += 0.1
result = symmath_check(str(expect), str(ans))
assert (('ok' in result) and (not result['ok'])), ('%f should != %f' % (expect, ans))

View File

@@ -22,7 +22,6 @@ sys.path.insert(0, root)
sys.path.append(root / "docs/guides")
sys.path.append(root / "common/lib/capa")
sys.path.append(root / "common/lib/safe_lxml")
sys.path.append(root / "common/lib/symmath")
sys.path.append(root / "common/lib/xmodule")
# Use a settings module that allows all LMS and Studio code to be imported
@@ -225,7 +224,6 @@ modules = {
'cms': 'cms',
'common/lib/capa/capa': 'common/lib/capa',
'common/lib/safe_lxml/safe_lxml': 'common/lib/safe_lxml',
'common/lib/symmath/symmath': 'common/lib/symmath',
'common/lib/xmodule/xmodule': 'common/lib/xmodule',
'lms': 'lms',
'openedx': 'openedx',

View File

@@ -10,5 +10,4 @@ out from edx-platform into separate packages at some point.
common/lib/capa/modules
common/lib/safe_lxml/modules
common/lib/symmath/modules
common/lib/xmodule/modules

View File

@@ -97,8 +97,14 @@ py2neo<2022
# Sphinx requires docutils<0.18. This pin can be removed once https://github.com/sphinx-doc/sphinx/issues/9777 is closed.
docutils<0.18
# Temporary constraint on openedx-calc. Latest openedx-calc also contains symmath, which may upend symmath package version
openedx-calc==2.0.1
# scipy version 1.8 requires numpy>=1.17.3, we've pinned numpy to <1.17.0 in requirements/edx-sandbox/py38.in
scipy<1.8.0
# mistune is a dependency of m2r (which is a dependency of sphinxcontrib-openapi)
# m2r fails to specify the version of mistune that it needs leading to the error message:
# AttributeError: module 'mistune' has no attribute 'BlockGrammar'
# See Issue: https://github.com/miyakogi/m2r/issues/66
# m2r is no longer actively maintained: https://github.com/miyakogi/m2r/pull/43
# This will be fixed when sphinxcontrib-openapi depends on m2r2 instead of m2r
# See issue: https://github.com/sphinx-contrib/openapi/issues/123
mistune<2.0.0

View File

@@ -20,4 +20,3 @@ numpy>=1.16.0,<1.17.0
# NOTE: if you change code in these packages, you MUST change the version
# number in its setup.py or the code WILL NOT be installed during deploy.
-e common/lib/sandbox-packages
-e common/lib/symmath

View File

@@ -6,8 +6,6 @@
#
common/lib/sandbox-packages
# via -r requirements/edx-sandbox/py38.in
common/lib/symmath
# via -r requirements/edx-sandbox/py38.in
cffi==1.15.0
# via cryptography
chem==1.2.0
@@ -30,8 +28,11 @@ lxml==4.5.0
# via
# -c requirements/edx-sandbox/../constraints.txt
# -r requirements/edx-sandbox/py38.in
# openedx-calc
markupsafe==2.0.1
# via chem
# via
# chem
# openedx-calc
matplotlib==3.3.4
# via
# -c requirements/edx-sandbox/../constraints.txt
@@ -53,10 +54,8 @@ numpy==1.16.6
# matplotlib
# openedx-calc
# scipy
openedx-calc==2.0.1
# via
# -c requirements/edx-sandbox/../constraints.txt
# -r requirements/edx-sandbox/py38.in
openedx-calc==3.0.1
# via -r requirements/edx-sandbox/py38.in
pillow==9.0.1
# via matplotlib
pycparser==2.21
@@ -89,6 +88,6 @@ sympy==1.6.2
# via
# -c requirements/edx-sandbox/../constraints.txt
# -r requirements/edx-sandbox/py38.in
# symmath
# openedx-calc
tqdm==4.62.3
# via nltk

View File

@@ -24,8 +24,6 @@
# via -r requirements/edx/local.in
-e common/lib/sandbox-packages
# via -r requirements/edx/local.in
-e common/lib/symmath
# via -r requirements/edx/local.in
-e openedx/core/lib/xblock_builtin/xblock_discussion
# via -r requirements/edx/local.in
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
@@ -628,6 +626,7 @@ lxml==4.5.0
# edxval
# lti-consumer-xblock
# olxcleaner
# openedx-calc
# ora2
# safe-lxml
# xblock
@@ -655,6 +654,7 @@ markupsafe==2.0.1
# chem
# jinja2
# mako
# openedx-calc
# xblock
maxminddb==2.2.0
# via geoip2
@@ -702,10 +702,8 @@ oauthlib==3.0.1
# lti-consumer-xblock
# requests-oauthlib
# social-auth-core
openedx-calc==2.0.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.in
openedx-calc==3.0.1
# via -r requirements/edx/base.in
openedx-events==0.7.1
# via -r requirements/edx/base.in
openedx-filters==0.4.3
@@ -1004,7 +1002,7 @@ super-csv==2.1.4
sympy==1.6.2
# via
# -c requirements/edx/../constraints.txt
# symmath
# openedx-calc
tableauserverclient==0.17.0
# via edx-enterprise
testfixtures==6.18.3

View File

@@ -24,8 +24,6 @@
# via -r requirements/edx/testing.txt
-e common/lib/sandbox-packages
# via -r requirements/edx/testing.txt
-e common/lib/symmath
# via -r requirements/edx/testing.txt
-e openedx/core/lib/xblock_builtin/xblock_discussion
# via -r requirements/edx/testing.txt
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
@@ -838,6 +836,7 @@ lxml==4.5.0
# edxval
# lti-consumer-xblock
# olxcleaner
# openedx-calc
# ora2
# pyquery
# safe-lxml
@@ -870,6 +869,7 @@ markupsafe==2.0.1
# chem
# jinja2
# mako
# openedx-calc
# xblock
maxminddb==2.2.0
# via
@@ -879,8 +879,10 @@ mccabe==0.6.1
# via
# -r requirements/edx/testing.txt
# pylint
mistune==2.0.2
# via m2r
mistune==0.8.4
# via
# -c requirements/edx/../constraints.txt
# m2r
mock==4.0.3
# via
# -r requirements/edx/testing.txt
@@ -934,10 +936,8 @@ oauthlib==3.0.1
# lti-consumer-xblock
# requests-oauthlib
# social-auth-core
openedx-calc==2.0.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/testing.txt
openedx-calc==3.0.1
# via -r requirements/edx/testing.txt
openedx-events==0.7.1
# via -r requirements/edx/testing.txt
openedx-filters==0.4.3
@@ -1422,7 +1422,7 @@ sympy==1.6.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/testing.txt
# symmath
# openedx-calc
tableauserverclient==0.17.0
# via
# -r requirements/edx/testing.txt

View File

@@ -3,7 +3,6 @@
-e common/lib/capa
-e common/lib/safe_lxml
-e common/lib/sandbox-packages
-e common/lib/symmath
-e common/lib/xmodule
-e openedx/core/lib/xblock_builtin/xblock_discussion

View File

@@ -24,8 +24,6 @@
# via -r requirements/edx/base.txt
-e common/lib/sandbox-packages
# via -r requirements/edx/base.txt
-e common/lib/symmath
# via -r requirements/edx/base.txt
-e openedx/core/lib/xblock_builtin/xblock_discussion
# via -r requirements/edx/base.txt
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
@@ -797,6 +795,7 @@ lxml==4.5.0
# edxval
# lti-consumer-xblock
# olxcleaner
# openedx-calc
# ora2
# pyquery
# safe-lxml
@@ -828,6 +827,7 @@ markupsafe==2.0.1
# chem
# jinja2
# mako
# openedx-calc
# xblock
maxminddb==2.2.0
# via
@@ -884,10 +884,8 @@ oauthlib==3.0.1
# lti-consumer-xblock
# requests-oauthlib
# social-auth-core
openedx-calc==2.0.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
openedx-calc==3.0.1
# via -r requirements/edx/base.txt
openedx-events==0.7.1
# via -r requirements/edx/base.txt
openedx-filters==0.4.3
@@ -1317,7 +1315,7 @@ sympy==1.6.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# symmath
# openedx-calc
tableauserverclient==0.17.0
# via
# -r requirements/edx/base.txt

View File

@@ -17,7 +17,7 @@ function clean_file {
# Workaround for https://github.com/jazzband/pip-tools/issues/204 -
# change absolute paths for local editable packages back to relative ones
FILE_CONTENT=$(<${FILE_PATH})
FILE_URL_REGEX="-e (file:///[^"$'\n'"]*)/common/lib/symmath"
FILE_URL_REGEX="-e (file:///[^"$'\n'"]*)/common/lib/\w+"
if [[ "${FILE_CONTENT}" =~ ${FILE_URL_REGEX} ]]; then
BASE_FILE_URL=${BASH_REMATCH[1]}
sed "s|$BASE_FILE_URL/||" ${FILE_PATH} > ${TEMP_FILE}

View File

@@ -38,7 +38,7 @@ exclude+='|^common/test/data/?.*$'
# * common/lib/xmodule -> EXCLUDE from check.
# * common/lib/xmodule/xmodule/modulestore -> INCLUDE in check.
exclude+='|^common/lib$'
exclude+='|^common/lib/(capa|safe_lxml|sandbox-packages|symmath|xmodule)$'
exclude+='|^common/lib/(capa|safe_lxml|sandbox-packages|xmodule)$'
# Docs, scripts.
exclude+='|^docs/.*$'