Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions dependency-license-policy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ reports = [{ version = "2026.2.25", license = "Mozilla Public License 2.0 (MPL 2
reason = "Unmodified character-detection library installed as a separate transitive dependency, imported rather than incorporated."
reports = [{ version = "5.2.0", license = "GNU Lesser General Public License v2 or later (LGPLv2+)" }]

[exceptions.matplotlib]
reason = "Plot rendering for the reporting layer. PSF-2.0 is the Python Software Foundation licence, ASF Category A."
reports = [{ version = "3.10.8", license = "Python Software Foundation License" }]


[exceptions.numpy]
reason = "Core numerical dependency, BSD-3-Clause overall. The additional identifiers cover vendored components and are each permissive or public-domain-equivalent. Python 3.10 resolves 2.2.6, which reports the same licence as the generic BSD classifier."
reports = [
Expand Down
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@ dependencies = [
"PyYAML>=6.0",
# The engine's config models are Pydantic; core/preview.py renders with
# Rich. Every third-party import must be declared here so the package
# installs standalone; tests/test_packaging_contract.py enforces it.
# installs standalone, and everything declared here must be imported;
# tests/test_packaging_contract.py enforces both.
"pydantic>=2.0",
"rich",
"pandas",
"pyarrow",
"seaborn",
"matplotlib",
"langdetect",
# NOTE: ipykernel is deliberately NOT a runtime dependency. The only
# IPython use in shipped code is a guarded `from IPython import
# get_ipython` probe in core/manifest.py, so requiring a Jupyter kernel
Expand Down
31 changes: 21 additions & 10 deletions tests/test_packaging_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,23 @@ def test_registry_discovers_both_plugins(self) -> None:


class TestDeclaredDependencies:
"""Every third-party import must be declared by the distribution that
contains it. Undeclared imports resolve fine in the monorepo, where a
sibling package happens to pull them in, and fail on a standalone install."""
"""Third-party imports and declared dependencies match in both directions.
An undeclared import resolves fine in the monorepo, where a sibling package
happens to pull it in, and fails on a standalone install. A declared
dependency nothing imports still installs, with everything it depends on,
for every user."""

#: Imported behind try/except ImportError on purpose; must stay optional.
#: Undeclared on purpose: a guarded notebook probe, not a dependency.
_OPTIONAL = {"IPython"}

#: Distribution names differ from import names for a few packages.
_IMPORT_TO_DISTRIBUTION = {
"yaml": "pyyaml",
"lingua": "lingua_language_detector",
"dotenv": "python_dotenv",
}

@staticmethod
def _imports(src_root: Path, own_package: str) -> set[str]:
found: set[str] = set()
Expand Down Expand Up @@ -122,21 +131,23 @@ def test_plugin_declares_everything_it_imports(self) -> None:
imported = self._imports(root / "src", "usersim")
declared = self._declared(root / "pyproject.toml")

# Distribution names differ from import names for a few packages.
aliases = {
"yaml": "pyyaml",
"lingua": "lingua_language_detector",
"dotenv": "python_dotenv",
}
# Packages this distribution ships; they are not dependencies.
first_party = {"usersim"}
missing = {
mod
for mod in imported - self._OPTIONAL - first_party
if aliases.get(mod, mod).lower().replace("-", "_") not in declared
if self._IMPORT_TO_DISTRIBUTION.get(mod, mod).lower().replace("-", "_") not in declared
}
assert not missing, f"the distribution imports but does not declare: {sorted(missing)}"

def test_plugin_imports_everything_it_declares(self) -> None:
imported = {
self._IMPORT_TO_DISTRIBUTION.get(mod, mod).lower().replace("-", "_")
for mod in self._imports(_REPO_ROOT / "src", "usersim")
}
unused = self._declared(_REPO_ROOT / "pyproject.toml") - imported
assert not unused, f"the distribution declares but never imports: {sorted(unused)}"

def test_optional_imports_stay_guarded(self) -> None:
"""An undeclared import is only acceptable behind try/except."""
src = _REPO_ROOT / "src"
Expand Down
Loading
Loading