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
20 changes: 0 additions & 20 deletions campus/__init__.py

This file was deleted.

4 changes: 4 additions & 0 deletions campus/yapper/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""campus.yapper.backends

Storage backends for yapper event databases (SQLite and PostgreSQL).
"""
32 changes: 32 additions & 0 deletions docs/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@ yapper → (nothing — self-contained)
database URI arrives via argument, provider callable, or environment
([yapper.md](yapper.md))

## Namespace Package Rules (PEP 420)

The `campus` import namespace is a **native namespace package**: no
distribution ships a `campus/__init__.py`. Python merges every `campus/`
directory found on `sys.path` into one virtual package, so each
distribution owns its own subtree and consumers import one flat namespace:

- `campus-suite` ships `campus/auth/`, `campus/api/`, `campus/model/`, ...
- `campus-common` (planned, #970 phase 3) will ship `campus/common/`

Rules:

- Never add `campus/__init__.py` to any distribution. One stale regular
package shadows every other dist's portions **silently** — imports
resolve from the wrong place with no error at install time.
- Each distribution's own subpackages (`campus/common/`, `campus/auth/`,
...) remain regular packages with their own `__init__.py`; only the
shared ancestor level is init-less.
- After `poetry build`, audit the wheels:

```bash
python scripts/audit_wheel_namespace.py
```

- When upgrading environments across this conversion, rebuild venvs
instead of layering installs: leftover `campus/` directories or
`__pycache__`-only portion dirs from older installs keep serving stale
imports under the merged namespace.

The `campus_python` client library uses a separate top-level namespace and
is unaffected by these rules.

## Installation

### For Production Use
Expand Down
80 changes: 80 additions & 0 deletions scripts/audit_wheel_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Audit built wheels: verify they ship init-less PEP 420 namespace portions.

`campus` is a native namespace package (PEP 420): every distribution that
contributes subpackages under `campus/` MUST NOT ship `campus/__init__.py`.
A single stale init file silently shadows the namespace - whichever dist
ships it wins, and every other dist's submodules stop importing, with no
error at install time. This audit turns that failure mode into a build-time
exit code.

Checks per wheel:
1. No `campus/__init__.py` (the shared ancestor level must stay init-less).
2. Every subdirectory of `campus/` containing .py files ships an
`__init__.py` (each dist's own subpackages stay regular packages).
3. The wheel ships something under `campus/` at all (catches a `packages`
include that drifted empty).

Usage:
python scripts/audit_wheel_namespace.py # audit dist/*.whl
python scripts/audit_wheel_namespace.py dist/foo.whl # audit specific wheels

Exit code 0 when all wheels pass; 1 on any violation or missing wheels.
"""

import sys
import zipfile
from pathlib import Path

NAMESPACE = "campus"


def audit_wheel(wheel: Path) -> list[str]:
"""Return a list of namespace violations for one built wheel."""
with zipfile.ZipFile(wheel) as archive:
names = set(archive.namelist())

problems: list[str] = []

top_init = f"{NAMESPACE}/__init__.py"
if top_init in names:
problems.append(f"{wheel.name}: ships {top_init} - a regular package here shadows all other dists' portions")

# Subpackage dirs shipped by this dist must remain regular packages;
# only the shared ancestor level is allowed to be init-less.
py_dirs = {n.rsplit("/", 1)[0] for n in names if n.endswith(".py") and "/" in n}
shipped = {d for d in py_dirs if d.startswith(f"{NAMESPACE}/") and d != NAMESPACE}
for directory in sorted(shipped):
if f"{directory}/__init__.py" not in names:
problems.append(f"{wheel.name}: {directory}/ ships .py files without __init__.py")

if not shipped and not any(n.startswith(f"{NAMESPACE}/") for n in names):
problems.append(f"{wheel.name}: ships nothing under {NAMESPACE}/ - packages include is misconfigured")

return problems


def main(argv: list[str]) -> int:
wheels = [Path(a) for a in argv[1:]] or sorted(Path("dist").glob("*.whl"))
if not wheels:
print("no wheels found: run `poetry build` first (or pass paths explicitly)", file=sys.stderr)
return 1

failures = 0
for wheel in wheels:
problems = audit_wheel(wheel)
if problems:
failures += len(problems)
for problem in problems:
print(f"FAIL {problem}")
else:
print(f"OK {wheel.name}: init-less {NAMESPACE}/ portion, subpackages intact")

if failures:
print(f"\n{failures} namespace violation(s) across {len(wheels)} wheel(s)", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Loading