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()
#
""")