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 '' % (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 = ''
+
+ 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 = '''
+'''.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 = '''
+'''.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 = '''
+'''.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/.*$'