jupyter-builder 0.1.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+ """Utilities for installing Javascript extensions for the notebook"""
2
+
3
+ # Copyright (c) Jupyter Development Team.
4
+ # Distributed under the terms of the Modified BSD License.
5
+
6
+ import importlib
7
+ import json
8
+ import os
9
+ import os.path as osp
10
+ import platform
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ try:
17
+ from importlib.metadata import PackageNotFoundError, version
18
+ except ImportError:
19
+ from importlib_metadata import PackageNotFoundError, version
20
+
21
+ from os.path import basename, normpath
22
+ from os.path import join as pjoin
23
+
24
+ from jupyter_core.paths import ENV_JUPYTER_PATH, SYSTEM_JUPYTER_PATH, jupyter_data_dir
25
+ from jupyter_core.utils import ensure_dir_exists
26
+ from jupyter_server.extension.serverextension import ArgumentConflict
27
+
28
+ # from jupyterlab_server.config import get_federated_extensions
29
+ from .federated_extensions_requirements import get_federated_extensions
30
+
31
+ try:
32
+ from tomllib import load # Python 3.11+
33
+ except ImportError:
34
+ from tomli import load
35
+
36
+ # from .commands import _test_overlap TO BE DONE -----------------------------
37
+
38
+ DEPRECATED_ARGUMENT = object()
39
+
40
+ HERE = osp.abspath(osp.dirname(__file__))
41
+
42
+
43
+ # ------------------------------------------------------------------------------
44
+ # Public API
45
+ # ------------------------------------------------------------------------------
46
+
47
+
48
+ def develop_labextension( # noqa
49
+ path,
50
+ symlink=True,
51
+ overwrite=False,
52
+ user=False,
53
+ labextensions_dir=None,
54
+ destination=None,
55
+ logger=None,
56
+ sys_prefix=False,
57
+ ):
58
+ """Install a prebuilt extension for JupyterLab
59
+
60
+ Stages files and/or directories into the labextensions directory.
61
+ By default, this compares modification time, and only stages files that need updating.
62
+ If `overwrite` is specified, matching files are purged before proceeding.
63
+
64
+ Parameters
65
+ ----------
66
+
67
+ path : path to file, directory, zip or tarball archive, or URL to install
68
+ By default, the file will be installed with its base name, so '/path/to/foo'
69
+ will install to 'labextensions/foo'. See the destination argument below to change this.
70
+ Archives (zip or tarballs) will be extracted into the labextensions directory.
71
+ user : bool [default: False]
72
+ Whether to install to the user's labextensions directory.
73
+ Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
74
+ overwrite : bool [default: False]
75
+ If True, always install the files, regardless of what may already be installed.
76
+ symlink : bool [default: True]
77
+ If True, create a symlink in labextensions, rather than copying files.
78
+ Windows support for symlinks requires a permission bit which only admin users
79
+ have by default, so don't rely on it.
80
+ labextensions_dir : str [optional]
81
+ Specify absolute path of labextensions directory explicitly.
82
+ destination : str [optional]
83
+ name the labextension is installed to. For example, if destination is 'foo', then
84
+ the source file will be installed to 'labextensions/foo', regardless of the source name.
85
+ logger : Jupyter logger [optional]
86
+ Logger instance to use
87
+ """
88
+ # the actual path to which we eventually installed
89
+ full_dest = None
90
+
91
+ labext = _get_labextension_dir(
92
+ user=user, sys_prefix=sys_prefix, labextensions_dir=labextensions_dir
93
+ )
94
+ # make sure labextensions dir exists
95
+ ensure_dir_exists(labext)
96
+
97
+ if isinstance(path, (list, tuple)):
98
+ msg = "path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions"
99
+ raise TypeError(msg)
100
+
101
+ if not destination:
102
+ destination = basename(normpath(path))
103
+
104
+ full_dest = normpath(pjoin(labext, destination))
105
+ if overwrite and os.path.lexists(full_dest):
106
+ if logger:
107
+ logger.info("Removing: %s" % full_dest)
108
+ if os.path.isdir(full_dest) and not os.path.islink(full_dest):
109
+ shutil.rmtree(full_dest)
110
+ else:
111
+ os.remove(full_dest)
112
+
113
+ # Make sure the parent directory exists
114
+ os.makedirs(os.path.dirname(full_dest), exist_ok=True)
115
+
116
+ if symlink:
117
+ path = os.path.abspath(path)
118
+ if not os.path.exists(full_dest):
119
+ if logger:
120
+ logger.info(f"Symlinking: {full_dest} -> {path}")
121
+ try:
122
+ os.symlink(path, full_dest)
123
+ except OSError as e:
124
+ if platform.platform().startswith("Windows"):
125
+ msg = (
126
+ "Symlinks can be activated on Windows 10 for Python version 3.8 or higher"
127
+ " by activating the 'Developer Mode'. That may not be allowed by your administrators.\n"
128
+ "See https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development"
129
+ )
130
+ raise OSError(msg) from e
131
+ raise
132
+
133
+ elif not os.path.islink(full_dest):
134
+ raise ValueError("%s exists and is not a symlink" % full_dest)
135
+
136
+ elif os.path.isdir(path):
137
+ path = pjoin(os.path.abspath(path), "") # end in path separator
138
+ for parent, _, files in os.walk(path):
139
+ dest_dir = pjoin(full_dest, parent[len(path) :])
140
+ if not os.path.exists(dest_dir):
141
+ if logger:
142
+ logger.info("Making directory: %s" % dest_dir)
143
+ os.makedirs(dest_dir)
144
+ for file_name in files:
145
+ src = pjoin(parent, file_name)
146
+ dest_file = pjoin(dest_dir, file_name)
147
+ _maybe_copy(src, dest_file, logger=logger)
148
+ else:
149
+ src = path
150
+ _maybe_copy(src, full_dest, logger=logger)
151
+
152
+ return full_dest
153
+
154
+
155
+ def develop_labextension_py(
156
+ module,
157
+ user=False,
158
+ sys_prefix=False,
159
+ overwrite=True,
160
+ symlink=True,
161
+ labextensions_dir=None,
162
+ logger=None,
163
+ ):
164
+ """Develop a labextension bundled in a Python package.
165
+
166
+ Returns a list of installed/updated directories.
167
+
168
+ See develop_labextension for parameter information."""
169
+ m, labexts = _get_labextension_metadata(module)
170
+ base_path = os.path.split(m.__file__)[0]
171
+
172
+ full_dests = []
173
+
174
+ for labext in labexts:
175
+ src = os.path.join(base_path, labext["src"])
176
+ dest = labext["dest"]
177
+ if logger:
178
+ logger.info(f"Installing {src} -> {dest}")
179
+
180
+ if not os.path.exists(src):
181
+ build_labextension(base_path, logger=logger)
182
+
183
+ full_dest = develop_labextension(
184
+ src,
185
+ overwrite=overwrite,
186
+ symlink=symlink,
187
+ user=user,
188
+ sys_prefix=sys_prefix,
189
+ labextensions_dir=labextensions_dir,
190
+ destination=dest,
191
+ logger=logger,
192
+ )
193
+ full_dests.append(full_dest)
194
+
195
+ return full_dests
196
+
197
+
198
+ from .core_path import default_core_path
199
+
200
+
201
+ def build_labextension(
202
+ path, logger=None, development=False, static_url=None, source_map=False, core_path=None
203
+ ):
204
+ """Build a labextension in the given path"""
205
+
206
+ core_path = default_core_path() if core_path is None else str(Path(core_path).resolve())
207
+
208
+ ext_path = str(Path(path).resolve())
209
+
210
+ if logger:
211
+ logger.info("Building extension in %s" % path)
212
+
213
+ builder = _ensure_builder(ext_path, core_path)
214
+
215
+ arguments = ["node", builder, "--core-path", core_path, ext_path]
216
+ if static_url is not None:
217
+ arguments.extend(["--static-url", static_url])
218
+ if development:
219
+ arguments.append("--development")
220
+ if source_map:
221
+ arguments.append("--source-map")
222
+
223
+ subprocess.check_call(arguments, cwd=ext_path) # noqa S603
224
+
225
+
226
+ def watch_labextension(
227
+ path, labextensions_path, logger=None, development=False, source_map=False, core_path=None
228
+ ):
229
+ """Watch a labextension in a given path"""
230
+ core_path = default_core_path() if core_path is None else str(Path(core_path).resolve())
231
+ ext_path = str(Path(path).resolve())
232
+
233
+ if logger:
234
+ logger.info("Building extension in %s" % path)
235
+
236
+ # Check to see if we need to create a symlink
237
+ federated_extensions = get_federated_extensions(labextensions_path)
238
+
239
+ with open(pjoin(ext_path, "package.json")) as fid:
240
+ ext_data = json.load(fid)
241
+
242
+ if ext_data["name"] not in federated_extensions:
243
+ develop_labextension_py(ext_path, sys_prefix=True)
244
+ else:
245
+ full_dest = pjoin(federated_extensions[ext_data["name"]]["ext_dir"], ext_data["name"])
246
+ output_dir = pjoin(ext_path, ext_data["jupyterlab"].get("outputDir", "static"))
247
+ if not osp.islink(full_dest):
248
+ shutil.rmtree(full_dest)
249
+ os.symlink(output_dir, full_dest)
250
+
251
+ builder = _ensure_builder(ext_path, core_path)
252
+ arguments = ["node", builder, "--core-path", core_path, "--watch", ext_path]
253
+ if development:
254
+ arguments.append("--development")
255
+ if source_map:
256
+ arguments.append("--source-map")
257
+
258
+ subprocess.check_call(arguments, cwd=ext_path) # noqa S603
259
+
260
+
261
+ # ------------------------------------------------------------------------------
262
+ # Private API
263
+ # ------------------------------------------------------------------------------
264
+
265
+
266
+ def _ensure_builder(ext_path, core_path):
267
+ """Ensure that we can build the extension and return the builder script path"""
268
+ # Test for compatible dependency on @jupyterlab/builder
269
+ with open(osp.join(core_path, "package.json")) as fid:
270
+ core_data = json.load(fid)
271
+ with open(osp.join(ext_path, "package.json")) as fid:
272
+ ext_data = json.load(fid)
273
+ dep_version1 = core_data["devDependencies"]["@jupyterlab/builder"]
274
+ dep_version2 = ext_data.get("devDependencies", {}).get("@jupyterlab/builder")
275
+ dep_version2 = dep_version2 or ext_data.get("dependencies", {}).get("@jupyterlab/builder")
276
+ if dep_version2 is None:
277
+ raise ValueError(
278
+ "Extensions require a devDependency on @jupyterlab/builder@%s" % dep_version1
279
+ )
280
+
281
+ # if we have installed from disk (version is a path), assume we know what
282
+ # we are doing and do not check versions.
283
+ if "/" in dep_version2:
284
+ with open(osp.join(ext_path, dep_version2, "package.json")) as fid:
285
+ dep_version2 = json.load(fid).get("version")
286
+ if not osp.exists(osp.join(ext_path, "node_modules")):
287
+ subprocess.check_call(["jlpm"], cwd=ext_path) # noqa S603 S607
288
+
289
+ # Find @jupyterlab/builder using node module resolution
290
+ # We cannot use a script because the script path is a shell script on Windows
291
+ target = ext_path
292
+ while not osp.exists(osp.join(target, "node_modules", "@jupyterlab", "builder")):
293
+ if osp.dirname(target) == target:
294
+ msg = "Could not find @jupyterlab/builder"
295
+ raise ValueError(msg)
296
+ target = osp.dirname(target)
297
+
298
+ # IGNORING Test Overlap ---------------------------------
299
+
300
+ # overlap = _test_overlap(
301
+ # dep_version1, dep_version2, drop_prerelease1=True, drop_prerelease2=True
302
+ # )
303
+ # if not overlap:
304
+ # with open(
305
+ # osp.join(target, "node_modules", "@jupyterlab", "builder", "package.json")
306
+ # ) as fid:
307
+ # dep_version2 = json.load(fid).get("version")
308
+ # overlap = _test_overlap(
309
+ # dep_version1, dep_version2, drop_prerelease1=True, drop_prerelease2=True
310
+ # )
311
+
312
+ # if not overlap:
313
+ # msg = f"Extensions require a devDependency on @jupyterlab/builder@{dep_version1}, you have a dependency on {dep_version2}"
314
+ # raise ValueError(msg)
315
+
316
+ return osp.join(
317
+ target, "node_modules", "@jupyterlab", "builder", "lib", "build-labextension.js"
318
+ )
319
+
320
+
321
+ def _should_copy(src, dest, logger=None):
322
+ """Should a file be copied, if it doesn't exist, or is newer?
323
+
324
+ Returns whether the file needs to be updated.
325
+
326
+ Parameters
327
+ ----------
328
+
329
+ src : string
330
+ A path that should exist from which to copy a file
331
+ src : string
332
+ A path that might exist to which to copy a file
333
+ logger : Jupyter logger [optional]
334
+ Logger instance to use
335
+ """
336
+ if not os.path.exists(dest):
337
+ return True
338
+ if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6: # noqa
339
+ # we add a fudge factor to work around a bug in python 2.x
340
+ # that was fixed in python 3.x: https://bugs.python.org/issue12904
341
+ if logger:
342
+ logger.warning("Out of date: %s" % dest)
343
+ return True
344
+ if logger:
345
+ logger.info("Up to date: %s" % dest)
346
+ return False
347
+
348
+
349
+ def _maybe_copy(src, dest, logger=None):
350
+ """Copy a file if it needs updating.
351
+
352
+ Parameters
353
+ ----------
354
+
355
+ src : string
356
+ A path that should exist from which to copy a file
357
+ src : string
358
+ A path that might exist to which to copy a file
359
+ logger : Jupyter logger [optional]
360
+ Logger instance to use
361
+ """
362
+ if _should_copy(src, dest, logger=logger):
363
+ if logger:
364
+ logger.info(f"Copying: {src} -> {dest}")
365
+ shutil.copy2(src, dest)
366
+
367
+
368
+ def _get_labextension_dir(user=False, sys_prefix=False, prefix=None, labextensions_dir=None):
369
+ """Return the labextension directory specified
370
+
371
+ Parameters
372
+ ----------
373
+
374
+ user : bool [default: False]
375
+ Get the user's .jupyter/labextensions directory
376
+ sys_prefix : bool [default: False]
377
+ Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/labextensions
378
+ prefix : str [optional]
379
+ Get custom prefix
380
+ labextensions_dir : str [optional]
381
+ Get what you put in
382
+ """
383
+ conflicting = [
384
+ ("user", user),
385
+ ("prefix", prefix),
386
+ ("labextensions_dir", labextensions_dir),
387
+ ("sys_prefix", sys_prefix),
388
+ ]
389
+ conflicting_set = [f"{n}={v!r}" for n, v in conflicting if v]
390
+ if len(conflicting_set) > 1:
391
+ msg = "cannot specify more than one of user, sys_prefix, prefix, or labextensions_dir, but got: {}".format(
392
+ ", ".join(conflicting_set)
393
+ )
394
+ raise ArgumentConflict(msg)
395
+ if user:
396
+ labext = pjoin(jupyter_data_dir(), "labextensions")
397
+ elif sys_prefix:
398
+ labext = pjoin(ENV_JUPYTER_PATH[0], "labextensions")
399
+ elif prefix:
400
+ labext = pjoin(prefix, "share", "jupyter", "labextensions")
401
+ elif labextensions_dir:
402
+ labext = labextensions_dir
403
+ else:
404
+ labext = pjoin(SYSTEM_JUPYTER_PATH[0], "labextensions")
405
+ return labext
406
+
407
+
408
+ def _get_labextension_metadata(module): # noqa
409
+ """Get the list of labextension paths associated with a Python module.
410
+
411
+ Returns a tuple of (the module path, [{
412
+ 'src': 'mockextension',
413
+ 'dest': '_mockdestination'
414
+ }])
415
+
416
+ Parameters
417
+ ----------
418
+
419
+ module : str
420
+ Importable Python module exposing the
421
+ magic-named `_jupyter_labextension_paths` function
422
+ """
423
+ mod_path = osp.abspath(module)
424
+ if not osp.exists(mod_path):
425
+ msg = f"The path `{mod_path}` does not exist."
426
+ # breakpoint()
427
+ raise FileNotFoundError(msg)
428
+
429
+ errors = []
430
+
431
+ # Check if the path is a valid labextension
432
+ try:
433
+ m = importlib.import_module(module)
434
+ if hasattr(m, "_jupyter_labextension_paths"):
435
+ return m, m._jupyter_labextension_paths()
436
+ except Exception as exc:
437
+ errors.append(exc)
438
+
439
+ # Try to get the package name
440
+ package = None
441
+
442
+ # Try getting the package name from pyproject.toml
443
+ if os.path.exists(os.path.join(mod_path, "pyproject.toml")):
444
+ with open(os.path.join(mod_path, "pyproject.toml"), "rb") as fid:
445
+ data = load(fid)
446
+ package = data.get("project", {}).get("name")
447
+
448
+ # Try getting the package name from setup.py
449
+ if not package:
450
+ try:
451
+ package = (
452
+ subprocess.check_output(
453
+ [sys.executable, "setup.py", "--name"], # noqa S603
454
+ cwd=mod_path,
455
+ )
456
+ .decode("utf8")
457
+ .strip()
458
+ )
459
+ except subprocess.CalledProcessError:
460
+ msg = (
461
+ f"The Python package `{module}` is not a valid package, "
462
+ "it is missing the `setup.py` file."
463
+ )
464
+ raise FileNotFoundError(msg) from None
465
+
466
+ # Make sure the package is installed
467
+ try:
468
+ version(package)
469
+ except PackageNotFoundError:
470
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", mod_path]) # noqa S603
471
+ sys.path.insert(0, mod_path)
472
+
473
+ from setuptools import find_namespace_packages, find_packages
474
+
475
+ package_candidates = [
476
+ package.replace("-", "_"), # Module with the same name as package
477
+ ]
478
+ package_candidates.extend(find_packages(mod_path)) # Packages in the module path
479
+ package_candidates.extend(
480
+ find_namespace_packages(mod_path)
481
+ ) # Namespace packages in the module path
482
+
483
+ for package in package_candidates:
484
+ try:
485
+ m = importlib.import_module(package)
486
+ if hasattr(m, "_jupyter_labextension_paths"):
487
+ return m, m._jupyter_labextension_paths()
488
+ except Exception as exc:
489
+ errors.append(exc)
490
+
491
+ msg = f"There is no labextension at {module}. Errors encountered: {errors}"
492
+ raise ModuleNotFoundError(msg)
@@ -0,0 +1,74 @@
1
+ """JupyterLab Server config"""
2
+
3
+ # Copyright (c) Jupyter Development Team.
4
+ # Distributed under the terms of the Modified BSD License.
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os.path as osp
9
+ from glob import iglob
10
+ from itertools import chain
11
+
12
+ # from logging import Logger
13
+ from os.path import join as pjoin
14
+ from typing import Any
15
+
16
+ # import json5 # type:ignore[import-untyped]
17
+ # from jupyter_core.paths import SYSTEM_CONFIG_PATH, jupyter_config_dir, jupyter_path
18
+ # from jupyter_server.services.config.manager import ConfigManager, recursive_update
19
+ # from jupyter_server.utils import url_path_join as ujoin
20
+ # from traitlets import Bool, HasTraits, List, Unicode, default
21
+
22
+ # -----------------------------------------------------------------------------
23
+ # Module globals
24
+ # -----------------------------------------------------------------------------
25
+
26
+ DEFAULT_TEMPLATE_PATH = osp.join(osp.dirname(__file__), "templates")
27
+
28
+
29
+ def get_package_url(data: dict[str, Any]) -> str:
30
+ """Get the url from the extension data"""
31
+ # homepage, repository are optional
32
+ if "homepage" in data:
33
+ url = data["homepage"]
34
+ elif "repository" in data and isinstance(data["repository"], dict):
35
+ url = data["repository"].get("url", "")
36
+ else:
37
+ url = ""
38
+ return url
39
+
40
+
41
+ def get_federated_extensions(labextensions_path: list[str]) -> dict[str, Any]:
42
+ """Get the metadata about federated extensions"""
43
+ federated_extensions = {}
44
+ for ext_dir in labextensions_path:
45
+ # extensions are either top-level directories, or two-deep in @org directories
46
+ for ext_path in chain(
47
+ iglob(pjoin(ext_dir, "[!@]*", "package.json")),
48
+ iglob(pjoin(ext_dir, "@*", "*", "package.json")),
49
+ ):
50
+ with open(ext_path, encoding="utf-8") as fid:
51
+ pkgdata = json.load(fid)
52
+ if pkgdata["name"] not in federated_extensions:
53
+ data = dict(
54
+ name=pkgdata["name"],
55
+ version=pkgdata["version"],
56
+ description=pkgdata.get("description", ""),
57
+ url=get_package_url(pkgdata),
58
+ ext_dir=ext_dir,
59
+ ext_path=osp.dirname(ext_path),
60
+ is_local=False,
61
+ dependencies=pkgdata.get("dependencies", dict()),
62
+ jupyterlab=pkgdata.get("jupyterlab", dict()),
63
+ )
64
+
65
+ # Add repository info if available
66
+ if "repository" in pkgdata and "url" in pkgdata.get("repository", {}):
67
+ data["repository"] = dict(url=pkgdata.get("repository").get("url"))
68
+
69
+ install_path = osp.join(osp.dirname(ext_path), "install.json")
70
+ if osp.exists(install_path):
71
+ with open(install_path, encoding="utf-8") as fid:
72
+ data["install"] = json.load(fid)
73
+ federated_extensions[data["name"]] = data
74
+ return federated_extensions
@@ -0,0 +1,43 @@
1
+ """A Jupyter-aware wrapper for the yarn package manager"""
2
+
3
+ import os
4
+
5
+ # Copyright (c) Jupyter Development Team.
6
+ # Distributed under the terms of the Modified BSD License.
7
+ import sys
8
+
9
+ from jupyterlab_server.process import subprocess, which
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+ YARN_PATH = os.path.join(HERE, "yarn.js")
13
+
14
+
15
+ def execvp(cmd, argv):
16
+ """Execvp, except on Windows where it uses Popen.
17
+
18
+ The first argument, by convention, should point to the filename
19
+ associated with the file being executed.
20
+
21
+ Python provides execvp on Windows, but its behavior is problematic
22
+ (Python bug#9148).
23
+ """
24
+ cmd = which(cmd)
25
+ if os.name == "nt":
26
+ import signal
27
+ import sys
28
+
29
+ p = subprocess.Popen([cmd] + argv[1:])
30
+ # Don't raise KeyboardInterrupt in the parent process.
31
+ # Set this after spawning, to avoid subprocess inheriting handler.
32
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
33
+ p.wait()
34
+ sys.exit(p.returncode)
35
+ else:
36
+ os.execvp(cmd, argv) # noqa S606
37
+
38
+
39
+ def main(argv=None):
40
+ """Run node and return the result."""
41
+ # Make sure node is available.
42
+ argv = argv or sys.argv[1:]
43
+ execvp("node", ["node", YARN_PATH, *argv])