1##
2# \file    cadabra2_defaults.py
3# \ingroup pythoncore
4# Cadabra2 pure Python functionality.
5#
6# This is a pure-python initialisation script to set the  path to
7# sympy and setup printing of Cadabra expressions.  This script is
8# called both by the command line interface 'cadabra2' as well as by
9# the GUI backend server 'cadabra-server'.
10
11import sys
12import cadabra2
13from cadabra2 import *
14from importlib.machinery import PathFinder, ModuleSpec, SourceFileLoader
15from importlib.abc import MetaPathFinder
16from cdb_appdirs import user_config_dir, user_data_dir
17import datetime
18import atexit
19import rlcompleter
20
21__cdbkernel__=cadabra2.__cdbkernel__
22__cdbkernel__.completer=rlcompleter.Completer()
23
24import os
25os.environ.setdefault('PATH', '')
26
27PY3 = sys.version_info[0] == 3
28if PY3:
29   unicode = str
30
31if "@ENABLE_JUPYTER@" == "OFF":
32   discr = "\\discretionary{}{}{} "
33else:
34   discr = ""
35
36class PackageCompiler(MetaPathFinder):
37        @classmethod
38        def find_module(cls, fullname, path):
39                return find_spec(fullname, path, None)
40
41        @classmethod
42        def find_spec(cls, fullname, path, target=None):
43                #log_("Finding {}, path=[{}]".format(fullname, ', '.join(f'"{p}"' for p in path) if path is not None else ""))
44                # Top-level import if path=None
45                if path is None or path == "":
46                        path = sys.path
47                # Get unqualified package name
48                if '.' in fullname:
49                        parents = fullname.split('.')
50                        name = parents.pop()
51                else:
52                        name = fullname
53                        parents = []
54                # Go through path and try to find a notebook.
55                for entry in path:
56                   have_cnb = os.path.isfile(os.path.join(entry, name + ".cnb"))
57                   have_cdb = os.path.isfile(os.path.join(entry, name + ".cdb"))
58                   have_ipynb = os.path.isfile(os.path.join(entry, name + ".ipynb"))
59                   if have_cnb or have_cdb or have_ipynb:
60                       # Notebook was found. Create a version of it in the temporary directory, then
61                       # return a ModuleSpec object to allow Python to load that file
62                       pkg_path = os.path.join(user_config_dir(), "cadabra_packages", *parents)
63                       # Create the path if it doesn't exist
64                       if not os.path.exists(pkg_path):
65                               os.makedirs(pkg_path)
66                       if have_cnb:
67                          compile_package__(os.path.join(entry, name + ".cnb"), os.path.join(pkg_path, name + ".py"))
68                       elif have_cdb:
69                          compile_package__(os.path.join(entry, name + ".cdb"), os.path.join(pkg_path, name + ".py"))
70                       else:
71                          compile_package__(os.path.join(entry, name + ".ipynb"), os.path.join(pkg_path, name + ".py"))
72                       return ModuleSpec(
73                                        fullname,
74                                        SourceFileLoader(fullname, os.path.join(pkg_path, name + ".py")),
75                                        origin=os.path.join(pkg_path, name + ".py"))
76
77                # Return none if no notebook was found
78                return None
79
80# Prepend to sys.meta_path, so that all imports will first be checked in
81# case they are notebooks that need compiling
82sys.meta_path.insert(0, PackageCompiler)
83
84# Add current directory to Python module import path.
85sys.path.append(".")
86
87#sys.path.insert(0,'/home/kasper/Development/git.others/sympy')
88
89# Attempt to import sympy; if not, setup logic so that the
90# shell does not fail later.
91
92try:
93    import sympy
94except:
95    class Sympy:
96        """!@brief Stub object for when Sympy itself is not available.
97
98        @long When Sympy is not available, this object contains some basic
99        functionality to prevent things from breaking elsewhere.
100        """
101        __version__="unavailable"
102
103    sympy = Sympy()
104
105if sympy.__version__ != "unavailable":
106    from sympy import factor
107    from sympy import integrate
108    from sympy import diff
109    from sympy import symbols
110    from sympy import latex
111    from sympy import sin, cos, tan, sqrt, trigsimp
112    from sympy import Matrix as sMatrix
113
114# Whether running in command-line mode or as client-server, there always
115# needs to be a Server object known as 'server' through which interaction
116# with the display routines is handled. The 'display' function will
117# call the 'server.send' method.
118
119if 'server' in globals():
120    mopen="\\begin{dmath*}{}";
121    mclose="\\end{dmath*}";
122else:
123    mopen=''
124    mclose=''
125    class Server:
126        """!@brief Object to handle advanced display in a UI-independent way.
127
128        @long Cadabra makes available to Python a Server object, which
129        contains functions to send output to the user. When running
130        from the command line this simply prints to the screen, but it
131        can talk to a remote client to display images and maths.
132        """
133
134        def send(self, data, typestr, parent_id, last_in_sequence):
135            """ Send a message to the client; 'typestr' indicates the cell type,
136            'parent_id', if non-null, indicates the serial number of the parent
137            cell.
138            """
139            print(data)
140            return 0
141
142        def architecture(self):
143            return "terminal"
144
145        def test(self):
146            print("hello there!")
147
148        def handles(self, otype):
149            if(otype=="plain"):
150                return True
151            return False
152
153        def totals(self):
154            return __cdb_progress_monitor__.totals()
155
156    server = Server()
157
158# Import matplotlib and setup functions to prepare its output
159# for sending as base64 to the client. Example use:
160#
161#   import matplotlib.pyplot as plt
162#   p = plt.plot([1,2,3],[1,2,5],'-o')
163#   display(p[0])
164#
165
166have_matplotlib=True
167try:
168    import matplotlib
169    import matplotlib.artist
170    import matplotlib.figure
171    matplotlib.use('Agg')
172except ImportError:
173    have_matplotlib=False
174
175def save_history(history_path):
176    try:
177        readline.write_history_file(history_path)
178    except:
179        pass
180
181try:
182    import readline
183    history_path = os.path.join(user_data_dir(), "cadabra_history")
184    if os.path.exists(history_path):
185        readline.read_history_file(history_path)
186        readline.set_history_length(1000)
187    atexit.register(save_history, history_path)
188except:
189    pass
190
191import io
192import base64
193
194## @brief Generic display function which handles local as well as remote clients.
195#
196# The 'display' function is a replacement for 'str', in the sense that
197# it will generate human-readable output. However, in contrast to
198# 'str', it knows about what the front-end ('server') can display, and
199# will adapt the output to that. For instance, if
200# server.handles('latex_view') is true, it will generate LaTeX output,
201# while it will generate just plain text otherwise.
202#
203# Once it has figured out which display is accepted by 'server', it
204# will call server.send() with data depending on the object type it is
205# being fed. Data types the server object can support are:
206#
207# - "latex_view": text-mode LaTeX string.
208# - "image_png":  base64 encoded png image.
209# - "verbatim":   ascii string to be displayed verbatim.
210
211def display(obj, delay_send=False):
212    """
213    Generalised 'print' function which knows how to display objects in the
214    best possible way on the used interface, be it a console or graphical
215    notebook. In particular, it knows how to display Cadabra expressions
216    in typeset form whenever LaTeX functionality is available. Can also be
217    used to display matplotlib plots.
218
219    When using a Cadabra front-end (command line or notebook), an expression
220    with a trailing semi-colon ';' will automatically be wrapped in a
221    'display' function call so that the expression is displayed immediately.
222    """
223    if 'matplotlib' in sys.modules and isinstance(obj, matplotlib.figure.Figure):
224        imgstring = io.BytesIO()
225        obj.savefig(imgstring,format='png')
226        imgstring.seek(0)
227        b64 = base64.b64encode(imgstring.getvalue())
228        server.send(b64, "image_png", 0, False)
229        # FIXME: Use the 'handles' query method on the Server object
230        # to figure out whether it can do something useful
231        # with a particular data type.
232
233    elif 'matplotlib' in sys.modules and isinstance(obj, matplotlib.artist.Artist):
234        f = obj.get_figure()
235        imgstring = io.BytesIO()
236        f.savefig(imgstring,format='png')
237        imgstring.seek(0)
238        b64 = base64.b64encode(imgstring.getvalue())
239        server.send(b64, "image_png", 0, False)
240
241    elif hasattr(obj,'_backend'):
242        if hasattr(obj._backend,'fig'):
243            f = obj._backend.fig
244            imgstring = io.BytesIO()
245            f.savefig(imgstring,format='png')
246            imgstring.seek(0)
247            b64 = base64.b64encode(imgstring.getvalue())
248            server.send(b64, "image_png", 0, False)
249
250    elif 'vtk' in sys.modules and isinstance(obj, vtk.vtkRenderer):
251        # Vtk renderer, see http://nbviewer.ipython.org/urls/bitbucket.org/somada141/pyscience/raw/master/20140917_RayTracing/Material/PythonRayTracingEarthSun.ipynb
252        pass
253
254#    elif isinstance(obj, numpy.ndarray):
255#        server.send("\\begin{dmath*}{}"+str(obj.to_list())+"\\end{dmath*}", "latex")
256
257    elif isinstance(obj, Ex):
258        if server.handles('latex_view'):
259            if delay_send:
260                return obj._latex_()
261            else:
262                ret = mopen+obj._latex_()+mclose
263                id=server.send(ret, "latex_view", 0, False)
264                # print(id)
265                # Make a child cell of the above with input form content.
266                server.send(obj.input_form(), "input_form", id, False)
267        else:
268            server.send(unicode(obj), "plain", 0, False)
269
270    elif isinstance(obj, Property):
271        if server.handles('latex_view'):
272            ret = mopen+obj._latex_()+mclose
273            if delay_send:
274                return ret
275            else:
276                server.send(ret , "latex_view", 0, False)
277                # Not yet available.
278                # server.send(obj.input_form(), "input_form", 0, False)
279        else:
280            server.send(unicode(obj), "plain", 0, False)
281
282    elif type(obj)==list:
283        out="{}$\\big[$"
284        first=True
285        for elm in obj:
286            if first==False:
287                out+=","+discr
288            else:
289                first=False
290            out+= "$"+display(elm, True)+"$"
291        out+="$\\big]$";
292        server.send(out, "latex_view", 0, False)
293        # FIXME: send input_form version.
294
295    elif hasattr(obj, "__module__") and hasattr(obj.__module__, "find") and obj.__module__.find("sympy")!=-1:
296        if delay_send:
297           return latex(obj)
298        else:
299           server.send("\\begin{dmath*}{}"+latex(obj)+"\\end{dmath*}", "latex_view", 0, False)
300
301    else:
302        # Failing all else, just dump a str representation to the notebook, asking
303        # it to display this verbatim.
304        # server.send("\\begin{dmath*}{}"+str(obj)+"\\end{dmath*}", "latex")
305        if delay_send:
306            return "\\verb|"+str(obj)+"|"
307        else:
308            server.send(unicode(obj), "verbatim", 0, False)
309
310__cdbkernel__.server=server
311__cdbkernel__.display=display
312
313class Console(object):
314        """
315        The interactive console works in the same Python context as
316        the notebook cells to allow evaluation of expressions for
317        debugging/logging purposes
318        """
319        def log(self, obj):
320                """
321                Sends a string representation of obj to the console
322                """
323                if server.architecture() == "terminal":
324                        print(text)
325                elif server.architecture() == "client-server":
326                        server.send(unicode(obj), "csl_out", 0, False)
327
328        def clear(self):
329                """
330                Clears the output of the console window
331                """
332                if server.architecture() == "client-server":
333                        server.send("", "csl_clear", 0, False)
334
335console = Console()
336
337# Set display hooks to catch certain objects and print them
338# differently. Should probably eventually be done cleaner.
339
340def _displayhook(arg):
341    global remember_display_hook
342    if isinstance(arg, Ex):
343        print(unicode(arg))
344    elif isinstance(arg, Property):
345        print(unicode(arg))
346    else:
347        remember_display_hook(arg)
348
349remember_display_hook = sys.displayhook
350sys.displayhook = _displayhook
351
352# Default post-processing algorithms. These are not pre-processed
353# so need to have the '__cdbkernel__' argument.
354
355def post_process(__cdbkernel__, ex):
356    collect_terms(ex)
357
358