packages.py 4.98 KB
Newer Older
1
2
3
4
5
6
7
8
"""
Package (or module) loader.

It manage Pyodide and Basthon's modules load.
"""

import pyodide
import js
Romain Casati's avatar
Romain Casati committed
9
from . import _patch_modules
10
from .utils import OrderedSet
11
12
13
14
15
16
17


__author__ = "Romain Casati"
__license__ = "GNU GPL v3"
__email__ = "romain.casati@basthon.fr"


Romain Casati's avatar
Romain Casati committed
18
__all__ = ["find_imports", "load_and_patch"]
19
20
21
22
23
24


# Link to useful pyodide's dict
_import_name_to_package_name = js.pyodide._module.packages.import_name_to_package_name
_import_name_to_package_name = {k: _import_name_to_package_name[k]
                                for k in dir(_import_name_to_package_name)
25
                                if isinstance(_import_name_to_package_name[k], str) and '.' not in k}
26
27
28
29
30

# Available packages in Pyodide.
_pyodide_pkgs = set(_import_name_to_package_name.keys())

# Root path to internal modules
Romain Casati's avatar
Romain Casati committed
31
_internal_modules_path = js.Basthon.basthonModulesRoot()
32
33
34

# Packages not implemented in Pyodide but in Basthon (internal)
# (dict pointing to Pypi or internal addresse).
35
_internal_pkgs_dict = {
36
37
38
39
40
41
42
43
44
45
46
47
    "turtle": {
        "path": f"{_internal_modules_path}/turtle-0.0.1-py3-none-any.whl",
    },
    "requests": {
        "path": f"{_internal_modules_path}/requests-0.0.1-py3-none-any.whl",
    },
    "proj4py": {
        "path": f"{_internal_modules_path}/proj4py-0.0.1-py3-none-any.whl",
        "deps": ["pkg_resources"],
    },
    "folium": {
        "path": "folium",  # loaded from PyPi
Romain Casati's avatar
Romain Casati committed
48
        "deps": ["requests"],
49
50
51
52
53
54
55
56
57
58
59
60
    },
    "graphviz": {
        "path": f"{_internal_modules_path}/graphviz-0.0.1-py3-none-any.whl",
        "deps": ["pkg_resources"],
    },
    "IPython": {
        "path": f"{_internal_modules_path}/IPython-0.0.1-py3-none-any.whl",
    },
    "p5": {
        "path": f"{_internal_modules_path}/p5-0.0.1-py3-none-any.whl",
        "deps": ["pkg_resources"],
    },
61
62
63
64
    "tutor": {
        "path": f"{_internal_modules_path}/tutor-0.0.1-py3-none-any.whl",
        "deps": ["pkg_resources"],
    },
65
66
67
68
69
70
    "_ivp": {
        "path": f"{_internal_modules_path}/ivp-0.0.1-py3-none-any.whl",
    },
    "scipy": {  # this force scipy to load _ivp first
        "deps": ["_ivp"],
    },
71
72
    "qrcode": {
        "path": "qrcode",  # loaded from PyPi
Romain Casati's avatar
Romain Casati committed
73
        "deps": ["PIL"],  # to force patching
74
    },
75
76
}

77
78
79
80
# checking that 'path' is in dict allows to add dependencies to pyodide packages
# (see _ivp/scipy)
_internal_pkgs = set(k for k, v in _internal_pkgs_dict.items() if 'path' in v)

81
# Union of internal and pyodide packages.
82
_all_pkgs = _pyodide_pkgs | _internal_pkgs
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

# Packages already loaded.
_loaded = set()


def _load_pyodide(packages):
    """ Load Pyodide packages (async). """
    if isinstance(packages, str):
        packages = [packages]

    if not packages:
        return js.Promise.resolve()
    # from Python name to Pyodide name
    return js.pyodide.loadPackage([_import_name_to_package_name[p]
                                   for p in packages])


def _load_internal(packages):
    """ Load internal module with micropip (async). """
    if isinstance(packages, str):
        packages = [packages]

    if not packages:
        return js.Promise.resolve()

    def micropip_wrapper(*args):
109
        return micropip_install([_internal_pkgs_dict[p]['path']
Romain Casati's avatar
Romain Casati committed
110
                                 for p in packages])
111
112
113
114
115

    return _load_pyodide('micropip').then(micropip_wrapper)


def _internal_dependencies(packages):
116
117
118
119
120
    """
    Return the (ordered) set of dependencies of a list of internal packages.

    Order maters: last elements should be loaded first.
    """
121
    def dependencies(package):
122
        deps = OrderedSet(_internal_pkgs_dict.get(package, {}).get("deps", []))
123
124
        return deps.union(*(dependencies(p) for p in deps))

125
126
    packages = OrderedSet(packages)
    return OrderedSet.union(*(dependencies(p) for p in packages)) - packages
127
128


Romain Casati's avatar
Romain Casati committed
129
130
def load_and_patch(packages):
    """ Load and patch modules (internal or Pyodide). """
131
132
133
134
    if isinstance(packages, str):
        packages = [packages]

    # remove already loaded
135
    packages = OrderedSet(packages) - _loaded
136
137
138
139

    if not packages:
        return js.Promise.resolve()

Romain Casati's avatar
Romain Casati committed
140
    # last dependencies first to ensure deps are resolved when patching
141
142
    # the order is guarenteed by our OrderedSet
    packages = reversed(packages | _internal_dependencies(packages))
143
    pyodide_packages = packages & _pyodide_pkgs
144
    internal_packages = packages & _internal_pkgs
145
146
147
148

    def callback(*args):
        if packages:
            _loaded.update(packages)
Romain Casati's avatar
Romain Casati committed
149
            _patch_modules.patch(packages)
150

151
152
153
    return getattr(js.Promise.all((_load_pyodide(pyodide_packages),
                                   _load_internal(internal_packages))),
                   'finally')(callback)
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172


def find_imports(code):
    """
    Wrapper around pyodide.find_imports.
    """
    if not isinstance(code, str):
        try:
            code = code.tobytes()
        except Exception:
            pass
        code = code.decode()
    return pyodide.find_imports(code)


def micropip_install(packages):
    """ Load packages using micropip and return a promise. """
    import micropip
    return micropip.install(packages)