diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bad1151..2f7947b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: set -eux jlpm jlpm run build - jlpm run eslint:check + jlpm run lint:check python -m pip install -v . jupyter labextension list 2>&1 | grep -ie "@jupyterlite/xeus-python-kernel.*OK" diff --git a/docs/conf.py b/docs/conf.py index c14246c..ba01ba2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - extensions = [ "jupyterlite_sphinx", "myst_parser", diff --git a/environment.yml b/environment.yml index 23f371b..9de7215 100644 --- a/environment.yml +++ b/environment.yml @@ -14,3 +14,5 @@ dependencies: - empack >=3,<4 - nodejs=20 - mamba + - black + - ruff diff --git a/jupyterlite_xeus_python/__init__.py b/jupyterlite_xeus_python/__init__.py index aeb85c0..f5ea975 100644 --- a/jupyterlite_xeus_python/__init__.py +++ b/jupyterlite_xeus_python/__init__.py @@ -5,12 +5,12 @@ # in editable mode with pip. It is highly recommended to install # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs import warnings - warnings.warn("Importing 'jupyterlite-xeus-python' outside a proper installation.") + + warnings.warn( + "Importing 'jupyterlite-xeus-python' outside a proper installation.", stacklevel=2 + ) __version__ = "dev" def _jupyter_labextension_paths(): - return [{ - "src": "labextension", - "dest": "@jupyterlite/xeus-python-kernel" - }] + return [{"src": "labextension", "dest": "@jupyterlite/xeus-python-kernel"}] diff --git a/jupyterlite_xeus_python/build.py b/jupyterlite_xeus_python/build.py index 098bcb6..4015a66 100644 --- a/jupyterlite_xeus_python/build.py +++ b/jupyterlite_xeus_python/build.py @@ -1,21 +1,18 @@ import csv import os -from copy import copy -from pathlib import Path -import requests import shutil -from subprocess import check_call, run, DEVNULL +import sys +from pathlib import Path +from subprocess import run from tempfile import TemporaryDirectory -from typing import List +from typing import List, Optional from urllib.parse import urlparse -import sys +import requests +import typer import yaml - -from empack.pack import pack_env, DEFAULT_CONFIG_PATH from empack.file_patterns import PkgFileFilter, pkg_file_filter_from_yaml - -import typer +from empack.pack import DEFAULT_CONFIG_PATH, pack_env try: from mamba.api import create as mamba_create @@ -40,6 +37,7 @@ ] PLATFORM = "emscripten-32" +DEFAULT_REQUEST_TIMEOUT = 1 # in minutes def create_env( @@ -131,10 +129,12 @@ def _install_pip_dependencies(prefix_path, dependencies, log=None): if log is not None: log.warning( """ - Installing pip dependencies. This is very much experimental so use this feature at your own risks. + Installing pip dependencies. This is very much experimental so use + this feature at your own risks. Note that you can only install pure-python packages. - pip is being run with the --no-deps option to not pull undesired system-specific dependencies, so please - install your package dependencies from emscripten-forge or conda-forge. + pip is being run with the --no-deps option to not pull undesired + system-specific dependencies, so please install your package dependencies + from emscripten-forge or conda-forge. """ ) @@ -144,6 +144,8 @@ def _install_pip_dependencies(prefix_path, dependencies, log=None): run( [ + sys.executable, + "-m", "pip", "install", *dependencies, @@ -166,7 +168,7 @@ def _install_pip_dependencies(prefix_path, dependencies, log=None): packages_dist_info = Path(pkg_dir.name).glob("*.dist-info") for package_dist_info in packages_dist_info: - with open(package_dist_info / "RECORD", "r") as record: + with open(package_dist_info / "RECORD") as record: record_content = record.read() record_csv = csv.reader(record_content.splitlines()) all_files = [_file[0] for _file in record_csv] @@ -181,7 +183,7 @@ def _install_pip_dependencies(prefix_path, dependencies, log=None): with open(package_dist_info / "RECORD", "w") as record: record.write(fixed_record_data) - non_supported_files = [".so", ".a", ".dylib", ".lib", ".exe" ".dll"] + non_supported_files = [".so", ".a", ".dylib", ".lib", ".exe.dll"] # COPY files under `prefix_path` for _file, inside_site_packages in files: @@ -208,10 +210,10 @@ def _install_pip_dependencies(prefix_path, dependencies, log=None): shutil.copy(src_path, dest_path) -def build_and_pack_emscripten_env( +def build_and_pack_emscripten_env( # noqa: C901, PLR0912, PLR0915 python_version: str = PYTHON_VERSION, xeus_python_version: str = XEUS_PYTHON_VERSION, - packages: List[str] = [], + packages: Optional[List[str]] = None, environment_file: str = "", root_prefix: str = "/tmp/xeus-python-kernel", env_name: str = "xeus-python-kernel", @@ -222,13 +224,13 @@ def build_and_pack_emscripten_env( log=None, ): """Build a conda environment for the emscripten platform and pack it with empack.""" + if packages is None: + packages = [] channels = CHANNELS specs = [ f"python={python_version}", "xeus-lite", - "xeus-python" - if not xeus_python_version - else f"xeus-python={xeus_python_version}", + "xeus-python" if not xeus_python_version else f"xeus-python={xeus_python_version}", *packages, ] bail_early = True @@ -261,13 +263,17 @@ def build_and_pack_emscripten_env( elif isinstance(dependency, dict) and dependency.get("pip") is not None: # If it's a local Python package, make its path relative to the environment file pip_dependencies = [ - ((env_file.parent / pip_dep).resolve() if os.path.isdir(env_file.parent / pip_dep) else pip_dep) + ( + (env_file.parent / pip_dep).resolve() + if os.path.isdir(env_file.parent / pip_dep) + else pip_dep + ) for pip_dep in dependency["pip"] ] # Bail early if there is nothing to do if bail_early and not force: - return [] + return "" orig_config = os.environ.get("CONDARC") @@ -294,7 +300,9 @@ def build_and_pack_emscripten_env( if empack_config: empack_config_is_url = urlparse(empack_config).scheme in ("http", "https") if empack_config_is_url: - empack_config_content = requests.get(empack_config).content + empack_config_content = requests.get( + empack_config, timeout=DEFAULT_REQUEST_TIMEOUT + ).content pack_kwargs["file_filters"] = PkgFileFilter.parse_obj( yaml.safe_load(empack_config_content) ) @@ -324,7 +332,7 @@ def build_and_pack_emscripten_env( dirs_exist_ok=True, ) - with open(Path(output_path) / "worker.ts", "r") as fobj: + with open(Path(output_path) / "worker.ts") as fobj: worker = fobj.read() worker = worker.replace("XEUS_KERNEL_FILE", "'xpython_wasm.js'") @@ -356,9 +364,7 @@ def build_and_pack_emscripten_env( def main( python_version: str = PYTHON_VERSION, xeus_python_version: str = XEUS_PYTHON_VERSION, - packages: List[str] = typer.Option( - [], help="The list of packages you want to install" - ), + packages: List[str] = typer.Option([], help="The list of packages you want to install"), environment_file: str = typer.Option( "", help="The path to the environment.yml file you want to use" ), diff --git a/jupyterlite_xeus_python/env_build_addon.py b/jupyterlite_xeus_python/env_build_addon.py index 9cbc482..50fe087 100644 --- a/jupyterlite_xeus_python/env_build_addon.py +++ b/jupyterlite_xeus_python/env_build_addon.py @@ -2,27 +2,17 @@ import json import os from pathlib import Path -import requests -import shutil -from subprocess import check_call, run, DEVNULL from tempfile import TemporaryDirectory -from urllib.parse import urlparse - -import yaml - -from traitlets import List, Unicode - -from empack.pack import pack_env, DEFAULT_CONFIG_PATH -from empack.file_patterns import PkgFileFilter, pkg_file_filter_from_yaml +from jupyterlite_core.addons.federated_extensions import FederatedExtensionAddon from jupyterlite_core.constants import ( - SHARE_LABEXTENSIONS, - LAB_EXTENSIONS, + FEDERATED_EXTENSIONS, JUPYTERLITE_JSON, + LAB_EXTENSIONS, + SHARE_LABEXTENSIONS, UTF8, - FEDERATED_EXTENSIONS, ) -from jupyterlite_core.addons.federated_extensions import FederatedExtensionAddon +from traitlets import List, Unicode from .build import XEUS_PYTHON_VERSION, build_and_pack_emscripten_env @@ -42,9 +32,8 @@ class XeusPythonEnv(FederatedExtensionAddon): ) empack_config = Unicode( - None, + "", config=True, - allow_none=True, description="The path or URL to the empack config file", ) @@ -64,7 +53,7 @@ class XeusPythonEnv(FederatedExtensionAddon): ) def __init__(self, *args, **kwargs): - super(XeusPythonEnv, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.cwd = TemporaryDirectory() @@ -72,7 +61,9 @@ def post_build(self, manager): """yield a doit task to create the emscripten-32 env and grab anything we need from it""" try: # JupyterLite 0.1.x - from jupyterlite_core.addons.federated_extensions import ENV_EXTENSIONS as env_extensions + from jupyterlite_core.addons.federated_extensions import ENV_EXTENSIONS + + env_extensions = ENV_EXTENSIONS except ImportError: # JupyterLite 0.2.x env_extensions = self.labextensions_path @@ -140,9 +131,7 @@ def safe_copy_extension(self, pkg_json): stem = json.loads(pkg_json.read_text(**UTF8))["name"] dest = self.output_extensions / stem file_dep = [ - p - for p in pkg_path.rglob("*") - if not (p.is_dir() or self.is_ignored_sourcemap(p.name)) + p for p in pkg_path.rglob("*") if not (p.is_dir() or self.is_ignored_sourcemap(p.name)) ] yield dict( diff --git a/package.json b/package.json index 4aca277..da861f9 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,14 @@ "eslint": "jlpm eslint:check --fix", "eslint:check": "eslint . --cache --ext .ts,.tsx", "install:extension": "jlpm build", - "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", - "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint && jlpm lint:py", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check && jlpm lint:py:check", + "lint:py": "jlpm lint:py:black && jlpm lint:py:ruff --fix-only", + "lint:py:check": "jlpm lint:py:black:check && jlpm lint:py:ruff:check", + "lint:py:black": "black .", + "lint:py:black:check": "black . --check", + "lint:py:ruff": "ruff .", + "lint:py:ruff:check": "ruff check .", "prettier": "jlpm prettier:base --write --list-different", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", diff --git a/pyproject.toml b/pyproject.toml index 02dfe25..ffc27c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,12 @@ dependencies = [ ] dynamic = ["version", "description", "authors", "urls", "keywords"] +[project.optional-dependencies] +dev = [ + "black", + "ruff==0.0.292", +] + [project.scripts] jupyterlite-xeus-python-build = "jupyterlite_xeus_python.build:start" @@ -85,3 +91,51 @@ before-build-python = ["jlpm clean:all"] [tool.check-wheel-contents] ignore = ["W002"] + +[tool.black] +line-length = 100 +target-version = ["py38"] + +[tool.ruff] +target-version = "py38" +exclude = [ + "*/tests/*", + "docs", +] +line-length = 100 +select = [ + "A", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "ISC", "N", + "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "SIM", "T", "TID", "UP", + "W", "YTT", +] +ignore = [ + "D100", + "D104", + # Q000 Single quotes found but double quotes preferred + "Q000", + # FBT001 Boolean positional arg in function definition + "FBT001", "FBT002", "FBT003", + # C408 Unnecessary `dict` call (rewrite as a literal) + "C408", "C416", + # allow for using print() + "T201", + # PLR0913 Too many arguments to function call + "PLR0913", + # extended flake8 ignore + "D104", "D100", "EM101", + # Probable insecure usage of temporary file or directory + "S108", + # RUF012 Mutable class attributes should be annotated with `typing.ClassVar` + "RUF012", +] + +[tool.ruff.per-file-ignores] +# S101 Use of `assert` detected +# F841 Local variable `foo` is assigned to but never used +# PLR2004 Magic value used in comparison +"tests/*" = ["S101", "F841", "PLR2004"] + +# B008 Do not perform function call `typer.Option` in argument defaults +# E501 `subprocess` call: check for execution of untrusted input +# S603 `subprocess` call: check for execution of untrusted input +"jupyterlite_xeus_python/build.py" = ["B008", "E501", "S603"] diff --git a/style/base.css b/style/base.css index 0a36db0..51b9d12 100644 --- a/style/base.css +++ b/style/base.css @@ -1,5 +1,5 @@ -/*----------------------------------------------------------------------------- +/* ----------------------------------------------------------------------------- | Copyright (c) Thorsten Beier | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ +|---------------------------------------------------------------------------- */ diff --git a/style/index.css b/style/index.css index a2cb1d1..88a516e 100644 --- a/style/index.css +++ b/style/index.css @@ -1,7 +1,7 @@ -/*----------------------------------------------------------------------------- +/* ----------------------------------------------------------------------------- | Copyright (c) Thorsten Beier | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ +|---------------------------------------------------------------------------- */ @import url('base.css'); diff --git a/tests/test_package/test_package/hey.py b/tests/test_package/test_package/hey.py index fc2deb4..71910a6 100644 --- a/tests/test_package/test_package/hey.py +++ b/tests/test_package/test_package/hey.py @@ -1 +1 @@ -print('Hey') +print("Hey") diff --git a/tests/test_xeus_python_env.py b/tests/test_xeus_python_env.py index e4b5375..4bf32c5 100644 --- a/tests/test_xeus_python_env.py +++ b/tests/test_xeus_python_env.py @@ -1,11 +1,9 @@ """Test creating Python envs for jupyterlite-xeus-python.""" import os -from tempfile import TemporaryDirectory from pathlib import Path import pytest - from jupyterlite_core.app import LiteStatusApp from jupyterlite_xeus_python.env_build_addon import XeusPythonEnv @@ -19,18 +17,14 @@ def test_python_env(): addon = XeusPythonEnv(manager) addon.packages = ["numpy", "ipyleaflet"] - for step in addon.post_build(manager): + for _step in addon.post_build(manager): pass # Check env assert os.path.isdir("/tmp/xeus-python-kernel/envs/xeus-python-kernel") - assert os.path.isfile( - "/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.js" - ) - assert os.path.isfile( - "/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.wasm" - ) + assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.js") + assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.wasm") # Check empack output assert os.path.isfile(Path(addon.cwd.name) / "empack_env_meta.json") @@ -46,23 +40,17 @@ def test_python_env_from_file_1(): addon = XeusPythonEnv(manager) addon.environment_file = "environment-1.yml" - for step in addon.post_build(manager): + for _step in addon.post_build(manager): pass # Check env assert os.path.isdir("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1") - assert os.path.isfile( - "/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.js" - ) - assert os.path.isfile( - "/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.wasm" - ) + assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.js") + assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.wasm") # Checking pip packages - assert os.path.isdir( - "/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/lib/python3.10" - ) + assert os.path.isdir("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/lib/python3.10") assert os.path.isdir( "/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/lib/python3.10/site-packages" ) @@ -98,7 +86,7 @@ def test_python_env_from_file_3(): addon = XeusPythonEnv(manager) addon.environment_file = "test_package/environment-3.yml" - for step in addon.post_build(manager): + for _step in addon.post_build(manager): pass # Test @@ -121,5 +109,5 @@ def test_python_env_from_file_2(): addon.environment_file = "environment-2.yml" with pytest.raises(RuntimeError, match="Cannot install binary PyPI package"): - for step in addon.post_build(manager): + for _step in addon.post_build(manager): pass