Repository: sympy/sympy
Pull request: Allow CodePrinter to generate code for any Function
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->
~~Not really a fix, but rather a test case showing the new syntax.~~
~~Perhaps a proper fix is needed, providing this to start with for discussion.~~

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234". See
https://github.com/blog/1506-closing-issues-via-pull-requests .-->
Fixes #15071

#### Brief description of what is fixed or changed
Fixed regression in lambdify.

#### Other comments


#### Release Notes
<!-- Write the release notes for this release below. See
https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
on how to write release notes. If there is no release notes entry for this PR,
write "NO ENTRY". The bot will check your release notes automatically to see
if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* printing
  * CodePrinter now generates code for unkown `Function` instances unless the new printer option `allow_unknown_functions` is set to `False`.
<!-- END RELEASE NOTES -->


Problem statement:
[regression] lambdify with Matrix: `NameError: name 'Matrix' is not defined`
I'm trying to create a lambda function from a sympy expression that involves a dot product with a `sympy.Matrix`. Since at least sympy 1.2, this fails.

MWE:
```python
from sympy import Matrix
import sympy
import numpy


class dot(sympy.Function):
    pass


# def vector2vector(x):
#     out = numpy.array(x)
#     if len(out.shape) == 2 and out.shape[1] == 1:
#         out = out[:, 0]
#     return out

# mods = [{"ImmutableDenseMatrix": vector2vector}, "numpy"]

x = sympy.Symbol("x")
expr = dot(x, Matrix([[2], [1], [0]]))
f = sympy.lambdify(x, expr)

X = numpy.zeros((17, 3))
val = f(X)
```
Error message:
```
  File "<lambdifygenerated-1>", line 4, in _lambdifygenerated
NameError: name 'Matrix' is not defined
```

Hints from issue discussion:
Bisected to 998946c03c0934b2bb638f182d728a31120488e7 CC @bjodah 
Mmmm yes, I remember you and I discussing that this could happen. `lambdify` was (and still is) quite under-tested. I opened gh-15085 with a test case that does pass. Is that viable?

Commit message: Make PythonCodePrinter print unkown Functions

Patch to review:
diff --git a/sympy/printing/pycode.py b/sympy/printing/pycode.py
index 85e88cc660b9..5116ef8c2005 100644
--- a/sympy/printing/pycode.py
+++ b/sympy/printing/pycode.py
@@ -106,6 +106,12 @@ def __init__(self, settings=None):
         self.known_constants = dict(self._kc, **(settings or {}).get(
             'user_constants', {}))
 
+    def _print_not_supported(self, expr):
+        if expr.is_Function:
+            return '%s(%s)' % (self._print(expr.func), ', '.join(map(self._print, expr.args)))
+        else:
+            return super(PythonCodePrinter, self)._print_not_supported(expr)
+
     def _get_statement(self, codestring):
         return codestring
 
diff --git a/sympy/utilities/tests/test_lambdify.py b/sympy/utilities/tests/test_lambdify.py
index 7152b8af98c5..42a4f122318f 100644
--- a/sympy/utilities/tests/test_lambdify.py
+++ b/sympy/utilities/tests/test_lambdify.py
@@ -361,6 +361,15 @@ def test_numpy_matrix():
     #Check that the types are arrays and matrices
     assert isinstance(f(1, 2, 3), numpy.ndarray)
 
+    # gh-15071
+    class dot(Function):
+        pass
+    f_dot1 = lambdify(x, dot(x, Matrix([[2], [1], [0]])), [{'dot': numpy.dot}, 'numpy'])
+    assert numpy.all(f_dot1(numpy.zeros((17, 3))) == 0)
+
+    f_dot2 = lambdify(x, dot(x, Matrix([[2], [1], [0]])))
+    assert numpy.all(f_dot2(numpy.zeros((17, 3))) == 0)
+
 def test_numpy_transpose():
     if not numpy:
         skip("numpy not installed.")


Code review comments:
diff_hunk: @@ -106,6 +106,12 @@ def __init__(self, settings=None):
         self.known_constants = dict(self._kc, **(settings or {}).get(
             'user_constants', {}))
 
+    def _print_not_supported(self, expr):
path: sympy/printing/pycode.py
text: @user1:
Any reason to not do this on CodePrinter?

@author:
Not really, changed this just now.