1# -*- coding: utf-8 -*-
2"""
3Mathics Built-in Functions and Variables.
4
5Mathics has over a thousand Built-in Functions and variables, all of which are defined here.
6"""
7
8import glob
9import importlib
10import pkgutil
11import re
12import os.path as osp
13from mathics.settings import ENABLE_FILES_MODULE
14from mathics.version import __version__  # noqa used in loading to check consistency.
15
16from typing import List
17
18# Get a list of files in this directory. We'll exclude from the start
19# files with leading characters we don't want like __init__ with its leading underscore.
20__py_files__ = [
21    osp.basename(f[0:-3])
22    for f in glob.glob(osp.join(osp.dirname(__file__), "[a-z]*.py"))
23]
24
25from mathics.builtin.base import (
26    Builtin,
27    SympyObject,
28    Operator,
29    PatternObject,
30)
31
32def add_builtins(new_builtins):
33    for var_name, builtin in new_builtins:
34        name = builtin.get_name()
35        if hasattr(builtin, "python_equivalent"):
36            # print("XXX0", builtin.python_equivalent)
37            mathics_to_python[name] = builtin.python_equivalent
38
39        if isinstance(builtin, SympyObject):
40            mathics_to_sympy[name] = builtin
41            for sympy_name in builtin.get_sympy_names():
42                ### print("XXX1", sympy_name)
43                sympy_to_mathics[sympy_name] = builtin
44        if isinstance(builtin, Operator):
45            builtins_precedence[name] = builtin.precedence
46        if isinstance(builtin, PatternObject):
47            pattern_objects[name] = builtin.__class__
48    _builtins.update(dict(new_builtins))
49
50def builtins_dict():
51    return {
52        builtin.get_name(): builtin
53        for modname, builtins in builtins_by_module.items()
54        for builtin in builtins
55    }
56
57def contribute(definitions):
58    # let MakeBoxes contribute first
59    _builtins["System`MakeBoxes"].contribute(definitions)
60    for name, item in _builtins.items():
61        if name != "System`MakeBoxes":
62            item.contribute(definitions)
63
64    from mathics.core.expression import ensure_context
65    from mathics.core.parser import all_operator_names
66    from mathics.core.definitions import Definition
67
68    # All builtins are loaded. Create dummy builtin definitions for
69    # any remaining operators that don't have them. This allows
70    # operators like \[Cup] to behave correctly.
71    for operator in all_operator_names:
72        if not definitions.have_definition(ensure_context(operator)):
73            op = ensure_context(operator)
74            definitions.builtin[op] = Definition(name=op)
75
76def get_module_doc(module):
77    doc = module.__doc__
78    if doc is not None:
79        doc = doc.strip()
80    if doc:
81        title = doc.splitlines()[0]
82        text = "\n".join(doc.splitlines()[1:])
83    else:
84        title = module.__name__
85        for prefix in ("mathics.builtin.", "mathics.optional."):
86            if title.startswith(prefix):
87                title = title[len(prefix) :]
88        title = title.capitalize()
89        text = ""
90    return title, text
91
92def import_builtins(module_names: List[str], submodule_name=None) -> None:
93    """
94    Imports the list of Mathics Built-in modules so that inside
95    Mathics we have these Builtin Functions, like Plus[], List[] are defined.
96
97    """
98    def import_module(module_name: str, import_name: str):
99        try:
100            module = importlib.import_module(import_name)
101        except Exception as e:
102            print(e)
103            print(f"    Not able to load {module_name}. Check your installation.")
104            print(f"    mathics.builtin loads from {__file__[:-11]}")
105            return None
106
107        if __version__ != module.__version__:
108            print(
109                f"Version {module.__version__} in the module does not match top-level Mathics version {__version__}"
110            )
111        if module:
112            modules.append(module)
113
114    if submodule_name:
115        import_module(submodule_name, f"mathics.builtin.{submodule_name}")
116
117    for module_name in module_names:
118        import_name = (
119            f"mathics.builtin.{submodule_name}.{module_name}"
120            if submodule_name
121            else f"mathics.builtin.{module_name}"
122        )
123        import_module(module_name, import_name)
124
125
126def is_builtin(var):
127    if var == Builtin:
128        return True
129    if hasattr(var, "__bases__"):
130        return any(is_builtin(base) for base in var.__bases__)
131    return False
132
133
134# FIXME: redo using importlib since that is probably less fragile.
135exclude_files = set(("codetables", "base"))
136module_names = [
137    f for f in __py_files__ if re.match("^[a-z0-9]+$", f) if f not in exclude_files
138]
139
140modules = []
141import_builtins(module_names)
142
143_builtins = []
144builtins_by_module = {}
145
146disable_file_module_names = [] if ENABLE_FILES_MODULE else ["files_io.files", "files_io.importexport"]
147
148for subdir in ("drawing", "files_io", "numbers", "specialfns", "fileformats"):
149    import_name = f"{__name__}.{subdir}"
150
151    if import_name in disable_file_module_names:
152        continue
153
154    builtin_module = importlib.import_module(import_name)
155    submodule_names = [
156        modname
157        for importer, modname, ispkg in pkgutil.iter_modules(builtin_module.__path__)
158    ]
159    # print("XXX3", submodule_names)
160    import_builtins(submodule_names, subdir)
161
162for module in modules:
163    builtins_by_module[module.__name__] = []
164    vars = dir(module)
165    for name in vars:
166        var = getattr(module, name)
167        if (
168            hasattr(var, "__module__")
169            and var.__module__.startswith("mathics.builtin.")
170            and var.__module__ != "mathics.builtin.base"
171            and is_builtin(var)
172            and not name.startswith("_")
173            and var.__module__ == module.__name__
174        ):  # nopep8
175
176            instance = var(expression=False)
177
178            if isinstance(instance, Builtin):
179                # This set the default context for symbols in mathics.builtins
180                if not type(instance).context:
181                    type(instance).context = "System`"
182                _builtins.append((instance.get_name(), instance))
183                builtins_by_module[module.__name__].append(instance)
184
185
186mathics_to_sympy = {}  # here we have: name -> sympy object
187mathics_to_python = {}  # here we have: name -> string
188sympy_to_mathics = {}
189
190pattern_objects = {}
191builtins_precedence = {}
192
193new_builtins = _builtins
194
195# FIXME: some magic is going on here..
196_builtins = {}
197
198add_builtins(new_builtins)
199