From 98f15fc73a890ab83afd3e9c9cd8e9a1e416ce7b Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Fri, 6 Dec 2024 23:25:42 +0000 Subject: [PATCH] make `site install` more configurable --- src/maturin_import_hook/__main__.py | 46 ++++- src/maturin_import_hook/_building.py | 12 +- src/maturin_import_hook/_site.py | 97 +++++++--- src/maturin_import_hook/project_importer.py | 4 +- src/maturin_import_hook/rust_file_importer.py | 4 +- src/maturin_import_hook/settings.py | 179 +++++++++++++----- tests/pytest.ini | 2 + tests/test_import_hook/test_site.py | 91 +++++++-- tests/test_import_hook/test_utilities.py | 74 +++++--- 9 files changed, 370 insertions(+), 139 deletions(-) create mode 100644 tests/pytest.ini diff --git a/src/maturin_import_hook/__main__.py b/src/maturin_import_hook/__main__.py index 999718f..6630d2c 100644 --- a/src/maturin_import_hook/__main__.py +++ b/src/maturin_import_hook/__main__.py @@ -6,6 +6,7 @@ import site import subprocess from pathlib import Path +from typing import Optional from maturin_import_hook._building import get_default_build_dir from maturin_import_hook._site import ( @@ -98,9 +99,17 @@ def _action_site_info(format_name: str) -> None: ) -def _action_site_install(*, user: bool, preset_name: str, force: bool) -> None: +def _action_site_install( + *, + user: bool, + force: bool, + args: Optional[str], + enable_project_importer: bool, + enable_rs_file_importer: bool, + detect_uv: bool, +) -> None: module_path = get_usercustomize_path() if user else get_sitecustomize_path() - insert_automatic_installation(module_path, preset_name, force) + insert_automatic_installation(module_path, force, args, enable_project_importer, enable_rs_file_importer, detect_uv) def _action_site_uninstall(*, user: bool) -> None: @@ -183,10 +192,26 @@ def _main() -> None: help="whether to overwrite any existing managed import hook installation", ) install.add_argument( - "--preset", - default="debug", - choices=["debug", "release"], - help="the settings preset for the import hook to use when building packages. Defaults to 'debug'.", + "--project-importer", + default=True, + help="Whether to enable the project importer", + action=argparse.BooleanOptionalAction, + ) + install.add_argument( + "--rs-file-importer", + default=True, + help="Whether to enable the rs file importer", + action=argparse.BooleanOptionalAction, + ) + install.add_argument( + "--detect-uv", + default=True, + help="Whether to automatically detect and use the --uv flag", + action=argparse.BooleanOptionalAction, + ) + install.add_argument( + "--args", + help="The arguments to pass to `maturin`. See `maturin develop --help` or `maturin build --help`", ) install.add_argument( "--user", @@ -226,7 +251,14 @@ def _main() -> None: if args.sub_action == "info": _action_site_info(args.format) elif args.sub_action == "install": - _action_site_install(user=args.user, preset_name=args.preset, force=args.force) + _action_site_install( + user=args.user, + force=args.force, + args=args.args, + enable_project_importer=args.project_importer, + enable_rs_file_importer=args.rs_file_importer, + detect_uv=args.detect_uv, + ) elif args.sub_action == "uninstall": _action_site_uninstall(user=args.user) else: diff --git a/src/maturin_import_hook/_building.py b/src/maturin_import_hook/_building.py index cd03508..b5c1b8f 100644 --- a/src/maturin_import_hook/_building.py +++ b/src/maturin_import_hook/_building.py @@ -149,9 +149,6 @@ def build_wheel( output_dir: Path, settings: MaturinSettings, ) -> str: - if "build" not in settings.supported_commands(): - msg = f'provided {type(settings).__name__} does not support the "build" command' - raise ImportHookError(msg) success, output = run_maturin( maturin_path, [ @@ -162,7 +159,7 @@ def build_wheel( sys.executable, "--out", str(output_dir), - *settings.to_args(), + *settings.to_args("build"), ], ) if not success: @@ -176,10 +173,9 @@ def develop_build_project( manifest_path: Path, settings: MaturinSettings, ) -> str: - if "develop" not in settings.supported_commands(): - msg = f'provided {type(settings).__name__} does not support the "develop" command' - raise ImportHookError(msg) - success, output = run_maturin(maturin_path, ["develop", "--manifest-path", str(manifest_path), *settings.to_args()]) + success, output = run_maturin( + maturin_path, ["develop", "--manifest-path", str(manifest_path), *settings.to_args("develop")] + ) if not success: msg = "Failed to build package with maturin" raise MaturinError(msg) diff --git a/src/maturin_import_hook/_site.py b/src/maturin_import_hook/_site.py index 655b3b4..708a366 100644 --- a/src/maturin_import_hook/_site.py +++ b/src/maturin_import_hook/_site.py @@ -1,35 +1,36 @@ +import dataclasses +import importlib +import shlex +import shutil import site from pathlib import Path -from textwrap import dedent +from typing import Optional from maturin_import_hook._logging import logger +from maturin_import_hook.settings import MaturinSettings MANAGED_INSTALL_START = "# " MANAGED_INSTALL_END = "# \n" MANAGED_INSTALL_COMMENT = """ -# the following commands install the maturin import hook during startup. +# the following installs the maturin import hook during startup. # see: `python -m maturin_import_hook site` """ -MANAGED_INSTALLATION_PRESETS = { - "debug": dedent("""\ - try: - import maturin_import_hook - except ImportError: - pass - else: - maturin_import_hook.install() - """), - "release": dedent("""\ - try: - import maturin_import_hook - from maturin_import_hook.settings import MaturinSettings - except ImportError: - pass - else: - maturin_import_hook.install(MaturinSettings(release=True)) - """), -} +INSTALL_TEMPLATE = """\ +try: + import maturin_import_hook + from maturin_import_hook.settings import MaturinSettings +except ImportError: + pass +else: + maturin_import_hook.install( + settings=MaturinSettings( + {settings} + ), + enable_project_importer={enable_project_importer}, + enable_rs_file_importer={enable_rs_file_importer}, + ) +""" def get_sitecustomize_path() -> Path: @@ -83,10 +84,44 @@ def remove_automatic_installation(module_path: Path) -> None: module_path.unlink(missing_ok=True) -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) +def _should_use_uv() -> bool: + """Whether the `--uv` flag should be used when installing into this environment. + + virtual environments managed with `uv` do not have `pip` installed so the `--uv` flag is required. + """ + try: + importlib.import_module("pip") + except ModuleNotFoundError: + if shutil.which("uv") is not None: + return True + else: + logger.warning("neither `pip` nor `uv` were found. `maturin develop` may not work...") + return False + else: + # since pip is a more established program, use it even if uv may be installed + return False + + +def insert_automatic_installation( + module_path: Path, + force: bool, + args: Optional[str], + enable_project_importer: bool, + enable_rs_file_importer: bool, + detect_uv: bool, +) -> None: + if args is None: + parsed_args = MaturinSettings.default() + else: + parsed_args = MaturinSettings.from_args(shlex.split(args)) + if parsed_args.color is None: + parsed_args.color = True + if detect_uv and not parsed_args.uv and _should_use_uv(): + parsed_args.uv = True + logger.info( + "using `--uv` flag as it was detected to be necessary for this environment. " + "Use `site install --no-detect-uv` to set manually." + ) logger.info(f"installing automatic activation into '{module_path}'") if has_automatic_installation(module_path): @@ -97,14 +132,22 @@ def insert_automatic_installation(module_path: Path, preset_name: str, force: bo logger.info("already installed. Aborting install.") return - parts = [] + parts: list[str] = [] if module_path.exists(): parts.append(module_path.read_text()) parts.append("\n") + + defaults = MaturinSettings() + non_default_settings = {k: v for k, v in dataclasses.asdict(parsed_args).items() if getattr(defaults, k) != v} + parts.extend([ MANAGED_INSTALL_START, MANAGED_INSTALL_COMMENT, - MANAGED_INSTALLATION_PRESETS[preset_name], + INSTALL_TEMPLATE.format( + settings=",\n ".join(f"{k}={v!r}" for k, v in non_default_settings.items()), + enable_project_importer=repr(enable_project_importer), + enable_rs_file_importer=repr(enable_rs_file_importer), + ), MANAGED_INSTALL_END, ]) code = "".join(parts) diff --git a/src/maturin_import_hook/project_importer.py b/src/maturin_import_hook/project_importer.py index a133123..9e80682 100644 --- a/src/maturin_import_hook/project_importer.py +++ b/src/maturin_import_hook/project_importer.py @@ -281,7 +281,7 @@ def _rebuild_project( if mtime is None: logger.error("could not get installed package mtime") else: - build_status = BuildStatus(mtime, project_dir, settings.to_args(), maturin_output) + build_status = BuildStatus(mtime, project_dir, settings.to_args("develop"), maturin_output) build_cache.store_build_status(build_status) return spec, True @@ -315,7 +315,7 @@ def _get_spec_for_up_to_date_package( return None, "no build status found" if build_status.source_path != project_dir: return None, "source path in build status does not match the project dir" - if build_status.maturin_args != settings.to_args(): + if build_status.maturin_args != settings.to_args("develop"): return None, "current maturin args do not match the previous build" installed_paths = self._file_searcher.get_installation_paths(installed_package_root) diff --git a/src/maturin_import_hook/rust_file_importer.py b/src/maturin_import_hook/rust_file_importer.py index fc45019..acaca32 100644 --- a/src/maturin_import_hook/rust_file_importer.py +++ b/src/maturin_import_hook/rust_file_importer.py @@ -236,7 +236,7 @@ def _import_rust_file( build_status = BuildStatus( extension_module_path.stat().st_mtime, file_path, - settings.to_args(), + settings.to_args("build"), maturin_output, ) build_cache.store_build_status(build_status) @@ -270,7 +270,7 @@ def _get_spec_for_up_to_date_extension_module( return None, "no build status found" if build_status.source_path != source_path: return None, "source path in build status does not match the project dir" - if build_status.maturin_args != settings.to_args(): + if build_status.maturin_args != settings.to_args("build"): return None, "current maturin args do not match the previous build" freshness = get_installation_freshness( diff --git a/src/maturin_import_hook/settings.py b/src/maturin_import_hook/settings.py index 7a55407..651c924 100644 --- a/src/maturin_import_hook/settings.py +++ b/src/maturin_import_hook/settings.py @@ -1,16 +1,17 @@ +import argparse +import re +from collections.abc import Sequence from dataclasses import dataclass -from typing import Optional +from typing import IO, Any, Literal, Optional, Union __all__ = [ "MaturinSettings", - "MaturinBuildSettings", - "MaturinDevelopSettings", ] @dataclass class MaturinSettings: - """Settings common to `maturin build` and `maturin develop`.""" + """Settings common to `maturin build` and `maturin develop` relevant to the import hook..""" release: bool = False strip: bool = False @@ -31,9 +32,14 @@ class MaturinSettings: verbose: int = 0 rustc_flags: Optional[list[str]] = None - @staticmethod - def supported_commands() -> set[str]: - return {"build", "develop"} + # `maturin build` specific + auditwheel: Optional[str] = None + zig: bool = False + + # `maturin develop` specific + extras: Optional[list[str]] = None + uv: bool = False + skip_install: bool = False @staticmethod def default() -> "MaturinSettings": @@ -42,8 +48,8 @@ def default() -> "MaturinSettings": color=True, ) - def to_args(self) -> list[str]: - args = [] + def to_args(self, cmd: Literal["develop", "build"]) -> list[str]: + args: list[str] = [] if self.release: args.append("--release") if self.strip: @@ -90,53 +96,122 @@ def to_args(self) -> list[str]: args.append(flag) if self.verbose > 0: args.append("-{}".format("v" * self.verbose)) - if self.rustc_flags is not None: - args.extend(self.rustc_flags) - return args + if cmd == "build": + if self.auditwheel is not None: + args.append("--auditwheel") + args.append(self.auditwheel) + if self.zig: + args.append("--zig") + + if cmd == "develop": + if self.extras is not None: + args.append("--extras") + args.append(",".join(self.extras)) + if self.uv: + args.append("--uv") + if self.skip_install: + args.append("--skip-install") -@dataclass -class MaturinBuildSettings(MaturinSettings): - """settings for `maturin build`.""" - - auditwheel: Optional[str] = None - zig: bool = False + if self.rustc_flags is not None: + args.append("--") + args.extend(self.rustc_flags) - @staticmethod - def supported_commands() -> set[str]: - return {"build"} - - def to_args(self) -> list[str]: - args = [] - if self.auditwheel is not None: - args.append("--auditwheel") - args.append(self.auditwheel) - if self.zig: - args.append("--zig") - args.extend(super().to_args()) return args - -@dataclass -class MaturinDevelopSettings(MaturinSettings): - """settings for `maturin develop`.""" - - extras: Optional[list[str]] = None - uv: bool = False - skip_install: bool = False + @staticmethod + def from_args(raw_args: list[str]) -> "MaturinSettings": + """Parse command line flags into this data structure""" + parser = MaturinSettings.parser() + args = parser.parse_args(raw_args) + if "--" in args.rustc_flags: + args.rustc_flags.remove("--") + if len(args.rustc_flags) == 0: + args.rustc_flags = None + return MaturinSettings(**vars(args)) @staticmethod - def supported_commands() -> set[str]: - return {"develop"} - - def to_args(self) -> list[str]: - args = [] - if self.extras is not None: - args.append("--extras") - args.append(",".join(self.extras)) - if self.uv: - args.append("--uv") - if self.skip_install: - args.append("--skip-install") - args.extend(super().to_args()) - return args + def parser() -> "NonExitingArgumentParser": + """Obtain an argument parser that can parse arguments related to this class""" + parser = NonExitingArgumentParser() + parser.add_argument("-r", "--release", action="store_true") + parser.add_argument("--strip", action="store_true") + parser.add_argument("-q", "--quiet", action="store_true") + parser.add_argument("-j", "--jobs", type=int) + parser.add_argument("--profile") + parser.add_argument("-F", "--features", type=lambda arg: re.split(",|[ ]", arg), action="extend") + parser.add_argument("--all-features", action="store_true") + parser.add_argument("--no-default-features", action="store_true") + parser.add_argument("--target") + parser.add_argument("--ignore-rust-version", action="store_true") + + def parse_color(arg: str) -> Optional[bool]: + if arg == "always": + return True + elif arg == "never": + return False + else: + return None + + parser.add_argument("--color", type=parse_color) + parser.add_argument("--frozen", action="store_true") + parser.add_argument("--locked", action="store_true") + parser.add_argument("--offline", action="store_true") + parser.add_argument("--config", action=_KeyValueAction) + parser.add_argument("-Z", dest="unstable_flags", action="append") + parser.add_argument("-v", "--verbose", action="count", default=0) + parser.add_argument("rustc_flags", nargs=argparse.REMAINDER) + + # `maturin build` specific + parser.add_argument("--auditwheel", choices=["repair", "check", "skip"]) + parser.add_argument("--zig", action="store_true") + + # `maturin develop` specific + parser.add_argument("-E", "--extras", type=lambda arg: arg.split(","), action="extend") + parser.add_argument("--uv", action="store_true") + parser.add_argument("--skip-install", action="store_true") + + return parser + + +class NonExitingArgumentParser(argparse.ArgumentParser): + """An `ArgumentParser` that does not call `sys.exit` if it fails to parse""" + + def error(self, message: str) -> None: # type: ignore[override] + msg = "argument parser error" + raise ValueError(msg) + + def exit(self, status: int = 0, message: Optional[str] = None) -> None: # type: ignore[override] + pass + + def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: + pass + + +class _KeyValueAction(argparse.Action): + """Parse 'key=value' arguments into a dictionary""" + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Union[str, Sequence[Any], None] = None, + ) -> None: + if values is None: + values = [] + elif isinstance(values, str): + values = [values] + + key_value_store = getattr(namespace, self.dest) + if key_value_store is None: + key_value_store = {} + setattr(namespace, self.dest, key_value_store) + + for value in values: + parts = value.split("=", maxsplit=2) + if len(parts) == 2: + key_value_store[parts[0]] = parts[1] + else: + msg = f"failed to parse KEY=VALUE from {value!r}" + raise ValueError(msg) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..8114205 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -vv diff --git a/tests/test_import_hook/test_site.py b/tests/test_import_hook/test_site.py index 7e29a12..c7bc5bc 100644 --- a/tests/test_import_hook/test_site.py +++ b/tests/test_import_hook/test_site.py @@ -24,10 +24,24 @@ def test_automatic_site_installation(tmp_path: Path) -> None: assert not has_automatic_installation(sitecustomize) - insert_automatic_installation(sitecustomize, preset_name="debug", force=False) + insert_automatic_installation( + sitecustomize, + force=False, + args=None, + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) with capture_logs() as cap: - insert_automatic_installation(sitecustomize, preset_name="debug", force=False) + insert_automatic_installation( + sitecustomize, + force=False, + args=None, + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) logs = cap.getvalue() assert "already installed. Aborting install" in logs @@ -37,14 +51,21 @@ def test_automatic_site_installation(tmp_path: Path) -> None: install() # another import hook # - # the following commands install the maturin import hook during startup. + # the following installs the maturin import hook during startup. # see: `python -m maturin_import_hook site` try: import maturin_import_hook + from maturin_import_hook.settings import MaturinSettings except ImportError: pass else: - maturin_import_hook.install() + maturin_import_hook.install( + settings=MaturinSettings( + color=True + ), + enable_project_importer=True, + enable_rs_file_importer=True, + ) # """) @@ -84,12 +105,26 @@ def test_automatic_site_installation_force_overwrite(tmp_path: Path) -> None: sitecustomize.write_text(header) - insert_automatic_installation(sitecustomize, preset_name="debug", force=False) + insert_automatic_installation( + sitecustomize, + force=False, + args="", + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) sitecustomize.write_text(sitecustomize.read_text() + "\n\n# more code") with capture_logs() as cap: - insert_automatic_installation(sitecustomize, preset_name="release", force=True) + insert_automatic_installation( + sitecustomize, + force=True, + args="--release", + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) logs = cap.getvalue() assert "already installed, but force=True. Overwriting..." in logs @@ -102,7 +137,7 @@ def test_automatic_site_installation_force_overwrite(tmp_path: Path) -> None: # more code # - # the following commands install the maturin import hook during startup. + # the following installs the maturin import hook during startup. # see: `python -m maturin_import_hook site` try: import maturin_import_hook @@ -110,7 +145,14 @@ def test_automatic_site_installation_force_overwrite(tmp_path: Path) -> None: except ImportError: pass else: - maturin_import_hook.install(MaturinSettings(release=True)) + maturin_import_hook.install( + settings=MaturinSettings( + release=True, + color=True + ), + enable_project_importer=True, + enable_rs_file_importer=True, + ) # """) @@ -118,27 +160,48 @@ def test_automatic_site_installation_force_overwrite(tmp_path: Path) -> None: assert has_automatic_installation(sitecustomize) -def test_automatic_site_installation_invalid_preset(tmp_path: Path) -> None: +def test_automatic_site_installation_invalid_args(tmp_path: Path) -> None: sitecustomize = tmp_path / "sitecustomize.py" - with pytest.raises(ValueError, match="Unknown managed installation preset name: 'foo'"): - insert_automatic_installation(sitecustomize, preset_name="foo", force=False) + with pytest.raises(ValueError, match="argument parser error"): + insert_automatic_installation( + sitecustomize, + force=False, + args="--foo", + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) assert not sitecustomize.exists() def test_automatic_site_installation_empty(tmp_path: Path) -> None: sitecustomize = tmp_path / "sitecustomize.py" - insert_automatic_installation(sitecustomize, preset_name="debug", force=False) + insert_automatic_installation( + sitecustomize, + force=False, + args=None, + enable_project_importer=True, + enable_rs_file_importer=True, + detect_uv=False, + ) expected_code = dedent("""\ # - # the following commands install the maturin import hook during startup. + # the following installs the maturin import hook during startup. # see: `python -m maturin_import_hook site` try: import maturin_import_hook + from maturin_import_hook.settings import MaturinSettings except ImportError: pass else: - maturin_import_hook.install() + maturin_import_hook.install( + settings=MaturinSettings( + color=True + ), + enable_project_importer=True, + enable_rs_file_importer=True, + ) # """) diff --git a/tests/test_import_hook/test_utilities.py b/tests/test_import_hook/test_utilities.py index ec14088..bf2e73f 100644 --- a/tests/test_import_hook/test_utilities.py +++ b/tests/test_import_hook/test_utilities.py @@ -6,7 +6,6 @@ import subprocess import time from pathlib import Path -from typing import cast import pytest @@ -14,7 +13,7 @@ from maturin_import_hook._resolve_project import _ProjectResolveError, _resolve_project, _TomlFile from maturin_import_hook.error import ImportHookError from maturin_import_hook.project_importer import _load_dist_info, _uri_to_path -from maturin_import_hook.settings import MaturinBuildSettings, MaturinDevelopSettings, MaturinSettings +from maturin_import_hook.settings import MaturinSettings from .common import ( TEST_CRATES_DIR, @@ -42,12 +41,8 @@ def test_maturin_unchanged() -> None: def test_settings() -> None: - assert MaturinSettings().to_args() == [] - assert MaturinSettings().supported_commands() == {"build", "develop"} - assert MaturinBuildSettings().to_args() == [] - assert MaturinBuildSettings().supported_commands() == {"build"} - assert MaturinDevelopSettings().to_args() == [] - assert MaturinDevelopSettings().supported_commands() == {"develop"} + assert MaturinSettings().to_args("develop") == [] + assert MaturinSettings().to_args("build") == [] settings = MaturinSettings( release=True, @@ -67,10 +62,10 @@ def test_settings() -> None: config={"key1": "value1", "key2": "value2"}, unstable_flags=["unstable1", "unstable2"], verbose=2, - rustc_flags=["flag1", "flag2"], + rustc_flags=["flag1", "--flag2"], ) # fmt: off - assert settings.to_args() == [ + expected_args = [ "--release", "--strip", "--quiet", @@ -90,47 +85,72 @@ def test_settings() -> None: "-Z", "unstable1", "-Z", "unstable2", "-vv", + "--", "flag1", - "flag2", + "--flag2", ] # fmt: on + assert settings.to_args("develop") == expected_args + assert settings.to_args("build") == expected_args - build_settings = MaturinBuildSettings(auditwheel="skip", zig=True, color=False, rustc_flags=["flag1", "flag2"]) - assert build_settings.to_args() == [ + assert MaturinSettings.from_args(expected_args) == settings + + build_settings = MaturinSettings(auditwheel="skip", zig=True) + expected_args = [ "--auditwheel", "skip", "--zig", - "--color", - "never", - "flag1", - "flag2", ] + assert build_settings.to_args("build") == expected_args + assert build_settings.to_args("develop") == [] + assert MaturinSettings.from_args(expected_args) == build_settings - develop_settings = MaturinDevelopSettings( + develop_settings = MaturinSettings( extras=["extra1", "extra2"], skip_install=True, - color=False, - rustc_flags=["flag1", "flag2"], ) - assert develop_settings.to_args() == [ + expected_args = [ "--extras", "extra1,extra2", "--skip-install", + ] + assert develop_settings.to_args("develop") == expected_args + assert develop_settings.to_args("build") == [] + assert MaturinSettings.from_args(expected_args) == develop_settings + + mixed_settings = MaturinSettings( + color=True, + extras=["extra1", "extra2"], + skip_install=True, + zig=True, + rustc_flags=["flag1", "--flag2"], + ) + assert mixed_settings.to_args("develop") == [ "--color", - "never", + "always", + "--extras", + "extra1,extra2", + "--skip-install", + "--", + "flag1", + "--flag2", + ] + assert mixed_settings.to_args("build") == [ + "--color", + "always", + "--zig", + "--", "flag1", - "flag2", + "--flag2", ] class TestGetInstallationFreshness: def _build_status(self, mtime: float) -> BuildStatus: - return BuildStatus(build_mtime=mtime, source_path=cast(Path, None), maturin_args=[], maturin_output="") + return BuildStatus(build_mtime=mtime, source_path=Path("/"), maturin_args=[], maturin_output="") def _build_status_for_file(self, path: Path) -> BuildStatus: - return BuildStatus( - build_mtime=path.stat().st_mtime, source_path=cast(Path, None), maturin_args=[], maturin_output="" - ) + return BuildStatus(build_mtime=path.stat().st_mtime, source_path=Path("/"), maturin_args=[], maturin_output="") def test_missing_installation(self, tmp_path: Path) -> None: (tmp_path / "source").touch()