Roadmap · Project hygiene · P2 · S

Single-source the version

A build-it-yourself guide. The version string lives in two places today — version = "0.2.0" in pyproject.toml and __version__ = "0.2.0" in modulearn/__init__.py. Two sources of truth drift; a release where they disagree is confusing and entirely avoidable.

Done when: the version is written in exactly one place, the other is derived from it at build time, and modulearn --version matches the version of the built wheel with no manual sync.
0 / 0 steps

The approach

ModuLearn already builds with hatchling (see [build-system] in pyproject.toml), and hatchling can read the version straight out of a Python attribute. So keep __version__ in modulearn/__init__.py as the one source — runtime code and modulearn --version already import it — and have pyproject.toml declare the version dynamic, pointing back at that attribute.

  1. Source of truth: modulearn/__init__.py__version__.
  2. Derived at build: pyproject.toml reads it via [tool.hatch.version].
Why this direction? The runtime needs __version__ importable regardless (it's in __all__), and you can't import from pyproject.toml. Making the Python file authoritative means zero runtime machinery — the build tool does the reading, not your package.

Steps

  1. Make pyproject's version dynamic
    pyproject.toml — the [project] table

    Delete the hard-coded version = "0.2.0" line and declare it dynamic instead.

    [project]
    name = "modulearn"
    # version = "0.2.0"        <- remove this line
    dynamic = ["version"]
    ...
  2. Point hatchling at __init__.py
    pyproject.toml — a new table near the build config

    Tell the hatchling version hook which attribute to read.

    [tool.hatch.version]
    path = "modulearn/__init__.py"
    No pattern needed. Hatchling looks for a __version__ = "..." assignment by default, which is exactly what __init__.py already has.
  3. Leave __init__.py as the single source
    modulearn/__init__.py — unchanged

    Nothing to edit here — __version__ = "0.2.0" stays put. From now on this is the only line you bump to cut a release.

  4. Verify build and runtime agree

    Build a wheel and confirm its version is read from the Python attribute, and that the runtime reports the same string.

    pip install -e ".[dev]"                    # ensure hatchling + build are present
    python -m build 2>&1 | grep -i modulearn    # wheel name shows the version
    # e.g. Successfully built modulearn-0.2.0.tar.gz and modulearn-0.2.0-py3-none-any.whl
    modulearn --version                        # expect: modulearn 0.2.0
    python -c "import modulearn; print(modulearn.__version__)"   # 0.2.0
    The real test is a bump. Temporarily change __version__ to "0.2.1", rebuild, and confirm the wheel name follows — then revert. If the wheel tracks the edit with no other change, the two sources are truly one.
  5. Commit
    git add pyproject.toml
    git commit -m "Single-source the version from modulearn/__init__.py"
    git push
    gh run list --branch main --limit 1   # expect: success
  6. Mark it shipped on the roadmap

    Add done:true, to the {area:"hygiene", … title:"Single-source the version"} item in docs/roadmap.html.

Worth knowing

This is a dependency for clean releases. Once the version has one home, the CHANGELOG + tags ritual becomes trivial: bump __version__, add a changelog entry, tag — and pyproject, the wheel, and modulearn --version all follow automatically. No more "which file did I forget?"