From 6ac1dd688a92248f97de2928a73c31cfde4d90c0 Mon Sep 17 00:00:00 2001 From: Phillip Shiu Date: Fri, 11 Feb 2022 10:30:08 -0500 Subject: [PATCH] Revert "Remove symmath from edx-platform" --- common/lib/symmath/setup.py | 11 + common/lib/symmath/symmath/README.md | 30 + common/lib/symmath/symmath/__init__.py | 3 + common/lib/symmath/symmath/formula.py | 588 ++++++++++++++++++ common/lib/symmath/symmath/symmath_check.py | 337 ++++++++++ common/lib/symmath/symmath/test_formula.py | 117 ++++ .../lib/symmath/symmath/test_symmath_check.py | 89 +++ docs/guides/conf.py | 2 + docs/guides/docstrings/common_lib.rst | 1 + requirements/constraints.txt | 12 +- requirements/edx-sandbox/py38.in | 1 + requirements/edx-sandbox/py38.txt | 17 +- requirements/edx/base.txt | 12 +- requirements/edx/development.txt | 18 +- requirements/edx/local.in | 1 + requirements/edx/testing.txt | 12 +- scripts/post-pip-compile.sh | 2 +- scripts/verify-dunder-init.sh | 2 +- 18 files changed, 1217 insertions(+), 38 deletions(-) create mode 100644 common/lib/symmath/setup.py create mode 100644 common/lib/symmath/symmath/README.md create mode 100644 common/lib/symmath/symmath/__init__.py create mode 100644 common/lib/symmath/symmath/formula.py create mode 100644 common/lib/symmath/symmath/symmath_check.py create mode 100644 common/lib/symmath/symmath/test_formula.py create mode 100644 common/lib/symmath/symmath/test_symmath_check.py diff --git a/common/lib/symmath/setup.py b/common/lib/symmath/setup.py new file mode 100644 index 0000000000..01e91bd133 --- /dev/null +++ b/common/lib/symmath/setup.py @@ -0,0 +1,11 @@ +# lint-amnesty, pylint: disable=missing-module-docstring +from setuptools import setup + +setup( + name="symmath", + version="0.3", + packages=["symmath"], + install_requires=[ + "sympy", + ], +) diff --git a/common/lib/symmath/symmath/README.md b/common/lib/symmath/symmath/README.md new file mode 100644 index 0000000000..8da9aa87ee --- /dev/null +++ b/common/lib/symmath/symmath/README.md @@ -0,0 +1,30 @@ +(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 diff --git a/common/lib/symmath/symmath/__init__.py b/common/lib/symmath/symmath/__init__.py new file mode 100644 index 0000000000..8d00aadd22 --- /dev/null +++ b/common/lib/symmath/symmath/__init__.py @@ -0,0 +1,3 @@ +# lint-amnesty, pylint: disable=missing-module-docstring +from .formula import * +from .symmath_check import * diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py new file mode 100644 index 0000000000..199b5aea1d --- /dev/null +++ b/common/lib/symmath/symmath/formula.py @@ -0,0 +1,588 @@ +#!/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 +# + + +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 '%s{}{}' % (xs[1:-1]) + if expr_s[0] == '$': + return HTML('[mathjax]{expression}[/mathjax]
').format(expression=expr_s[1:-1]) # for sympy v6 + return HTML('[mathjax]{expression}[/mathjax]
').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 'f-2" 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 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 i^ ; mangle + this into hat(f) hat i also somtimes turned into + j ^ + """ + 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 + + m + a + x + + 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 N + 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: + + a + b + c + + to be interpreted '(a_b)^c' (nothing done by this method) + + And modified: + + b + x + + + d + + + to be interpreted 'a_b__c' + + also: + + x + + + B + + + 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 replace such + elements with an , 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 "Error! Cannot process pmathml" + 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 , , , and + """ + 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') diff --git a/common/lib/symmath/symmath/symmath_check.py b/common/lib/symmath/symmath/symmath_check.py new file mode 100644 index 0000000000..ffd56c8922 --- /dev/null +++ b/common/lib/symmath/symmath/symmath_check.py @@ -0,0 +1,337 @@ +# 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}
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}
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}
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}
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}
in normalizing trace of OUR expression {xexpect}'). + format(err=err, xexpect=to_latex(xexpect))} + + msg = 'Your expression was evaluated as ' + to_latex(xgiven) + # msg += '
Expected ' + to_latex(xexpect) + + # msg += "
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 += "

expect='%s', given='%s'" % (expect,given) # debugging + # msg += "

dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y'))) + return {'ok': False, 'msg': msg} + +#----------------------------------------------------------------------------- +# helper function to convert all

to + + +def make_error_message(msg): + # msg = msg.replace('

','

').replace('

','

') + msg = HTML('
{msg}
').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 += '

abname=%s' % abname + # msg += '

adict=%s' % (repr(adict).replace('<','<')) + + 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('

Error {err} in parsing OUR expected answer "{expect}"

').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('

You entered: {fans}

').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('

You entered: {fans} (note that a numerical answer is expected)

').\ + format(fans=to_latex(fans)) + return {'ok': False, 'msg': msg} + + if fexpect == fans: + msg += HTML('

You entered: {fans}

').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 += '

mmlans=%s' % repr(mmlans).replace('<','<') + try: + fsym = f.sympy + msg += HTML('

You entered: {sympy}

').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("

Error in evaluating your expression '{ans}' as a valid equation

").format(ans=ans) + if "Illegal math" in str(err): + msg += HTML("

Illegal math expression

") + if DEBUG: + msg += HTML('Error: {err}

DEBUG messages:

{format_exc}

' + '

cmathml=

{cmathml}

pmathml=

{pmathml}


').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("

Expecting a numerical answer!

given = {ans}

fsym = {fsym}

").format( + ans=repr(ans), fsym=repr(fsym) + ) + # msg += "

cmathml =

%s

" % str(f.cmathml).replace('<','<') + 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("

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("

Error %s in comparing expected (a list) and your answer

").format(escape(str(err))) + if DEBUG: + msg += HTML("

{format_exc}
").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('

DEBUG messages:

Got: {fsym}

Expecting: {fexpect}

')\ + .format(fsym=repr(fsym), fexpect=repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)')) + # msg += "

Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','<') + # msg += "

Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','<') + if diff: + msg += HTML("

Difference: {diff}

").format(diff=to_latex(diff)) + msg += HTML('
') + + # Used to return more keys: 'ex': fexpect, 'got': fsym + return {'ok': False, 'msg': msg} diff --git a/common/lib/symmath/symmath/test_formula.py b/common/lib/symmath/symmath/test_formula.py new file mode 100644 index 0000000000..1235f7b771 --- /dev/null +++ b/common/lib/symmath/symmath/test_formula.py @@ -0,0 +1,117 @@ +""" +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 = '' + mathml_end = '' + + def setUp(self): + super(FormulaTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments + self.formulaInstance = formula('') + + def test_replace_mathvariants(self): + expr = ''' + + N +''' + + expected = 'scriptN' + + # 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 = ''' + + a + + + b + +''' + + expected = 'a__b' + + # 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 = ''' + + a + b + + + c + +''' + + expected = '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 + + def test_fix_msubsup(self): + expr = ''' + + a + b + c +''' + + expected = 'a_bc' # 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 diff --git a/common/lib/symmath/symmath/test_symmath_check.py b/common/lib/symmath/symmath/test_symmath_check.py new file mode 100644 index 0000000000..5a3cfe85f4 --- /dev/null +++ b/common/lib/symmath/symmath/test_symmath_check.py @@ -0,0 +1,89 @@ +# 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 = ''' + + + + x + + + 2 + * + y + + +'''.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 = ''' + + + + x + + + y + + + y + + +'''.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 = ''' + + + + x + + + y + + +'''.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)) diff --git a/docs/guides/conf.py b/docs/guides/conf.py index f3038eb462..58441b5c1b 100644 --- a/docs/guides/conf.py +++ b/docs/guides/conf.py @@ -22,6 +22,7 @@ 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 @@ -224,6 +225,7 @@ 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', diff --git a/docs/guides/docstrings/common_lib.rst b/docs/guides/docstrings/common_lib.rst index 4a4a1d40d6..419063bd5b 100644 --- a/docs/guides/docstrings/common_lib.rst +++ b/docs/guides/docstrings/common_lib.rst @@ -10,4 +10,5 @@ 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 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d37461e984..96da3a211a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -97,14 +97,8 @@ 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 diff --git a/requirements/edx-sandbox/py38.in b/requirements/edx-sandbox/py38.in index 9d90dcba97..ac1af4fcf9 100644 --- a/requirements/edx-sandbox/py38.in +++ b/requirements/edx-sandbox/py38.in @@ -20,3 +20,4 @@ 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 diff --git a/requirements/edx-sandbox/py38.txt b/requirements/edx-sandbox/py38.txt index 69b77bc712..8c9db08481 100644 --- a/requirements/edx-sandbox/py38.txt +++ b/requirements/edx-sandbox/py38.txt @@ -4,7 +4,9 @@ # # make upgrade # --e common/lib/sandbox-packages +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 @@ -28,11 +30,8 @@ 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 - # openedx-calc + # via chem matplotlib==3.3.4 # via # -c requirements/edx-sandbox/../constraints.txt @@ -54,8 +53,10 @@ numpy==1.16.6 # matplotlib # openedx-calc # scipy -openedx-calc==3.0.1 - # via -r requirements/edx-sandbox/py38.in +openedx-calc==2.0.1 + # via + # -c requirements/edx-sandbox/../constraints.txt + # -r requirements/edx-sandbox/py38.in pillow==9.0.1 # via matplotlib pycparser==2.21 @@ -88,6 +89,6 @@ sympy==1.6.2 # via # -c requirements/edx-sandbox/../constraints.txt # -r requirements/edx-sandbox/py38.in - # openedx-calc + # symmath tqdm==4.62.3 # via nltk diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 305c6da624..677ca7ad57 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -24,6 +24,8 @@ # 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 @@ -626,7 +628,6 @@ lxml==4.5.0 # edxval # lti-consumer-xblock # olxcleaner - # openedx-calc # ora2 # safe-lxml # xblock @@ -654,7 +655,6 @@ markupsafe==2.0.1 # chem # jinja2 # mako - # openedx-calc # xblock maxminddb==2.2.0 # via geoip2 @@ -702,8 +702,10 @@ oauthlib==3.0.1 # lti-consumer-xblock # requests-oauthlib # social-auth-core -openedx-calc==3.0.1 - # via -r requirements/edx/base.in +openedx-calc==2.0.1 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/base.in openedx-events==0.7.1 # via -r requirements/edx/base.in openedx-filters==0.4.3 @@ -1002,7 +1004,7 @@ super-csv==2.1.4 sympy==1.6.2 # via # -c requirements/edx/../constraints.txt - # openedx-calc + # symmath tableauserverclient==0.17.0 # via edx-enterprise testfixtures==6.18.3 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 690821aa1e..badf0e1fa2 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -24,6 +24,8 @@ # 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 @@ -836,7 +838,6 @@ lxml==4.5.0 # edxval # lti-consumer-xblock # olxcleaner - # openedx-calc # ora2 # pyquery # safe-lxml @@ -869,7 +870,6 @@ markupsafe==2.0.1 # chem # jinja2 # mako - # openedx-calc # xblock maxminddb==2.2.0 # via @@ -879,10 +879,8 @@ mccabe==0.6.1 # via # -r requirements/edx/testing.txt # pylint -mistune==0.8.4 - # via - # -c requirements/edx/../constraints.txt - # m2r +mistune==2.0.2 + # via m2r mock==4.0.3 # via # -r requirements/edx/testing.txt @@ -936,8 +934,10 @@ oauthlib==3.0.1 # lti-consumer-xblock # requests-oauthlib # social-auth-core -openedx-calc==3.0.1 - # via -r requirements/edx/testing.txt +openedx-calc==2.0.1 + # via + # -c requirements/edx/../constraints.txt + # -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 - # openedx-calc + # symmath tableauserverclient==0.17.0 # via # -r requirements/edx/testing.txt diff --git a/requirements/edx/local.in b/requirements/edx/local.in index d60eab5e64..e0252e71ce 100644 --- a/requirements/edx/local.in +++ b/requirements/edx/local.in @@ -3,6 +3,7 @@ -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 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index ac411e5151..9f2092aadd 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -24,6 +24,8 @@ # 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 @@ -795,7 +797,6 @@ lxml==4.5.0 # edxval # lti-consumer-xblock # olxcleaner - # openedx-calc # ora2 # pyquery # safe-lxml @@ -827,7 +828,6 @@ markupsafe==2.0.1 # chem # jinja2 # mako - # openedx-calc # xblock maxminddb==2.2.0 # via @@ -884,8 +884,10 @@ oauthlib==3.0.1 # lti-consumer-xblock # requests-oauthlib # social-auth-core -openedx-calc==3.0.1 - # via -r requirements/edx/base.txt +openedx-calc==2.0.1 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/base.txt openedx-events==0.7.1 # via -r requirements/edx/base.txt openedx-filters==0.4.3 @@ -1315,7 +1317,7 @@ sympy==1.6.2 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt - # openedx-calc + # symmath tableauserverclient==0.17.0 # via # -r requirements/edx/base.txt diff --git a/scripts/post-pip-compile.sh b/scripts/post-pip-compile.sh index 71be02ac33..a87faa5984 100755 --- a/scripts/post-pip-compile.sh +++ b/scripts/post-pip-compile.sh @@ -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/xmodule" + FILE_URL_REGEX="-e (file:///[^"$'\n'"]*)/common/lib/symmath" if [[ "${FILE_CONTENT}" =~ ${FILE_URL_REGEX} ]]; then BASE_FILE_URL=${BASH_REMATCH[1]} sed "s|$BASE_FILE_URL/||" ${FILE_PATH} > ${TEMP_FILE} diff --git a/scripts/verify-dunder-init.sh b/scripts/verify-dunder-init.sh index bc5da5860f..5055138e3d 100755 --- a/scripts/verify-dunder-init.sh +++ b/scripts/verify-dunder-init.sh @@ -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|xmodule)$' +exclude+='|^common/lib/(capa|safe_lxml|sandbox-packages|symmath|xmodule)$' # Docs, scripts. exclude+='|^docs/.*$'