just-makeit — Developer Guide¶
This document is the entry point for working on just-makeit itself.
It covers the project layout, how the code is organized, the test suite,
and the release process.
What is just-makeit?¶
just-makeit is a Python CLI tool that scaffolds Python C extension projects.
One command generates a complete, working project: core C library, thin Python
binding, CMake build system, and full test coverage — all passing before you
write a single line of code.
Key idea: At the C level, every "object" is the same structure:
_core.h (API + inline step()), _core.c (lifecycle + block processor),
and a CMake OBJECT library that compiles once and links into both the Python
.so and the combined C shared library.
The only thing that varies is the Python packaging layer:
- Standalone (
object <name>) — its own.so, imported asfrom pkg import Name - In-module (
object <name> --module mod) — shares a.sosubpackage, imported asfrom pkg.mod import Name
Repository layout¶
just-makeit/
├── src/just_makeit/ # the CLI package
│ ├── _cli.py # argument parsing and dispatch
│ ├── _cli_*.py # per-command argument parsers
│ ├── _new.py # `new` command — project scaffold
│ ├── _object.py # `object` command — add a type (standalone or in-module)
│ ├── _init.py # internal: standalone object file generation
│ ├── _module.py # `module` command — scaffold empty extension module
│ ├── _method.py # `method` command — named execute variants
│ ├── _property.py # `property` command — Python properties
│ ├── _function.py # `function` command — module-level C functions
│ ├── _add.py # `add` command — append state vars to existing object
│ ├── _perf.py # `perf` command — add performance annotations
│ ├── _impl.py # `--impl` body lifting (funcname or line range)
│ ├── _apply.py # `apply` command — sacred/glue materialize from TOML
│ ├── _regenerate.py # `regenerate` command — rebuild a component's files
│ ├── _remove.py # `remove` command — delete + strip TOML/CMake wiring
│ ├── _bind.py # `bind` command — synthesise _ext.c from a _core.h
│ ├── _build.py # `build`/`test`/`dry-run` commands
│ ├── _config.py # just-makeit.toml read/write
│ ├── _render.py # render engine + template constants loaded from templates/
│ ├── _context/ # make_*_ctx() context builders
│ ├── templates/ # the real template files (c/, cmake/, py/, make/, toml/, …)
│ ├── _scripts.py # entry points: jm-install-deps, jm-run-tests, jm-docker-e2e
│ └── scripts/ # bundled shell utilities (shipped in wheel)
│ ├── install-deps.sh # OS-aware dep installer + venv setup
│ └── docker-e2e.sh # Docker end-to-end smoke test
├── tests/ # pytest suite
│ ├── test_new.py # `new` command integration tests
│ ├── test_init.py # internal `_init.run()` tests (standalone path)
│ ├── test_cli.py # CLI dispatch tests (subprocess)
│ ├── test_add.py # `add` command tests
│ ├── test_perf.py # `perf` command tests
│ ├── test_templates.py # template rendering unit tests
│ ├── test_config.py # config load/save tests
│ ├── test_example_*.py # end-to-end example tests (cmake + build)
│ └── bench_scaffold.py # pytest-benchmark for scaffold generation speed
├── docs/ # MkDocs source
│ ├── developers/ # this directory
│ ├── examples/ # per-example walkthroughs
│ └── *.md # commands, workflow, types, perf, c-library
├── examples/ # repo-root README only; the worked examples
│ # now live in src/just_makeit/examples/ (dozens,
│ # bundled in the wheel as package data)
├── scripts/
│ ├── copy_examples.py # copies example sources into docs/
│ └── sync_version.py # keeps version strings in sync across files
├── .github/workflows/
│ ├── ci.yml # runs tests on every push to main / PR
│ ├── release.yml # triggered by v* tag: test → build → publish to PyPI
│ ├── artifact.yml # post-release smoke test: installs from PyPI, builds real projects
│ └── docs.yml # builds and deploys MkDocs site
├── CHANGELOG.md
├── pyproject.toml
└── uv.lock
How the code fits together¶
Command flow¶
just-makeit <cmd>
└── _cli.py:main()
├── new → _new.run()
│ └── _object.run(module=None) ← if --object given
│ └── _init.run() ← standalone path
├── object → _object.run()
│ ├── module=None → _init.run() (standalone)
│ └── module=name → in-module path
├── module → _module.run()
├── method/property/function → _method.*/_property.*/_function.*
├── add → _add.run()
├── perf → _perf.run()
├── apply → _apply.run() (sacred/glue materialize)
├── regenerate→ _regenerate.run() (delete + re-apply a component)
├── remove → _remove.run()
├── bind → _bind.run()
└── build/test/dry-run → _build.*
Templates and rendering¶
Generated file content lives as real files under
src/just_makeit/templates/ (c/, cmake/, py/, make/, toml/,
doc/, misc/). _render.py loads each at import time and substitutes
<<placeholder>> tokens (C/H templates wrap them as /*<<token>>*/ so
clang-format can still parse the file). The context dict is built by the
make_*_ctx() functions in _context/.
To change what generated files look like: edit the relevant file
under templates/, not a Python string constant.
Config (_config.py + just-makeit.toml)¶
just-makeit.toml is the source of truth for scaffolded state. It records:
[project]— name, version, build system, perf flag[<comp>]— state vars, arg/return types, for each standalone object[module.<name>]— objects list for each module
_config.py provides typed accessors (components(), modules(),
module_objects(), state_vars(), arg_type(), …).
Development setup¶
Run the tests:
uv run pytest # fast unit + integration tests
uv run pytest tests/test_examples.py # slow: cmake build + run (skipped by default)
uv run pytest --benchmark-only # scaffold generation benchmarks
The default pytest.ini_options in pyproject.toml ignores test_examples.py
to keep the normal suite fast.
Git workflow¶
All non-trivial changes — new features, bug fixes, refactors, docs — go through
a branch and a PR. Direct pushes to main are reserved for release mechanics
(version bump + CHANGELOG commit, see release-checklist.md).
Branch naming¶
| Prefix | Use |
|---|---|
feat/ |
new command, flag, or generated output |
fix/ |
bug fix |
docs/ |
documentation only |
chore/ |
tooling, CI, deps, version bump |
PR rules¶
- CI must be green before a PR can enter the merge queue (
ci.ymlruns on every PR and on the merge queue's batched commit). - Keep PRs focused — one logical change per PR makes bisect and revert easy.
- The PR title becomes the CHANGELOG entry; write it accordingly
(
fix: jm apply drops extra_link_libs on regeneration).
Merging¶
PRs land through the merge queue. Once CI is green, choose "Merge when
ready" to add the PR to the queue: GitHub rebases it onto the latest main,
runs the full CI on that batched commit (the required "CI passed" check), and
squash-merges automatically when green. You never have to manually rebase a
PR just because another landed ahead of it — the queue keeps things up to date
by construction. The branch is deleted on merge; history stays linear.
What goes directly on main¶
Only two things skip the PR process:
- Release bump —
chore: bump to X.Y.Z(pyproject.toml + CHANGELOG only). - Hotfix — a one-liner fix that is urgent and trivially correct (rare).
Adding a new template / feature¶
- Add or edit the template file under
src/just_makeit/templates/. - Update the context builder in
_context/if new placeholder keys are needed. - Wire the new file into the relevant
run()function (_new.py,_object.py,_init.py). - Add tests in
tests/test_new.pyortests/test_init.py. - Update the relevant page in
docs/commands/and any relevant workflow docs.
CI overview¶
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
push to main, PRs |
pytest on Ubuntu + macOS × Python 3.9–3.14; separate coverage job uploads to Codecov |
release.yml |
push of v* tag |
Same tests → build wheel → publish to PyPI |
artifact.yml |
after Release succeeds | Installs from PyPI, scaffolds real projects, cmake build + test, C library install + pkg-config/find_package verification |
docs.yml |
push to main |
Builds MkDocs site and deploys to GitHub Pages |
CI must be green on main before tagging a release.
Contributing¶
main is always working code. Never commit directly to main.
Branch workflow¶
-
Create a feature branch from
main: -
Make your changes. Commit early and often on the branch.
-
Push and open a PR:
-
Wait for CI to go green on the PR. Fix any failures before merging.
-
Add the PR to the merge queue ("Merge when ready"). The queue rebases it onto
main, re-runs CI on the batched commit, and squash-merges it automatically when green — no manual rebase, even if other PRs land first. -
The branch is deleted automatically on merge.
Branch naming¶
| Prefix | When to use |
|---|---|
feat/ |
New command, flag, or template feature |
fix/ |
Bug fix |
docs/ |
Docs-only changes |
chore/ |
Deps, CI config, version bumps, formatting |
Release checklist¶
See release-checklist.md for the step-by-step process.