diff --git a/.gitignore b/.gitignore index c609547..7916a60 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ AGENTS.md todo.md notes/ +.cursor-persona/ docs/superpowers/ .worktrees/ docker/dogfood-staging/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d225d..b79ba49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ ## Unreleased +## v0.1.9 + +### Fixed + +- `shipgate.__version__` follows the installed package metadata (`pyproject.toml`), + so it cannot drift from the published version. +- `import-linter.check` uses layout detection for an importable package (src or + flat) instead of requiring `src/*/__init__.py`. + ## v0.1.8 ### Fixed diff --git a/docs/check-flow.md b/docs/check-flow.md index 900dfde..a7f7477 100644 --- a/docs/check-flow.md +++ b/docs/check-flow.md @@ -115,7 +115,7 @@ uv run shipgate list checks | `tags` | Metadata labels (e.g. `security`); filter with `shipgate list tools --tag` | | `cache` | Optional result-cache policy (`results`, `ttl_seconds`). Keys include scoped file contents, tool version, config bytes, and check bindings (threshold / metric extras). `--no-cache` disables the cache; `--display-cli` prints `(cached)` on a hit. | | `suggest_if` | Additive init hints when matching files exist (does not change default suites) | -| `require_if` | Skip the check (exit 0, skipped status) unless matching files exist; e.g. `files_present: ["pyproject.toml"]` or `src/*/__init__.py` for import-linter. Require-if skips print to stderr (the glob is in the reason). `no matching files in scope` stays silent unless `--display-cli`. | +| `require_if` | Skip the check (exit 0, skipped status) unless prerequisites match: `files_present` globs and/or `importable_package` (layout-detected package with `__init__.py`, src or flat). Require-if skips print to stderr. `no matching files in scope` stays silent unless `--display-cli`. | Project overlays under `.shipgate/catalog/tools/` can replace or `extends:` a bundled tool without editing the package. diff --git a/docs/configuration.md b/docs/configuration.md index a74f43f..0b3cd68 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,9 +10,10 @@ threshold bindings. For suites, commands, and CI examples see Policy lives in `.shipgate/shipgate.yaml` or `[tool.shipgate]` in `pyproject.toml` (see `.shipgate/pyproject.toml.example` after init). `shipgate init` also scaffolds `.shipgate/catalog/`, `.shipgate/gates/`, `.shipgate/configs/`, and cache metadata. -That includes an `importlinter.ini` starter when an importable `src//` -package exists (src-layout only; flat-layout packages skip `import-linter.check` -with a stderr line such as `required files not present: src/*/__init__.py`) +That includes an `importlinter.ini` starter when layout detection finds an +importable package (src-layout `src//` or a flat `pkg/` at the repo root). +Trees with no importable package skip `import-linter.check` with a stderr line +such as `no importable package in project layout` and a `[tool.deptry]` section in `pyproject.toml` when missing. Managed env prepends `src/` onto `PYTHONPATH` when that src-layout package exists, so `lint-imports` can import the package without a consumer `.pth` diff --git a/pyproject.toml b/pyproject.toml index 48986f5..1ccedc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "shipgate" -version = "0.1.8" +version = "0.1.9" description = "Portable quality-gate orchestrator for developer tools" readme = "README.md" requires-python = ">=3.11,<3.15" diff --git a/src/shipgate/__init__.py b/src/shipgate/__init__.py index 505cff2..baa74c0 100644 --- a/src/shipgate/__init__.py +++ b/src/shipgate/__init__.py @@ -1,8 +1,13 @@ """ShipGate — portable, metadata-driven quality-gate orchestrator.""" +from importlib.metadata import PackageNotFoundError, version + from shipgate.api import install, load_catalog, run -__version__ = "0.1.5" +try: + __version__ = version("shipgate") +except PackageNotFoundError: + __version__ = "0.0.0" __all__ = [ "__version__", diff --git a/src/shipgate/catalog/bundled/catalog/tools/import-linter.check.yaml b/src/shipgate/catalog/bundled/catalog/tools/import-linter.check.yaml index 0bb9829..5225fba 100644 --- a/src/shipgate/catalog/bundled/catalog/tools/import-linter.check.yaml +++ b/src/shipgate/catalog/bundled/catalog/tools/import-linter.check.yaml @@ -1,9 +1,9 @@ # Enforces layer and forbidden-import contracts (architecture / policy). # `shipgate init` scaffolds configs/importlinter.ini with optional starter layers -# only when an importable src-layout package exists (`src//__init__.py`). +# when layout detection finds an importable package (src-layout or flat). # Customize contracts for your architecture. The package under test must be # importable. Managed env prepends src/ onto PYTHONPATH when src//__init__.py -# exists. Skipped when no src-layout package is present (require_if). +# exists. Skipped when no importable package is present (require_if.importable_package). import-linter.check: executable: lint-imports display_name: import-linter @@ -44,8 +44,4 @@ import-linter.check: - .py delivery: root require_if: - files_present: - - src/*/__init__.py - suggest_if: - files_present: - - src/*/__init__.py + importable_package: true diff --git a/src/shipgate/catalog/bundled/configs/importlinter.ini b/src/shipgate/catalog/bundled/configs/importlinter.ini index c00e1a0..32c6cd7 100644 --- a/src/shipgate/catalog/bundled/configs/importlinter.ini +++ b/src/shipgate/catalog/bundled/configs/importlinter.ini @@ -1,5 +1,5 @@ # ShipGate starter import contracts (copied by `shipgate init` / `configs sync`). -# Only scaffolded when an importable src-layout package is detected. +# Only scaffolded when an importable package is detected (src-layout or flat). # Replace optional layer names with your real package layout, or switch to # forbidden / independence contracts. root_package must be importable. # Managed env prepends src/ onto PYTHONPATH when src//__init__.py exists. diff --git a/src/shipgate/catalog/core/parser.py b/src/shipgate/catalog/core/parser.py index db3d935..82a3946 100644 --- a/src/shipgate/catalog/core/parser.py +++ b/src/shipgate/catalog/core/parser.py @@ -169,6 +169,7 @@ def _parse_require_if(raw: dict | None) -> RequireIfDefinition | None: return ( RequireIfDefinition( files_present=tuple(str(item) for item in raw.get("files_present", []) or []), + importable_package=raw.get("importable_package") is True, ) if raw else None diff --git a/src/shipgate/catalog/core/validate_tool_assets.py b/src/shipgate/catalog/core/validate_tool_assets.py index 40dbf61..22fb513 100644 --- a/src/shipgate/catalog/core/validate_tool_assets.py +++ b/src/shipgate/catalog/core/validate_tool_assets.py @@ -67,5 +67,7 @@ def validate_suggest_if(tool: ToolDefinition) -> None: def validate_require_if(tool: ToolDefinition) -> None: if tool.require_if is None: return - if not tool.require_if.files_present: - raise CatalogError(f"tool {tool.id!r} require_if.files_present must not be empty") + if not tool.require_if.files_present and not tool.require_if.importable_package: + raise CatalogError( + f"tool {tool.id!r} require_if must set files_present or importable_package" + ) diff --git a/src/shipgate/domain/catalog.py b/src/shipgate/domain/catalog.py index 8a1ae2c..1b198e3 100644 --- a/src/shipgate/domain/catalog.py +++ b/src/shipgate/domain/catalog.py @@ -78,9 +78,10 @@ class SuggestIfDefinition: @dataclass(frozen=True) class RequireIfDefinition: - """Skip the tool at check time unless at least one pattern matches.""" + """Skip the tool at check time unless prerequisites match.""" files_present: tuple[str, ...] = () + importable_package: bool = False @dataclass(frozen=True) diff --git a/src/shipgate/planning/check_resolver.py b/src/shipgate/planning/check_resolver.py index d0cb918..8cec4c8 100644 --- a/src/shipgate/planning/check_resolver.py +++ b/src/shipgate/planning/check_resolver.py @@ -25,6 +25,7 @@ effective_incremental, tool_paths_after_incremental, ) +from shipgate.project.layout.packages import detect_importable_packages from shipgate.project.python import discover_project_python if TYPE_CHECKING: @@ -141,12 +142,17 @@ def prepare(self, selected: SelectedTool, command: RunCommand) -> PreparedRun: return PreparedRun(request=resolved) def _require_if_skip_reason(self, tool: ToolDefinition) -> str | None: - if tool.require_if is None or not tool.require_if.files_present: + require_if = tool.require_if + if require_if is None: return None - if any_files_present(self.project_root, tool.require_if.files_present): - return None - patterns = ", ".join(tool.require_if.files_present) - return f"required files not present: {patterns}" + if require_if.importable_package and not detect_importable_packages(self.project_root): + return "no importable package in project layout" + if require_if.files_present and not any_files_present( + self.project_root, require_if.files_present + ): + patterns = ", ".join(require_if.files_present) + return f"required files not present: {patterns}" + return None @staticmethod def _requires_python_skip_reason(tool: ToolDefinition) -> str | None: diff --git a/src/shipgate/project/config_setup.py b/src/shipgate/project/config_setup.py index 8bf0cf8..325eeea 100644 --- a/src/shipgate/project/config_setup.py +++ b/src/shipgate/project/config_setup.py @@ -14,6 +14,7 @@ update_project_cache_env, ) from shipgate.project.layout import detect_layout +from shipgate.project.layout.packages import detect_importable_root_package from shipgate.project.scope_defaults import ( render_scopes_toml, render_scopes_yaml, @@ -57,27 +58,6 @@ def bundled_template_path(tool: ToolDefinition) -> Path: return bundled_root_path() / tool.configuration.bundled -def detect_importable_root_package(project_root: Path) -> str | None: - """Importable src-layout package name for import-linter / deptry scaffolding.""" - return RootPackageDetector(project_root).from_src_layout() - - -class RootPackageDetector: - def __init__(self, project_root: Path) -> None: - self.root = project_root.resolve() - - def from_src_layout(self) -> str | None: - src = self.root / "src" - if not src.is_dir(): - return None - packages = sorted( - path.name - for path in src.iterdir() - if path.is_dir() and not path.name.startswith(".") and (path / "__init__.py").is_file() - ) - return packages[0] if packages else None - - def render_root_package_template(text: str, root_package: str) -> str: return text.replace(ROOT_PACKAGE_PLACEHOLDER, root_package) @@ -214,7 +194,7 @@ def scaffold_bundled_configs(project_root: Path, catalog: Catalog) -> list[Path] root = project_root.resolve() created: list[Path] = [] seen: set[Path] = set() - # Only substitute import-linter placeholders when a real src package exists. + # Only substitute import-linter placeholders when a real package exists. importable_package = detect_importable_root_package(root) for tool in catalog.tools.values(): rel = project_config_relpath(tool) diff --git a/src/shipgate/project/layout/packages.py b/src/shipgate/project/layout/packages.py new file mode 100644 index 0000000..91745bc --- /dev/null +++ b/src/shipgate/project/layout/packages.py @@ -0,0 +1,63 @@ +"""Map layout python dirs to importable package names.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from shipgate.project.layout.engine import LayoutEngine + +if TYPE_CHECKING: + from pathlib import Path + + +def detect_importable_packages(project_root: Path) -> tuple[str, ...]: + """Importable package names from layout detection (src or flat).""" + return RootPackageDetector(project_root).packages() + + +def detect_importable_root_package(project_root: Path) -> str | None: + """First importable package name for import-linter / deptry scaffolding.""" + packages = detect_importable_packages(project_root) + return packages[0] if packages else None + + +def has_src_layout_package(project_root: Path) -> bool: + """True when ``src//__init__.py`` exists (managed PYTHONPATH=src).""" + return RootPackageDetector(project_root).from_src_layout() is not None + + +class RootPackageDetector: + def __init__(self, project_root: Path) -> None: + self.root = project_root.resolve() + + def packages(self) -> tuple[str, ...]: + names: list[str] = [] + for rel in LayoutEngine(self.root).detect().python_dirs: + names.extend(self._names_for_python_dir(rel)) + return tuple(sorted(set(names))) + + def from_src_layout(self) -> str | None: + found = self._packages_under(self.root / "src") + return found[0] if found else None + + def _names_for_python_dir(self, rel: str) -> tuple[str, ...]: + if rel == "src": + return self._packages_under(self.root / "src") + path = self.root / rel + return (path.name,) if (path / "__init__.py").is_file() else () + + @staticmethod + def _packages_under(parent: Path) -> tuple[str, ...]: + return ( + tuple( + sorted( + path.name + for path in parent.iterdir() + if path.is_dir() + and not path.name.startswith(".") + and (path / "__init__.py").is_file() + ) + ) + if parent.is_dir() + else () + ) diff --git a/src/shipgate/runtime/environment.py b/src/shipgate/runtime/environment.py index 2f894d9..606ad32 100644 --- a/src/shipgate/runtime/environment.py +++ b/src/shipgate/runtime/environment.py @@ -68,9 +68,9 @@ def managed_environment(project_root: Path) -> ExecutionEnvironment: def apply_src_layout_pythonpath(env: dict[str, str], project_root: Path) -> None: """Prepend src/ onto PYTHONPATH when an importable src-layout package exists.""" - from shipgate.project.config_setup import detect_importable_root_package + from shipgate.project.layout.packages import has_src_layout_package - if detect_importable_root_package(project_root) is None: + if not has_src_layout_package(project_root): return src = str((project_root / "src").resolve()) parts = [part for part in env.get("PYTHONPATH", "").split(os.pathsep) if part] diff --git a/tests/unit/catalog/test_parser.py b/tests/unit/catalog/test_parser.py index 909ec53..9625de5 100644 --- a/tests/unit/catalog/test_parser.py +++ b/tests/unit/catalog/test_parser.py @@ -63,6 +63,17 @@ def test_parse_tool_require_if(): tool = catalog.get_tool("demo.tool") assert tool.require_if is not None assert tool.require_if.files_present == ("pyproject.toml",) + assert tool.require_if.importable_package is False + + +def test_parse_tool_require_if_importable_package(): + raw = demo_tool_raw() + raw["require_if"] = {"importable_package": True} + catalog = CatalogParser.parse({"tools": {"demo.tool": raw}, "suites": {}}) + tool = catalog.get_tool("demo.tool") + assert tool.require_if is not None + assert tool.require_if.files_present == () + assert tool.require_if.importable_package is True def test_parse_install_download_and_known_bad(): diff --git a/tests/unit/catalog/test_validate.py b/tests/unit/catalog/test_validate.py index a5ea644..f216461 100644 --- a/tests/unit/catalog/test_validate.py +++ b/tests/unit/catalog/test_validate.py @@ -6,6 +6,7 @@ Catalog, CliOptionDefinition, InstallDefinition, + RequireIfDefinition, SuiteDefinition, ToolDefinition, ) @@ -115,6 +116,22 @@ def test_validator_rejects_invalid_requires_python(): CatalogValidator.validate(catalog) +def test_validator_rejects_empty_require_if(): + catalog = Catalog( + tools={ + "bad.tool": ToolDefinition( + id="bad.tool", + executable="bad", + modes=(RunMode.CHECK,), + require_if=RequireIfDefinition(), + ) + }, + suites={}, + ) + with pytest.raises(CatalogError, match="files_present or importable_package"): + CatalogValidator.validate(catalog) + + def test_bundled_deadcode_and_semgrep_declare_requires_python(): catalog = CatalogLoader.load() deadcode = catalog.get_tool("deadcode.check") diff --git a/tests/unit/planning/test_import_linter_resolve.py b/tests/unit/planning/test_import_linter_resolve.py new file mode 100644 index 0000000..66a7d82 --- /dev/null +++ b/tests/unit/planning/test_import_linter_resolve.py @@ -0,0 +1,82 @@ +from pathlib import Path + +from shipgate.catalog.loader import CatalogLoader +from shipgate.domain.execution import ExecutionEnvironment +from shipgate.domain.modes import RunMode +from shipgate.domain.project import ProjectConfig +from shipgate.domain.run_command import RunCommand +from shipgate.planning.utils.incremental import RunScopeSession +from shipgate.planning.workflow import SelectedTool +from shipgate.runtime.session.check_resolver import prepare_run +from shipgate.runtime.session.context import RunContext + + +class ImportLinterPrepare: + def __init__(self, tmp_path: Path) -> None: + self.tmp_path = tmp_path + + def run(self): + catalog = CatalogLoader.load() + selected = SelectedTool(tool_id="import-linter.check", mode=RunMode.CHECK) + command = RunCommand( + project_root=self.tmp_path, + target=self.tmp_path, + check="import-linter.check", + ) + return prepare_run( + selected=selected, + command=command, + context=self.context(selected), + catalog=catalog, + ) + + def context(self, selected: SelectedTool) -> RunContext: + return RunContext( + project=ProjectConfig(env="system", target=Path()), + project_root=self.tmp_path.resolve(), + suite_id=selected.tool_id, + selected_tools=(selected,), + environment=ExecutionEnvironment(kind="system", root=None, env={}), + parallel=False, + fail_fast=False, + scope_session=RunScopeSession( + project_root=self.tmp_path.resolve(), + changed_only=False, + since=None, + ), + ) + + +def test_prepare_run_skips_import_linter_without_importable_package(tmp_path: Path): + (tmp_path / "src").mkdir() + (tmp_path / "src" / "main.py").write_text("x = 1\n", encoding="utf-8") + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "fresh"\nversion = "0.1.0"\n', + encoding="utf-8", + ) + prepared = ImportLinterPrepare(tmp_path).run() + assert prepared.request is None + assert prepared.report is not None + assert prepared.report.status == "skipped" + assert prepared.report.extra["skipped"] == "no importable package in project layout" + + +def test_prepare_run_runs_import_linter_with_src_package(tmp_path: Path): + pkg = tmp_path / "src" / "demo" + pkg.mkdir(parents=True) + (pkg / "__init__.py").write_text("", encoding="utf-8") + prepared = ImportLinterPrepare(tmp_path).run() + assert prepared.report is None + assert prepared.request is not None + assert prepared.request.runnable == "import-linter.check" + + +def test_prepare_run_runs_import_linter_with_flat_package(tmp_path: Path): + pkg = tmp_path / "mypkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("", encoding="utf-8") + (pkg / "service.py").write_text("x = 1\n", encoding="utf-8") + prepared = ImportLinterPrepare(tmp_path).run() + assert prepared.report is None + assert prepared.request is not None + assert prepared.request.runnable == "import-linter.check" diff --git a/tests/unit/project/test_config_setup.py b/tests/unit/project/test_config_setup.py index 3b1d64f..1cfd35c 100644 --- a/tests/unit/project/test_config_setup.py +++ b/tests/unit/project/test_config_setup.py @@ -7,12 +7,12 @@ from shipgate.config.loader import ProjectConfigLoader from shipgate.paths import SHIPGATE_YAML from shipgate.project.config_setup import ( - detect_importable_root_package, ensure_minimal_pyproject, project_config_relpath, scaffold_bundled_configs, ) from shipgate.project.init import init_project, scaffold_project_layout +from shipgate.project.layout.packages import detect_importable_root_package def test_project_config_relpath_deduplicates_ruff(): @@ -49,6 +49,14 @@ def test_detect_importable_root_package_from_src_layout(tmp_path: Path): assert detect_importable_root_package(tmp_path) == "acme" +def test_detect_importable_root_package_from_flat_layout(tmp_path: Path): + pkg = tmp_path / "mypkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("", encoding="utf-8") + (pkg / "service.py").write_text("x = 1\n", encoding="utf-8") + assert detect_importable_root_package(tmp_path) == "mypkg" + + def test_detect_importable_returns_none_without_package(tmp_path: Path): (tmp_path / "src").mkdir() (tmp_path / "src" / "main.py").write_text("x = 1\n", encoding="utf-8") @@ -93,6 +101,17 @@ def test_scaffold_import_linter_substitutes_root_package(tmp_path: Path): assert "widgets.domain" not in content +def test_scaffold_import_linter_flat_layout(tmp_path: Path): + pkg = tmp_path / "mypkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("", encoding="utf-8") + (pkg / "service.py").write_text("x = 1\n", encoding="utf-8") + catalog = CatalogLoader.load() + scaffold_bundled_configs(tmp_path, catalog) + content = (tmp_path / ".shipgate/configs/importlinter.ini").read_text(encoding="utf-8") + assert "root_package = mypkg" in content + + def test_scaffold_merges_deptry_into_pyproject(tmp_path: Path): (tmp_path / "pyproject.toml").write_text( '[project]\nname = "demo"\nversion = "0.1.0"\n', diff --git a/tests/unit/runtime/test_check_resolver.py b/tests/unit/runtime/test_check_resolver.py index 3527bb2..cd13639 100644 --- a/tests/unit/runtime/test_check_resolver.py +++ b/tests/unit/runtime/test_check_resolver.py @@ -240,32 +240,6 @@ def test_prepare_run_short_circuits_when_incremental_clean(tmp_path: Path): assert prepared.report.extra["skipped"] == "no matching files in scope" -def test_prepare_run_skips_import_linter_without_src_package(tmp_path: Path): - (tmp_path / "src").mkdir() - (tmp_path / "src" / "main.py").write_text("x = 1\n", encoding="utf-8") - (tmp_path / "pyproject.toml").write_text( - '[project]\nname = "fresh"\nversion = "0.1.0"\n', - encoding="utf-8", - ) - catalog = CatalogLoader.load() - selected = SelectedTool(tool_id="import-linter.check", mode=RunMode.CHECK) - command = RunCommand( - project_root=tmp_path, - target=tmp_path, - check="import-linter.check", - ) - prepared = prepare_run( - selected=selected, - command=command, - context=make_run_context(tmp_path, selected), - catalog=catalog, - ) - assert prepared.request is None - assert prepared.report is not None - assert prepared.report.status == "skipped" - assert prepared.report.extra["skipped"] == "required files not present: src/*/__init__.py" - - def test_prepare_run_skips_deptry_without_pyproject(tmp_path: Path): (tmp_path / "src").mkdir() (tmp_path / "src" / "main.py").write_text("x = 1\n", encoding="utf-8") @@ -300,28 +274,6 @@ def test_prepare_run_skips_pip_audit_without_pyproject(tmp_path: Path): assert prepared.report.extra["skipped"] == "required files not present: pyproject.toml" -def test_prepare_run_runs_import_linter_with_src_package(tmp_path: Path): - pkg = tmp_path / "src" / "demo" - pkg.mkdir(parents=True) - (pkg / "__init__.py").write_text("", encoding="utf-8") - catalog = CatalogLoader.load() - selected = SelectedTool(tool_id="import-linter.check", mode=RunMode.CHECK) - command = RunCommand( - project_root=tmp_path, - target=tmp_path, - check="import-linter.check", - ) - prepared = prepare_run( - selected=selected, - command=command, - context=make_run_context(tmp_path, selected), - catalog=catalog, - ) - assert prepared.report is None - assert prepared.request is not None - assert prepared.request.runnable == "import-linter.check" - - def test_prepare_run_skips_deadcode_on_unsupported_python(tmp_path: Path, monkeypatch): (tmp_path / "app.py").write_text("x = 1\n", encoding="utf-8") monkeypatch.setattr( diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index e99f00a..08e90ed 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,3 +1,4 @@ +import tomllib from pathlib import Path from shipgate import __version__, load_catalog @@ -37,4 +38,6 @@ def test_public_api_run(tmp_path: Path): def test_version_exported(): - assert __version__ == "0.1.5" + root = Path(__file__).resolve().parents[2] + data = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + assert __version__ == data["project"]["version"] diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 493189f..871ffa1 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -82,8 +82,6 @@ def test_display_cli_prints_subprocess_argv(tmp_path, capsys): def test_require_if_skip_prints_without_display_cli(tmp_path, capsys): - (tmp_path / "pkg").mkdir() - (tmp_path / "pkg" / "__init__.py").write_text("", encoding="utf-8") app = ShipGateApp(catalog=CatalogLoader.load(), executor=FakeExecutor()) code = app.check( RunCommand( @@ -94,10 +92,7 @@ def test_require_if_skip_prints_without_display_cli(tmp_path, capsys): ) captured = capsys.readouterr() assert code == 0 - assert ( - "import-linter.check: (skipped: required files not present: src/*/__init__.py)" - in captured.err - ) + assert "import-linter.check: (skipped: no importable package in project layout)" in captured.err def test_no_matching_files_skip_silent_without_display_cli(tmp_path, capsys): diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py index a5603b3..70a81d9 100644 --- a/tests/unit/test_import.py +++ b/tests/unit/test_import.py @@ -1,5 +1,10 @@ +import tomllib +from pathlib import Path + from shipgate import __version__ -def test_version(): - assert __version__ == "0.1.5" +def test_version_matches_pyproject(): + root = Path(__file__).resolve().parents[2] + data = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + assert __version__ == data["project"]["version"] diff --git a/uv.lock b/uv.lock index 97cd264..d3ef9ee 100644 --- a/uv.lock +++ b/uv.lock @@ -2128,7 +2128,7 @@ wheels = [ [[package]] name = "shipgate" -version = "0.1.8" +version = "0.1.9" source = { editable = "." } dependencies = [ { name = "libcst" },