diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2191cf9..b3a31fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# This workflow will install Python dependencies, run tests and lint with a +# variety of Python versions +# +# For more information see: +# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package @@ -9,38 +12,130 @@ on: pull_request: branches: [ master ] +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +defaults: + run: + shell: bash -l {0} + jobs: build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: 3.9 - runs-on: ${{ matrix.os }} + - name: Upgrade packaging deps + run: | + set -eux + python -m pip install --upgrade --user pip wheel setuptools + + - name: Build distributions + run: | + set -eux + python setup.py sdist bdist_wheel + cd dist + sha256sum * | tee SHA256SUMS + + - name: publish dists + uses: actions/upload-artifact@v2 + with: + name: jupyter_core dist ${{ github.run_number }} + path: ./dist + + test: + needs: [build] + runs-on: ${{ matrix.os }}-latest strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + os: [ubuntu, macos, windows] + python-version: [3.6, 3.7, 3.8, 3.9, pypy3] + include: + - os: ubuntu + py-cmd: python + - os: macos + py-cmd: python3 + - os: windows + py-cmd: python + # different dists + - python-version: 3.6 + dist: jupyter_core*.tar.gz + - python-version: 3.7 + dist: jupyter_core*.whl + - python-version: 3.8 + dist: jupyter_core*.tar.gz + - python-version: 3.9 + dist: jupyter_core*.whl + - python-version: pypy3 + dist: jupyter_core*.tar.gz + exclude: + - os: windows + python-version: pypy3 steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: pip cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('dev-requirements.txt', 'setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade setuptools - pip install --upgrade nose - pip install -r dev-requirements.txt . - - name: Test with pytest - run: | - pytest jupyter_core + - uses: actions/checkout@v2 + + - uses: actions/download-artifact@v2 + with: + name: jupyter_core dist ${{ github.run_number }} + path: ./dist + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade packaging deps + run: | + ${{ matrix.py-cmd }} -m pip install --upgrade pip wheel setuptools + + - name: Install distribution and test dependencies + run: | + set -eux + cd dist + ls + ${{ matrix.py-cmd }} -m pip install -r ../dev-requirements.txt + ${{ matrix.py-cmd }} -m pip install --ignore-installed $(ls ${{ matrix.dist }}) + ${{ matrix.py-cmd }} -m pip freeze + ${{ matrix.py-cmd }} -m pip check + + - name: Test with pytest + run: | + set -eux + cd dist + ${{ matrix.py-cmd }} -m pytest -vv --ff --pyargs jupyter_core \ + --cov=jupyter_core \ + --cov-report=term-missing:skip-covered \ + --cov-report=html \ + --no-cov-on-fail + + - name: Upload coverage + if: ${{ matrix.python-version != 'pypy3' }} + run: | + set -eux + cd dist + ${{ matrix.py-cmd }} -m pip install -vv codecov + ${{ matrix.py-cmd }} -m codecov + + - name: Test setuptools example (develop) + run: | + set -eux + cd examples/jupyter_path_entrypoint_setuptools + ${{ matrix.py-cmd }} -m pip install -e . + ${{ matrix.py-cmd }} -m jupyter --paths | tee paths.txt + cat paths.txt | grep entry_point_example_setuptools.share + cat paths.txt | grep entry_point_example_setuptools.etc + + - name: Test flit example (develop) + run: | + set -eux + ${{ matrix.py-cmd }} -m pip install flit + cd examples/jupyter_path_entrypoint_flit + ${{ matrix.py-cmd }} -m flit install --pth-file + ${{ matrix.py-cmd }} -m jupyter --paths | tee paths.txt + cat paths.txt | grep entry_point_example_flit.share + cat paths.txt | grep entry_point_example_flit.etc diff --git a/MANIFEST.in b/MANIFEST.in index d9dada4..f20cb3e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ graft jupyter_core/tests/dotipython_empty # docs subdirs we want to skip prune docs/_build +prune examples # Patterns to exclude from any directory global-exclude *~ diff --git a/dev-requirements.txt b/dev-requirements.txt index 4ccaa98..2f6ffa2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ -pytest ipykernel +pytest +pytest-cov diff --git a/docs/changelog.rst b/docs/changelog.rst index 935eedb..6f7ada6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,19 @@ Changes in jupyter-core ======================= +4.8 +--- + +4.8.0 +~~~~~ + +`on +GitHub `__ + +- Add new ``jupyter_data_path`` and ``jupyter_config_path`` ``entry_points`` + (:ghpull:`209`) to allow python packages to extend the data and config paths + in ``jupyter --paths``. These paths are considered immediately after those put + in-place with ``data_files``, but work with modern python packaging tools. 4.7 --- diff --git a/examples/jupyter_path_entrypoint_flit/COPYING.md b/examples/jupyter_path_entrypoint_flit/COPYING.md new file mode 100644 index 0000000..e5ae344 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/COPYING.md @@ -0,0 +1,61 @@ +# The Jupyter licensing terms + +Jupyter is licensed under the terms of the Modified BSD License (also known as +New or Revised or 3-Clause BSD), as follows: + +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Jupyter Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## About the Jupyter Development Team + +The Jupyter Development Team is the set of all contributors to the Jupyter +project. This includes all of the Jupyter subprojects. A full list with +details is kept in the documentation directory, in the file +`about/credits.txt`. + +The core team that coordinates development on GitHub can be found here: +https://github.com/ipython/. + +## Our Copyright Policy + +Jupyter uses a shared copyright model. Each contributor maintains copyright +over their contributions to Jupyter. It is important to note that these +contributions are typically only changes to the repositories. Thus, the Jupyter +source code in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Jupyter +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Jupyter repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + + # Copyright (c) Jupyter Development Team. + # Distributed under the terms of the Modified BSD License. diff --git a/examples/jupyter_path_entrypoint_flit/README.md b/examples/jupyter_path_entrypoint_flit/README.md new file mode 100644 index 0000000..2ee7cf8 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/README.md @@ -0,0 +1,39 @@ +# entry_point_example_flit + +A minimal example of a package which provides additional `jupyter_*_path`s +via `entry_points`, packaged with `flit`. + +## Developing + +### Setup + +- ensure `flit` is installed + +### Editable install + +On any platform, enable live development by putting a `.pth` file in `site-packages`: + +```bash +flit install --pth-file +``` + +On UNIX, a symlink may be used instead: + +```bash +flint install --symlink +``` + +### Building + +```bash +flit build +``` + +Optionally, set the `SOURCE_DATE_EPOCH` environment variable to ensure a +[reproducible](https://reproducible-builds.org) `.whl`. + +> For example, to use the last `git` commit: +> +> ```bash +> SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) flit build +> ``` diff --git a/examples/jupyter_path_entrypoint_flit/pyproject.toml b/examples/jupyter_path_entrypoint_flit/pyproject.toml new file mode 100644 index 0000000..37cee43 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "entry_point_example_flit" +author = "Sir Robin" +author-email = "robin@camelot.uk" +home-page = "https://github.com/sirrobin/foobar" + +[tool.flit.entrypoints.jupyter_config_path] +entry-point-example-flit = "entry_point_example_flit:etc" + +[tool.flit.entrypoints.jupyter_data_path] +entry-point-example-flit = "entry_point_example_flit:share" + +[tool.flit.sdist] +include = ["src/entry_point_example_flit/etc/", "src/entry_point_example_flit/share/"] +exclude = ["src/entry_point_example_flit/share/example_excluded_file_flit.json"] diff --git a/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/__init__.py b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/__init__.py new file mode 100644 index 0000000..c10ede5 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/__init__.py @@ -0,0 +1,5 @@ +"""an example of using the jupyter_*_paths entry_points with flit + +The entry_points are defined in pyproject.toml under tool.flit.entrypoints +""" +__version__ = "0.1.0" diff --git a/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/etc/jupyter_config.d/entrypoint-example-flit.json b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/etc/jupyter_config.d/entrypoint-example-flit.json new file mode 100644 index 0000000..081664f --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/etc/jupyter_config.d/entrypoint-example-flit.json @@ -0,0 +1,5 @@ +{ + "SingletonConfigurable": { + "log_level": "INFO" + } +} diff --git a/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_excluded_file_flit.json b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_excluded_file_flit.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_excluded_file_flit.json @@ -0,0 +1 @@ +[] diff --git a/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_file_flit.json b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_file_flit.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/examples/jupyter_path_entrypoint_flit/src/entry_point_example_flit/share/example_file_flit.json @@ -0,0 +1 @@ +{} diff --git a/examples/jupyter_path_entrypoint_setuptools/COPYING.md b/examples/jupyter_path_entrypoint_setuptools/COPYING.md new file mode 100644 index 0000000..e5ae344 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/COPYING.md @@ -0,0 +1,61 @@ +# The Jupyter licensing terms + +Jupyter is licensed under the terms of the Modified BSD License (also known as +New or Revised or 3-Clause BSD), as follows: + +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Jupyter Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## About the Jupyter Development Team + +The Jupyter Development Team is the set of all contributors to the Jupyter +project. This includes all of the Jupyter subprojects. A full list with +details is kept in the documentation directory, in the file +`about/credits.txt`. + +The core team that coordinates development on GitHub can be found here: +https://github.com/ipython/. + +## Our Copyright Policy + +Jupyter uses a shared copyright model. Each contributor maintains copyright +over their contributions to Jupyter. It is important to note that these +contributions are typically only changes to the repositories. Thus, the Jupyter +source code in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Jupyter +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Jupyter repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + + # Copyright (c) Jupyter Development Team. + # Distributed under the terms of the Modified BSD License. diff --git a/examples/jupyter_path_entrypoint_setuptools/MANIFEST.in b/examples/jupyter_path_entrypoint_setuptools/MANIFEST.in new file mode 100644 index 0000000..4e07645 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include src/entry_point_example_setuptools *.* +exclude src/entry_point_example_setuptools/share/example_excluded_file_setuptools.json diff --git a/examples/jupyter_path_entrypoint_setuptools/README.md b/examples/jupyter_path_entrypoint_setuptools/README.md new file mode 100644 index 0000000..90cca80 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/README.md @@ -0,0 +1,22 @@ +# entry_point_example_setuptools + +A minimal example of a package which provides additional `jupyter_*_path`s +via `entry_points`, packaged with `setuptools`. + +## Developing + +### Setup + +- ensure `pip` is installed + +### Editable install + +```bash +pip install -e . +``` + +### Building + +```bash +python setup.py sdist bdist_wheel +``` diff --git a/examples/jupyter_path_entrypoint_setuptools/setup.cfg b/examples/jupyter_path_entrypoint_setuptools/setup.cfg new file mode 100644 index 0000000..5c4670b --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = entry_point_example_setuptools +version = attr: entry_point_example_setuptools.__version__ +description = a dummy module for testing jupyter_paths entrypoint +long_description = file: README.md +long_description_content_type = text/markdown +url = https://jupyter.org +author = Jupyter Development Team +author_email = jupyter@googlegroups.org +license = BSD +license_file = COPYING.md +classifiers = + Framework :: Jupyter + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved :: BSD License + Programming Language :: Python + +[options] +package_dir = + = src + +packages = find: +include_package_data = True +zip_safe = False +python_requires = >=3.6 + +install_requires = + jupyter_core + +[options.packages.find] +where = + src + +[options.entry_points] +jupyter_config_path = + entry-point-example-setuptools = entry_point_example_setuptools:etc +jupyter_data_path = + entry-point-example-setuptools = entry_point_example_setuptools:share diff --git a/examples/jupyter_path_entrypoint_setuptools/setup.py b/examples/jupyter_path_entrypoint_setuptools/setup.py new file mode 100644 index 0000000..a4f49f9 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/setup.py @@ -0,0 +1,2 @@ +import setuptools +setuptools.setup() diff --git a/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/__init__.py b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/__init__.py new file mode 100644 index 0000000..5e4f1d1 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/__init__.py @@ -0,0 +1,5 @@ +"""an example of using the jupyter_*_paths entry_points in setuptools + +The entrypoints are defined in setup.cfg under [options.entry_points] +""" +__version__ = "0.1.0" diff --git a/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/etc/jupyter_config.d/entrypoint-example.json b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/etc/jupyter_config.d/entrypoint-example.json new file mode 100644 index 0000000..cb32537 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/etc/jupyter_config.d/entrypoint-example.json @@ -0,0 +1,5 @@ +{ + "LoggingConfigurable": { + "log_level": "DEBUG" + } +} diff --git a/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_excluded_file_setuptools.json b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_excluded_file_setuptools.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_excluded_file_setuptools.json @@ -0,0 +1 @@ +[] diff --git a/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_file_setuptools.json b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_file_setuptools.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/examples/jupyter_path_entrypoint_setuptools/src/entry_point_example_setuptools/share/example_file_setuptools.json @@ -0,0 +1 @@ +{} diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index 4bee4ff..4a6e679 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -9,14 +9,22 @@ import os +import ast import sys import stat import errno +import time +import pathlib import tempfile import warnings +import functools +import traceback +import importlib from contextlib import contextmanager +import entrypoints + pjoin = os.path.join # UF_HIDDEN is a stat flag not defined in the stat module. @@ -24,6 +32,55 @@ UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) +# The entry_point MUST be the name of a directory, consisting of only +# letters, numbers, and underscores, adjacent to __init__.py +JUPYTER_DATA_PATH_ENTRY_POINT = "jupyter_data_path" +JUPYTER_CONFIG_PATH_ENTRY_POINT = "jupyter_config_path" + +# TODO: decide whether to keep, or grow a logging endpoint +JUPYTER_ENTRY_POINT_TIMINGS = os.environ.get("JUPYTER_ENTRY_POINT_TIMINGS") + + +def _get_path_from_one_entry_point(ep): + """ use the entrypoint attribute name to discover the path without loading + """ + spec = importlib.util.find_spec(ep.module_name) + module = importlib.util.module_from_spec(spec) + origin = pathlib.Path(module.__file__).parent.resolve() + return str(origin / ep.object_name) + + +def _entry_point_paths(ep_group): + start = time.time() + + group = entrypoints.get_group_named(ep_group).items() + + JUPYTER_ENTRY_POINT_TIMINGS and print( + f"{1e3 * (time.time() - start):.2f}ms {ep_group} loaded" + ) + paths = [] + + for name, ep in reversed(sorted(group)): + try: + ep_start = time.time() + paths.append(_get_path_from_one_entry_point(ep)) + except Exception: + warnings.warn('Failed to load {} from entry_point "{}"\n{}'.format( + ep_group, + name, + traceback.format_exc() + )) + finally: + JUPYTER_ENTRY_POINT_TIMINGS and print( + f"{1e3 * (time.time() - ep_start):.2f}ms\t{ep_group}\t{name}" + ) + + + end = time.time() + JUPYTER_ENTRY_POINT_TIMINGS and print(f"{1e3 * (end - start):.2f}ms {ep_group}\tTOTAL") + return paths + + def envset(name): """Return True if the given environment variable is set @@ -162,13 +219,16 @@ def jupyter_path(*subdirs): # Next is environment or user, depending on the JUPYTER_PREFER_ENV_PATH flag user = jupyter_data_dir() env = [p for p in ENV_JUPYTER_PATH if p not in SYSTEM_JUPYTER_PATH] + entry_points = [p for p in _entry_point_paths(JUPYTER_DATA_PATH_ENTRY_POINT) if p not in SYSTEM_JUPYTER_PATH] if envset('JUPYTER_PREFER_ENV_PATH'): paths.extend(env) + paths.extend(entry_points) paths.append(user) else: paths.append(user) paths.extend(env) + paths.extend(entry_points) # finally, system paths.extend(SYSTEM_JUPYTER_PATH) @@ -196,7 +256,7 @@ def jupyter_path(*subdirs): def jupyter_config_path(): """Return the search path for Jupyter config files as a list. - + If the JUPYTER_PREFER_ENV_PATH environment variable is set, the environment-level directories will have priority over user-level directories. """ @@ -216,13 +276,16 @@ def jupyter_config_path(): # Next is environment or user, depending on the JUPYTER_PREFER_ENV_PATH flag user = jupyter_config_dir() env = [p for p in ENV_CONFIG_PATH if p not in SYSTEM_CONFIG_PATH] + entry_points = [p for p in _entry_point_paths(JUPYTER_CONFIG_PATH_ENTRY_POINT) if p not in SYSTEM_CONFIG_PATH] if envset('JUPYTER_PREFER_ENV_PATH'): paths.extend(env) + paths.extend(entry_points) paths.append(user) else: paths.append(user) paths.extend(env) + paths.extend(entry_points) # Finally, system path paths.extend(SYSTEM_CONFIG_PATH) diff --git a/jupyter_core/tests/test_entrypoints.py b/jupyter_core/tests/test_entrypoints.py new file mode 100644 index 0000000..c37e8f3 --- /dev/null +++ b/jupyter_core/tests/test_entrypoints.py @@ -0,0 +1,72 @@ +"""TODO: these should be merged back into test_paths.py when they settle down +""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import importlib +from unittest.mock import patch, Mock + +import entrypoints + +import pytest + +from jupyter_core.paths import ( + jupyter_path, jupyter_config_path, + JUPYTER_CONFIG_PATH_ENTRY_POINT, JUPYTER_DATA_PATH_ENTRY_POINT, +) + + +def test_data_entry_point(data_path_entry_point, tmp_path): + data_path = jupyter_path() + path = str(tmp_path / "foo/share") + assert path in data_path + + +def test_config_entry_point(config_path_entry_point, tmp_path): + config_path = jupyter_config_path() + path = str(tmp_path / "bar/etc") + assert path in config_path + + +# there's a lot of duplication, as ugly path hacks get confused otheriwse +@pytest.fixture +def foo_entry_point_module(tmp_path): + with _mock_modspec("foo", tmp_path): + yield + + +@pytest.fixture +def data_path_entry_point(foo_entry_point_module): + with _mock_entry_point(JUPYTER_DATA_PATH_ENTRY_POINT, "foo-config", "foo", "share"): + yield + + +@pytest.fixture +def bar_entry_point_module(tmp_path): + with _mock_modspec("bar", tmp_path): + yield + + +@pytest.fixture +def config_path_entry_point(bar_entry_point_module): + loader = _mock_entry_point(JUPYTER_CONFIG_PATH_ENTRY_POINT, "bar-config", "bar", "etc") + with loader: + yield + + +def _mock_modspec(name, tmp_path): + mod = tmp_path / f"{name}/__init__.py" + mod.parent.mkdir() + mod.write_text('__version__ = "0.1.0"') + spec = Mock() + spec.origin = str(mod) + return patch.object(importlib.util, 'find_spec', return_value=spec) + + +def _mock_entry_point(ep_group, ep_name, module_name, object_name): + ep = Mock() + ep.name = ep_name + ep.module_name = module_name + ep.object_name = object_name + return patch.object(entrypoints, 'get_group_named', return_value={ep_name: ep}) diff --git a/jupyter_core/tests/test_paths.py b/jupyter_core/tests/test_paths.py index 6d37d22..908f8a5 100644 --- a/jupyter_core/tests/test_paths.py +++ b/jupyter_core/tests/test_paths.py @@ -183,8 +183,8 @@ def test_jupyter_path(): def test_jupyter_path_prefer_env(): with patch.dict('os.environ', {'JUPYTER_PREFER_ENV_PATH': 'true'}): path = jupyter_path() - assert path[0] == paths.ENV_JUPYTER_PATH[0] - assert path[1] == jupyter_data_dir() + + assert path.index(paths.ENV_JUPYTER_PATH[0]) < path.index(jupyter_data_dir()) def test_jupyter_path_env(): path_env = os.pathsep.join([ @@ -194,7 +194,8 @@ def test_jupyter_path_env(): with patch.dict('os.environ', {'JUPYTER_PATH': path_env}): path = jupyter_path() - assert path[:2] == [pjoin('foo', 'bar'), pjoin('bar', 'baz')] + + assert path.index(pjoin('foo', 'bar')) == path.index(pjoin('bar', 'baz')) - 1 def test_jupyter_path_sys_prefix(): @@ -211,13 +212,13 @@ def test_jupyter_path_subdir(): def test_jupyter_config_path(): path = jupyter_config_path() assert path[0] == jupyter_config_dir() - assert path[1] == paths.ENV_CONFIG_PATH[0] + assert paths.ENV_CONFIG_PATH[0] in path def test_jupyter_config_path_prefer_env(): with patch.dict('os.environ', {'JUPYTER_PREFER_ENV_PATH': 'true'}): path = jupyter_config_path() assert path[0] == paths.ENV_CONFIG_PATH[0] - assert path[1] == jupyter_config_dir() + assert jupyter_config_dir() in path def test_jupyter_config_path_env(): path_env = os.pathsep.join([ diff --git a/setup.cfg b/setup.cfg index f68a494..ea9765c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,12 +23,18 @@ packages = jupyter_core, jupyter_core.utils, jupyter_core.tests include_package_data = True python_requires = >=3.6 install_requires = + entrypoints traitlets pywin32>=1.0 ; sys_platform == 'win32' +[options.extras_require] +test = + ipykernel + pytest + pytest-cov + [options.entry_points] console_scripts = jupyter = jupyter_core.command:main jupyter-migrate = jupyter_core.migrate:main jupyter-troubleshoot = jupyter_core.troubleshoot:main -