diff --git a/src/maturin_import_hook/__main__.py b/src/maturin_import_hook/__main__.py index 90fdbb4..6f9559d 100644 --- a/src/maturin_import_hook/__main__.py +++ b/src/maturin_import_hook/__main__.py @@ -3,12 +3,14 @@ import json import platform import shutil +import site import subprocess from pathlib import Path from maturin_import_hook._building import get_default_build_dir from maturin_import_hook._site import ( get_sitecustomize_path, + get_usercustomize_path, has_automatic_installation, insert_automatic_installation, remove_automatic_installation, @@ -80,25 +82,30 @@ def _action_cache_clear(interactive: bool) -> None: def _action_site_info(format_name: str) -> None: sitecustomize_path = get_sitecustomize_path() + usercustomize_path = get_usercustomize_path() _print_info( { - "sitecustomize_exists": sitecustomize_path.exists(), "sitecustomize_path": str(sitecustomize_path), - "import_hook_installed": has_automatic_installation(sitecustomize_path), + "sitecustomize_exists": sitecustomize_path.exists(), + "sitecustomize_import_hook_installed": has_automatic_installation(sitecustomize_path), + "user_site_enabled": str(site.ENABLE_USER_SITE), + "usercustomize_path": str(usercustomize_path), + "usercustomize_exists": usercustomize_path.exists(), + "usercustomize_import_hook_installed": has_automatic_installation(usercustomize_path), }, format_name, ) -def _action_site_install(preset_name: str, force: bool) -> None: - sitecustomize_path = get_sitecustomize_path() - insert_automatic_installation(sitecustomize_path, preset_name, force) +def _action_site_install(*, user: bool, preset_name: str, force: bool) -> None: + module_path = get_usercustomize_path() if user else get_sitecustomize_path() + insert_automatic_installation(module_path, preset_name, force) -def _action_site_uninstall() -> None: - sitecustomize_path = get_sitecustomize_path() - remove_automatic_installation(sitecustomize_path) +def _action_site_uninstall(*, user: bool) -> None: + module_path = get_usercustomize_path() if user else get_sitecustomize_path() + remove_automatic_installation(module_path) def _ask_yes_no(question: str) -> bool: @@ -149,23 +156,31 @@ def _main() -> None: site_action = subparsers.add_parser( "site", - help="manage installation of the import hook into site-packages/sitecustomize.py (so it starts automatically)", + help=( + "manage installation of the import hook into site-packages/sitecustomize.py " + "or usercustomize.py (so it starts automatically)" + ), ) site_sub_actions = site_action.add_subparsers(dest="sub_action") site_info = site_sub_actions.add_parser( - "info", help="information about the current status of installation into sitecustomize" + "info", help="information about the current status of installation into sitecustomize/usercustomize" ) site_info.add_argument( "-f", "--format", choices=["text", "json"], default="text", help="the format to output the data in" ) + install = site_sub_actions.add_parser( - "install", help="install the import hook into site-packages/sitecustomize.py so that it starts automatically" + "install", + help=( + "install the import hook into site-packages/sitecustomize.py " + "or usercustomize.py so that it starts automatically" + ), ) install.add_argument( "-f", "--force", action="store_true", - help="whether to overwrite any existing managed import hook installation in sitecustomize.py", + help="whether to overwrite any existing managed import hook installation", ) install.add_argument( "--preset", @@ -173,7 +188,24 @@ def _main() -> None: choices=["debug", "release"], help="the settings preset for the import hook to use when building packages. Defaults to 'debug'.", ) - site_sub_actions.add_parser("uninstall", help="uninstall the import hook from site-packages/sitecustomize.py") + install.add_argument( + "--user", + action="store_true", + help="whether to install into usercustomize.py instead of sitecustomize.py. " + "Note that usercustomize.py is shared between virtualenvs of the same interpreter version and is not loaded " + "unless the virtualenv is created with the `--system-site-packages` argument. Use `site info` to check " + "whether usercustomize.py is loaded the current interpreter.", + ) + + uninstall = site_sub_actions.add_parser( + "uninstall", + help="uninstall the import hook from site-packages/sitecustomize.py or site-packages/usercustomize.py", + ) + uninstall.add_argument( + "--user", + action="store_true", + help="whether to uninstall from usercustomize.py instead of sitecustomize.py", + ) args = parser.parse_args() @@ -192,9 +224,9 @@ def _main() -> None: if args.sub_action == "info": _action_site_info(args.format) elif args.sub_action == "install": - _action_site_install(args.preset, args.force) + _action_site_install(user=args.user, preset_name=args.preset, force=args.force) elif args.sub_action == "uninstall": - _action_site_uninstall() + _action_site_uninstall(user=args.user) else: site_action.print_help() else: diff --git a/src/maturin_import_hook/_site.py b/src/maturin_import_hook/_site.py index c97f5bc..dadde46 100644 --- a/src/maturin_import_hook/_site.py +++ b/src/maturin_import_hook/_site.py @@ -13,13 +13,21 @@ MANAGED_INSTALLATION_PRESETS = { "debug": dedent("""\ - import maturin_import_hook - maturin_import_hook.install() + try: + import maturin_import_hook + except ImportError: + pass + else: + maturin_import_hook.install() """), "release": dedent("""\ - import maturin_import_hook - from maturin_import_hook.settings import MaturinSettings - maturin_import_hook.install(MaturinSettings(release=True)) + try: + import maturin_import_hook + from maturin_import_hook.settings import MaturinSettings + except ImportError: + pass + else: + maturin_import_hook.install(MaturinSettings(release=True)) """), } @@ -36,54 +44,62 @@ def get_sitecustomize_path() -> Path: return Path(site_packages[0]) / "sitecustomize.py" -def has_automatic_installation(sitecustomize: Path) -> bool: - if not sitecustomize.is_file(): +def get_usercustomize_path() -> Path: + user_site_packages = site.getusersitepackages() + if user_site_packages is None: + msg = "could not find usercustomize.py (user site-packages not found)" + raise FileNotFoundError(msg) + return Path(user_site_packages) / "usercustomize.py" + + +def has_automatic_installation(module_path: Path) -> bool: + if not module_path.is_file(): return False - code = sitecustomize.read_text() + code = module_path.read_text() return MANAGED_INSTALL_START in code -def remove_automatic_installation(sitecustomize: Path) -> None: - logger.info(f"removing automatic activation from '{sitecustomize}'") - if not has_automatic_installation(sitecustomize): +def remove_automatic_installation(module_path: Path) -> None: + logger.info(f"removing automatic activation from '{module_path}'") + if not has_automatic_installation(module_path): logger.info("no installation found") return - code = sitecustomize.read_text() + code = module_path.read_text() managed_start = code.find(MANAGED_INSTALL_START) if managed_start == -1: - msg = f"failed to find managed install start marker in '{sitecustomize}'" + msg = f"failed to find managed install start marker in '{module_path}'" raise RuntimeError(msg) managed_end = code.find(MANAGED_INSTALL_END) if managed_end == -1: - msg = f"failed to find managed install start marker in '{sitecustomize}'" + msg = f"failed to find managed install start marker in '{module_path}'" raise RuntimeError(msg) code = code[:managed_start] + code[managed_end + len(MANAGED_INSTALL_END) :] if code.strip(): - sitecustomize.write_text(code) + module_path.write_text(code) else: logger.info("module is now empty. Removing file.") - sitecustomize.unlink(missing_ok=True) + module_path.unlink(missing_ok=True) -def insert_automatic_installation(sitecustomize: Path, preset_name: str, force: bool) -> None: +def insert_automatic_installation(module_path: Path, preset_name: str, force: bool) -> None: if preset_name not in MANAGED_INSTALLATION_PRESETS: msg = f"Unknown managed installation preset name: '{preset_name}'" raise ValueError(msg) - logger.info(f"installing automatic activation into '{sitecustomize}'") - if has_automatic_installation(sitecustomize): + logger.info(f"installing automatic activation into '{module_path}'") + if has_automatic_installation(module_path): if force: logger.info("already installed, but force=True. Overwriting...") - remove_automatic_installation(sitecustomize) + remove_automatic_installation(module_path) else: logger.info("already installed. Aborting install") return parts = [] - if sitecustomize.exists(): - parts.append(sitecustomize.read_text()) + if module_path.exists(): + parts.append(module_path.read_text()) parts.append("\n") parts.extend([ MANAGED_INSTALL_START, @@ -92,4 +108,5 @@ def insert_automatic_installation(sitecustomize: Path, preset_name: str, force: MANAGED_INSTALL_END, ]) code = "".join(parts) - sitecustomize.write_text(code) + module_path.parent.mkdir(parents=True, exist_ok=True) + module_path.write_text(code) diff --git a/src/maturin_import_hook/project_importer.py b/src/maturin_import_hook/project_importer.py index acdcdb1..7f3367c 100644 --- a/src/maturin_import_hook/project_importer.py +++ b/src/maturin_import_hook/project_importer.py @@ -641,3 +641,7 @@ def uninstall() -> None: with contextlib.suppress(ValueError): sys.meta_path.remove(IMPORTER) IMPORTER = None + + +def is_installed() -> bool: + return IMPORTER is not None and IMPORTER in sys.meta_path diff --git a/src/maturin_import_hook/rust_file_importer.py b/src/maturin_import_hook/rust_file_importer.py index 92e4bb0..c50d0fd 100644 --- a/src/maturin_import_hook/rust_file_importer.py +++ b/src/maturin_import_hook/rust_file_importer.py @@ -420,3 +420,7 @@ def uninstall() -> None: with contextlib.suppress(ValueError): sys.meta_path.remove(IMPORTER) IMPORTER = None + + +def is_installed() -> bool: + return IMPORTER is not None and IMPORTER in sys.meta_path diff --git a/tests/test_import_hook/test_site.py b/tests/test_import_hook/test_site.py index da2e5f6..bc8a915 100644 --- a/tests/test_import_hook/test_site.py +++ b/tests/test_import_hook/test_site.py @@ -38,8 +38,12 @@ def test_automatic_site_installation(tmp_path: Path) -> None: # # the following commands install the maturin import hook during startup. # see: `python -m maturin_import_hook site` - import maturin_import_hook - maturin_import_hook.install() + try: + import maturin_import_hook + except ImportError: + pass + else: + maturin_import_hook.install() # """) @@ -99,9 +103,13 @@ def test_automatic_site_installation_force_overwrite(tmp_path: Path) -> None: # # the following commands install the maturin import hook during startup. # see: `python -m maturin_import_hook site` - import maturin_import_hook - from maturin_import_hook.settings import MaturinSettings - maturin_import_hook.install(MaturinSettings(release=True)) + try: + import maturin_import_hook + from maturin_import_hook.settings import MaturinSettings + except ImportError: + pass + else: + maturin_import_hook.install(MaturinSettings(release=True)) # """) @@ -124,8 +132,12 @@ def test_automatic_site_installation_empty(tmp_path: Path) -> None: # # the following commands install the maturin import hook during startup. # see: `python -m maturin_import_hook site` - import maturin_import_hook - maturin_import_hook.install() + try: + import maturin_import_hook + except ImportError: + pass + else: + maturin_import_hook.install() # """)