Contributing to ZeoCore¶
Thanks for considering a contribution. This document covers how to set up a dev environment, what the verification gate expects, and how to add tests.
Development setup¶
This repo uses uv for environment and
dependency management, wired up through make:
make setup creates a .venv (Python 3.14 by default -- see PYTHON_VERSION
in the Makefile; the package itself requires 3.14+), installs zeocore in
editable mode with all optional integrations, and installs the dev and
lint extras. Activate it with:
Run make help at any point to see every available target.
Before you open a PR: the gate¶
This repo has one gate that matters: make verify. Run it before every
commit you intend to submit:
It runs, in order: format-check -> ruff lint -> mypy (strict) ->
arch-check (import-linter directional contracts) -> hygiene-check (fails
if production code detects it's under test) -> hygiene-secrets ->
plugin-boundary -> the test suite with coverage (floor: 90%).
For a slower, more thorough version that reinstalls from a clean environment first (catches issues that stale caches or a broken package layout would hide), use:
A PR that doesn't pass make verify won't merge cleanly -- run it locally
first, not just in CI.
Individual checks¶
If you want faster inner-loop feedback while iterating:
make format # auto-fix formatting with ruff + isort (mutates files)
make lint # ruff check + format --check
make typecheck # mypy --strict
make test-fast # pytest without coverage
make test-docs # documentation links + safe beginner example smoke tests
make test-module M=test_fs # run one module's tests with coverage
make test-docs is a focused inner-loop target. The full make verify test
step already discovers tests/test_docs, so documentation tests run there
exactly once without a separate gate step.
Code style¶
- Linting: ruff, configured in
pyproject.toml's[tool.ruff]/[tool.ruff.lint]. Enabled rule sets include pycodestyle (E/W), pyflakes (F), isort (I), flake8-comprehensions (C), flake8-bugbear (B), pyupgrade (UP), pep8-naming (N), flake8-annotations (ANN), flake8-bandit (S), and flake8-builtins (A). Line length is 88, double-quote strings. - Type checking: mypy in strict mode
(
disallow_untyped_defs,disallow_incomplete_defs,warn_return_any, etc -- see[tool.mypy]inpyproject.toml). New or changed code must be fully typed; mypy only checks what's annotated, so untyped code isn't actually gated. - Import architecture:
import-linterenforces the directional contracts declared in.importlinter(checked bymake arch-check). - Canonical import paths:
zeo_core.tools's mixins (IntegrationEnabledMixin,LifecycleMixin,ToolEnvInitializerMixin) are implemented underzeo_core.tools.mixins.*but should always be imported fromzeo_core.toolsdirectly (from zeo_core.tools import LifecycleMixin, notfrom zeo_core.tools.mixins.lifecycle import LifecycleMixin). SetZEO_WARN_NONCANONICAL_IMPORTS=1in your dev/CI environment to get aFutureWarningwhen this is violated -- it's opt-in (not on by default) because it cannot reliably distinguish a non-canonical import from the package's own internal bootstrap; seezeo_core/tools/mixins/__init__.py's module docstring for the full reasoning. - Examples: files under
examples/must importzeo_core(never a predecessor package name) and be runnable withuv run examples/<name>.py. Credential-gated examples may skip with a printed reason; they must not crash on a missing extra if they document that extra. - Documentation links: use repository-relative links in user-facing
Markdown so local readers and GitHub resolve the same files. Include heading
fragments when linking to a section, and run
make test-docsafter moving or renaming documentation. - Beginner examples: keep the credential-free onboarding examples
deterministic, offline, and runnable with the base install. If their
intentional output changes, update the expected output in
tests/test_docs/test_examples.py. Do not add credential, OAuth, server, ffmpeg, or optional-extra examples to that smoke-test allowlist. - No test-detection in production code: production code (
src/zeo_core) must never branch on whether it's running under test (noinspect.stack(), no"pytest" in sys.modules, etc).make hygiene-checkfails the build if it finds this pattern. If a test needs different behavior, inject it (a parameter, a fixture, a fake) rather than having production code sniff its caller.
Adding tests¶
- Tests live under
tests/, mirroringsrc/zeo_core/'s module layout -- seetests/README.mdfor the full structure and the fixtures available intests/conftest.py. - Test both success and failure paths, not just the happy path.
- Prefer the dedicated mock classes already present in each test package
over ad hoc
MagicMockwiring, for consistency with existing tests. - The coverage floor is 90% (
--cov-fail-under=90inmake test); a PR that drops coverage below that will fail the gate. hypothesisis available for property-based testing where it adds real value (see thedevextra inpyproject.toml).
Submitting a change¶
- Fork the repository and create a branch for your change.
- Make your change, with tests.
- Run
make verifylocally and confirm it passes clean. - Open a pull request describing what changed and why.
Reporting issues¶
Please open a GitHub issue with a clear description, steps to reproduce (for bugs), and your Python/zeocore version. For vulnerabilities, see SECURITY.md — do not file a public issue.
Releasing (maintainers)¶
Releases go through .github/workflows/publish.yml, using PyPI's trusted
publishing (OIDC) -- no API
token is stored as a repo secret for either index.
- Bump
pyproject.toml's[project] version. It is the single source of truth: installedzeo_core.__version__reads the distribution metadata throughimportlib.metadata, so there is no second version literal to edit. - Add a dated entry to
CHANGELOG.md, updateRELEASE_NOTES.mdwith the exact heading# zeocore X.Y.Z, update docs/examples, and runuv lock. Runmake verify,make build, andmake release-check. The latter verifies metadata consistency and that the version is unused on both indexes, using live positive controls. Run it before staging consumes the TestPyPI version. Index filenames cannot be reused. - Stage through Actions -> "Publish" -> "Run workflow", selecting the
release branch and target
testpypi. Require every job, including the clean-environment install, to pass. That smoke job gets the exact wheel URL fromhttps://test.pypi.org/pypi/zeocore/X.Y.Z/json, verifies its TestPyPI file host, and installs that URL while resolving dependencies exclusively fromhttps://pypi.org/simple/. Do not combine package indexes or install an unpinned latest version as evidence for this release. - After the reviewed release changes land on main, tag that verified commit:
git tag -a vX.Y.Z -m "Release zeocore X.Y.Z", thengit push origin vX.Y.Z. This triggers real PyPI publication. The workflow refuses a tag that disagrees with package metadata or release notes and creates the GitHub release fromRELEASE_NOTES.mdafter the PyPI smoke passes. - The workflow's own
smoke-testjob installs the just-published package into a clean venv and imports it -- treat that job's failure as the release having NOT actually succeeded, even if the publish step itself reported success (PyPI's own index can take a moment to propagate; the workflow waits, but a failure here is still real signal, not a fluke to ignore).
Trusted publisher setup (one-time, on each index's web UI — not something a CI workflow can do for itself):
- https://pypi.org/manage/account/publishing/
- https://test.pypi.org/manage/account/publishing/
Register a trusted publisher on each with all four fields exact:
repository owner profrodai, repository name zeocore, workflow
filename publish.yml (just the filename, not the full path), and
environment name pypi (for the pypi.org entry) or testpypi (for the
test.pypi.org entry). All four are baked into the OIDC token's claims and
must match byte-for-byte, or the publish step fails with
invalid-publisher: valid token, but no corresponding publisher -- an
error that does not say which field is wrong, so get all four right rather
than guessing from one failed attempt.