diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index b8b9771..43eec48 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -48,6 +48,12 @@ jobs: run: | python -m pip install git+https://github.com/emscripten-forge/pyjs-code-runner --no-deps --ignore-installed + # Remove after https://github.com/emscripten-forge/pyjs-code-runner/pull/15 is merged. + - name: Install appdirs + shell: bash -l {0} + run: | + python -m pip install appdirs + - name: Run pytest shell: bash -l {0} run: | diff --git a/empack/pack.py b/empack/pack.py index 652aba8..a792c8c 100644 --- a/empack/pack.py +++ b/empack/pack.py @@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory from typing import Callable, Optional -from appdirs import user_cache_dir +from platformdirs import user_cache_dir from .filter_env import filter_env, filter_pkg, iterate_env_pkg_meta from .micromamba_wrapper import create_environment @@ -16,7 +16,88 @@ EMPACK_CACHE_DIR = Path(user_cache_dir("empack")) PACKED_PACKAGES_CACHE_DIR = EMPACK_CACHE_DIR / "packed_packages_cache" PACKED_PACKAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True) -DEFAULT_CONFIG_PATH = Path(sys.prefix) / "share" / "empack" / "empack_config.yaml" + + +def _do_i_own(path: str | Path) -> bool: + """Verify if the current user has write access to the given path. Sourced from + https://github.com/jupyter/jupyter_core/blob/fa513c1550bbd1ebcc14a4a79eb8c5d95e3e23c9/jupyter_core/paths.py#L75-L99 + """ + # Copyright (c) Jupyter Development Team. + # Distributed under the terms of the Modified BSD License. + + # Derived from IPython.utils.path, which is + # Copyright (c) IPython Development Team. + # Distributed under the terms of the Modified BSD License. + + p = Path(path).resolve() + + while not p.exists() and p != p.parent: + p = p.parent + + # simplest check: owner by name + # not always implemented or available + try: + return p.owner() == os.getlogin() + except Exception: # noqa: S110 + pass + + if hasattr(os, "geteuid"): + try: + st = p.stat() + return st.st_uid == os.geteuid() + except (NotImplementedError, OSError): + # geteuid not always implemented + pass + + # no ownership checks worked, check write access + return os.access(p, os.W_OK) + + +def get_config_path() -> Path: + """Find the empack configuration file by checking common locations. + + This function checks for the config file in the following order: + 1. Inside the environment's share directory (conda-style environments) + 2. Inside a share/ directory next to a virtual environment + + Returns: + Path: Location of the empack_config.yaml file + """ + # Copyright (c) Jupyter Development Team. + # Distributed under the terms of the Modified BSD License. + + # Derived from IPython.utils.path, which is + # Copyright (c) IPython Development Team. + # Distributed under the terms of the Modified BSD License. + + # 1. Check for config in environment's share directory. This is applicable + # for conda/mamba/micromamba style environments. + prefix = None + if ( + "CONDA_PREFIX" in os.environ + and sys.prefix.startswith(os.environ["CONDA_PREFIX"]) + and os.environ.get("CONDA_DEFAULT_ENV", "base") != "base" + and _do_i_own(sys.prefix) + ): + prefix = Path(os.environ["CONDA_PREFIX"]) + else: + prefix = Path(sys.prefix) + + config_path = prefix / "share" / "empack" / "empack_config.yaml" + if config_path.exists(): + return config_path + + # 2. For virtual environments via virtualenv/venv/uv, check if share/ is next + # to the environment base directory. This is also applicable for the case + # where there's no virtual environment at all (--user install). + if sys.prefix != sys.base_prefix and _do_i_own(sys.prefix): + venv_parent = Path(sys.prefix).parent + parent_share = venv_parent / "share" / "empack" / "empack_config.yaml" + if parent_share.exists(): + return parent_share + + +DEFAULT_CONFIG_PATH = get_config_path() def filename_base_from_meta(pkg_meta): diff --git a/pyproject.toml b/pyproject.toml index 076a755..49a2459 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "appdirs", + "platformdirs", "networkx", "pyyaml", "requests",