Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
350512c
Fix cache isolation between configuration resolution modes
ibro45 Sep 18, 2026
98ccddb
Preserve configuration source across resolution generations
ibro45 Sep 18, 2026
6120c39
Add retained configuration recipes and isolated construction scopes
ibro45 Sep 18, 2026
448573f
Make configuration edits atomic and protect frozen source views
ibro45 Sep 18, 2026
dac009a
Expose pure retained definition inspection
ibro45 Sep 18, 2026
f8ff6f3
Validate schema argument boundaries and coerce values conservatively
ibro45 Sep 18, 2026
f12a630
Respect Python lexical boundaries and import expression results
ibro45 Sep 18, 2026
c915d83
Prepare unpublished Sparkwheel 0.1.0.dev0 snapshot
ibro45 Sep 18, 2026
95bfbdc
Resolve component guards before payload effects
ibro45 Sep 18, 2026
a84dbb0
Reject authored YAML duplicates with source locations
ibro45 Sep 18, 2026
66f9c23
Apply nested update operators to newly supplied containers
ibro45 Sep 18, 2026
5eac8c6
docs: repair generated API reference links
ibro45 Sep 18, 2026
276d29b
Resolve Python-selected references within stable build generations
ibro45 Sep 18, 2026
ce80a3a
Preserve source locations for lazy reference cycles
ibro45 Sep 18, 2026
78ad8f7
Align reference examples and complete repository quality checks
ibro45 Sep 18, 2026
e92a21f
Name documented return values for strict API generation
ibro45 Sep 18, 2026
007efa5
Preserve development versions in pinned release tooling
ibro45 Sep 18, 2026
74cf6b0
Clarify standalone configuration docs and object lifetimes
ibro45 Sep 19, 2026
f7f154b
fix: repair documentation contracts and typing dependencies
ibro45 Sep 19, 2026
c2286a0
fix: update Codecov action for migrated signing key
ibro45 Sep 19, 2026
67eacd9
fix: preserve blocked construction path provenance
ibro45 Sep 19, 2026
b5138c1
build: rehearse source installs and guard stable releases
ibro45 Sep 19, 2026
b73e786
docs: clarify source versions and nested error causes
ibro45 Sep 19, 2026
9a67c07
docs: advance the immutable public installation snapshot
ibro45 Sep 19, 2026
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
67 changes: 67 additions & 0 deletions .github/scripts/check_release_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Reject release tags unless stable metadata and fetched main ancestry agree.

Run with Python 3.11+ after fetching origin/main. This script does not publish.
"""

import argparse
import ast
import re
import subprocess
from pathlib import Path

STABLE_TAG = re.compile(r"refs/tags/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")


def git(root: Path, *arguments: str) -> str:
"""Read an identity from the checked-out repository."""
return subprocess.check_output(["git", "-C", str(root), *arguments], text=True, stderr=subprocess.PIPE).strip()


def validate_release(root: Path, event: str, ref: str, sha: str) -> str:
"""Return the stable version only for a matching checkout on fetched main."""
import tomllib

if event != "push" or STABLE_TAG.fullmatch(ref) is None:
raise ValueError("Publication requires a push of an exact stable vMAJOR.MINOR.PATCH tag.")
version = ref.removeprefix("refs/tags/v")
project = tomllib.loads((root / "pyproject.toml").read_text())["project"]
if project["version"] != version:
raise ValueError("Release tag does not match project.version.")
module = root / "src" / project["name"].replace("-", "_") / "__init__.py"
source_versions = [
ast.literal_eval(node.value)
for node in ast.parse(module.read_text()).body
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets)
]
if source_versions != [version]:
raise ValueError("Release tag does not match the single source __version__ literal.")
commit = git(root, "rev-parse", "--verify", f"{sha}^{{commit}}")
if git(root, "rev-parse", "HEAD") != commit:
raise ValueError("Release checkout does not match the event commit.")
subprocess.run(
["git", "-C", str(root), "merge-base", "--is-ancestor", commit, "refs/remotes/origin/main"],
check=True,
capture_output=True,
text=True,
)
return version


def main() -> None:
"""Check explicit event inputs; a nonzero exit prevents release steps."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--event", required=True)
parser.add_argument("--ref", required=True)
parser.add_argument("--sha", required=True)
args = parser.parse_args()
try:
version = validate_release(args.root, args.event, args.ref, args.sha)
except (ValueError, KeyError, OSError, subprocess.SubprocessError) as error:
parser.exit(1, f"Release rejected: {error}\n")
print(f"Verified stable release {version}: metadata, source version, checkout and main ancestry agree.")


if __name__ == "__main__":
main()
6 changes: 3 additions & 3 deletions .github/workflows/ci-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ jobs:
- name: Generate test summary
if: always()
run: |
echo "## Test Results - Python ${{ matrix.python }}" >> $GITHUB_STEP_SUMMARY
python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY
echo "## Test Results - Python ${{ matrix.python }}" >> "$GITHUB_STEP_SUMMARY"
python3 .github/scripts/test_summary.py >> "$GITHUB_STEP_SUMMARY"

- name: Upload test results
if: always()
Expand All @@ -65,7 +65,7 @@ jobs:

- name: Upload coverage to Codecov
if: matrix.python == '3.12'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./coverage.xml
fail_ci_if_error: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:

- name: Generate test summary
if: always()
run: python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY
run: python3 .github/scripts/test_summary.py >> "$GITHUB_STEP_SUMMARY"

- name: Upload test results
if: always()
Expand All @@ -92,7 +92,7 @@ jobs:
run: uv run coverage xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./coverage.xml
fail_ci_if_error: true
Expand Down
19 changes: 11 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
name: Build the package
runs-on: ubuntu-latest
timeout-minutes: 10
if: startsWith(github.ref, 'refs/tags') || github.event_name == 'workflow_dispatch'
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
permissions:
contents: read

Expand All @@ -28,14 +28,16 @@ jobs:
with:
install-deps: 'false'

- name: Verify tag is on main branch
if: startsWith(github.ref, 'refs/tags')
- name: Verify stable release tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
env:
RELEASE_EVENT: ${{ github.event_name }}
RELEASE_REF: ${{ github.ref }}
RELEASE_SHA: ${{ github.sha }}
run: |
git fetch origin main
if ! git merge-base --is-ancestor ${{ github.sha }} origin/main; then
echo "Error: Tag is not on the main branch"
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
uv run --no-project --python 3.12 python .github/scripts/check_release_tag.py \
--event "$RELEASE_EVENT" --ref "$RELEASE_REF" --sha "$RELEASE_SHA"

- name: Build a binary wheel and a source tarball
run: uv build
Expand All @@ -49,6 +51,7 @@ jobs:
publish:
name: Publish the package
needs: build
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand Down
21 changes: 14 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,27 @@ jobs:
name: Create GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 5
if: startsWith(github.ref, 'refs/tags')
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Verify tag is on main branch
- uses: ./.github/actions/setup
with:
install-deps: 'false'

- name: Verify stable release tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
env:
RELEASE_EVENT: ${{ github.event_name }}
RELEASE_REF: ${{ github.ref }}
RELEASE_SHA: ${{ github.sha }}
run: |
git fetch origin main
if ! git merge-base --is-ancestor ${{ github.sha }} origin/main; then
echo "Error: Tag is not on the main branch"
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
uv run --no-project --python 3.12 python .github/scripts/check_release_tag.py \
--event "$RELEASE_EVENT" --ref "$RELEASE_REF" --sha "$RELEASE_SHA"

- name: Create Release
uses: softprops/action-gh-release@v2
Expand Down
65 changes: 62 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ brew install just
# Or see https://github.com/casey/just for other platforms
```

### 2. Clone and Setup
### 2. Set up the matching source

Follow the public clone and editable commands in [installation](docs/getting-started/installation.md#development-setup). They select an immutable reviewed snapshot; use the explicitly agreed branch/revision for new work. From that clone, run:

```bash
git clone https://github.com/project-lighter/sparkwheel.git
cd sparkwheel
just setup
```

This uses or updates the clone's `.venv` and adds contributor dependencies. Activate that environment when running its tools directly.

This will:
- Install all dependencies (dev, test, doc groups)
- Set up pre-commit hooks
Expand Down Expand Up @@ -53,6 +55,30 @@ We use:

Pre-commit hooks will automatically run on commit.

## Keep documentation useful

Use one documentation set for people and agents. Keep behavior in ordinary
Python, teach one complete first example, and link to the canonical semantic
explanation instead of maintaining parallel copies.

- State the working directory, prerequisites, exact inputs and expected outputs
for a runnable example. Mark excerpts through their surrounding explanation.
- Preserve the distinction between source inspection and execution, and between
a shared result and a copied definition.
- Run changed complete examples with the documented install; record which checks
actually ran. Review other affected snippets against source.
- Build the site strictly and check affected links/anchors. With documentation
dependencies installed and the environment active, use:

```bash
python -m mkdocs build --strict
```

Keep explanatory Markdown under `docs/`, entrypoint guidance in README, and the
Python API reference generated from source. Prefer existing paths and descriptive
headings. New documentation machinery needs a concrete unmet need. A passing
build does not execute every code example.

## Pull Request Process

1. Fork the repository
Expand Down Expand Up @@ -86,3 +112,36 @@ When reporting issues, please include:
## License

By contributing, you agree that your contributions will be licensed under the Apache License 2.0.

## Version preparation

Version maintenance uses the pinned `bump-my-version==0.30.1` executable through
`uvx`, independently of runtime dependency installation. The supported recipe
parts are `major`, `minor`, `patch`, and `release`:

```bash
just bump-dry release # Preview 0.1.0.dev0 -> 0.1.0
just bump-dry patch # A stable 0.1.0 previews 0.1.1
```

`release` removes an existing `.devN` suffix without advancing the numeric
version. Numeric bumps advance that component and reset subordinate components
to stable; `patch` from `0.1.0.dev0` therefore previews `0.1.1`, not promotion to
`0.1.0`. Use `release` for that promotion. Unsupported parts, including the raw
`dev` counter, fail before invoking the tool. Starting another development cycle
requires a separately reviewed explicit version choice.

Dry runs disable file writes, commits and tags. After release authorization,
`just bump <part>` updates project metadata, the source version constant, the
bump configuration and only Sparkwheel's editable-root version in `uv.lock`; it
also creates the configured local commit and tag. Review the diff and run the
ordinary package checks before any separately authorized push or publication.
Current development snapshots have not been tagged or published by this work.

Previewing or preparing a version locally does not authorize its remote release.

## Stable release guard

Use the existing pinned `bump-my-version` recipes to preview or promote a version. Actual bump commands can create a commit and tag; `just bump-dry release` previews without those changes. For a rehearsal, work in disposable copies and explicitly disable commits and tags.

Publication accepts only a pushed `vMAJOR.MINOR.PATCH` tag matching both project metadata and the source version, at a commit on freshly fetched `main`. Development/prerelease tags and mismatches fail before publishing or creating a stable GitHub Release. Manual dispatch of Publish builds and retains workflow artifacts only. Current token authentication is unchanged. Building an artifact does not publish it or establish registry availability.
95 changes: 43 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,56 @@
<br/>
<div align="center">
<img src="assets/images/sparkwheel_banner.png" width="65%"/>
</div>
<br/><br/>
<p align="center">
<a href="https://github.com/project-lighter/sparkwheel/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/project-lighter/sparkwheel/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://codecov.io/gh/project-lighter/sparkwheel"><img alt="Coverage" src="https://codecov.io/gh/project-lighter/sparkwheel/branch/main/graph/badge.svg"></a>
<a href="https://pypi.org/project/sparkwheel/"><img alt="PyPI" src="https://img.shields.io/pypi/v/sparkwheel"></a>
<a href="https://github.com/project-lighter/sparkwheel/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
<a href="https://project-lighter.github.io/sparkwheel"><img alt="Documentation" src="https://img.shields.io/badge/docs-latest-olive"></a>
</p>
# Sparkwheel

<h3 align="center">YAML configuration meets Python</h3>
<p align="center">Define Python objects in YAML. Reference, compose, and instantiate them effortlessly.</p>
<br/>
Compose configuration, then build ordinary Python objects.

## Quick Start
Sparkwheel loads YAML or Python dictionaries, combines settings, and calls your
classes and functions. Your application keeps its normal Python code. `@` shares
a resolved object; `%` copies a definition for separate construction.

```bash
pip install sparkwheel
```

```yaml
# config.yaml
dataset:
num_classes: 10
batch_size: 32
**Development version:** this checkout is `0.1.0.dev0`, an unpublished development
package. The documentation describes this source, not necessarily the version
available on PyPI. Python 3.10 or newer is required; the installation below uses
Python 3.11. Torch and Lightning are not required. Use the source documentation
linked below for this pair; the hosted site may describe an earlier release.

model:
_target_: torch.nn.Linear
in_features: 784
out_features: "%dataset::num_classes" # Reference
Install the immutable reviewed public snapshot in a new directory (Python 3.11 and Git required):

training:
steps_per_epoch: "$10000 // @dataset::batch_size" # Expression
```bash
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install "sparkwheel @ git+https://github.com/project-lighter/sparkwheel.git@b73e786e8716d11a77206fb3482d21a621a4ba81"
python -m pip check
```

See [installation](docs/getting-started/installation.md) for Windows activation
and editable-development instructions. This full commit pin does not follow later branch changes.

```python
from sparkwheel import Config

config = Config()
config.update("config.yaml")
config = Config().update(
{
"words": ["red", "blue", "red"],
"counts": {"_target_": "collections.Counter", "_args_": ["@words"]},
}
)

model = config.resolve("model") # Actual torch.nn.Linear(784, 10)
print(dict(config.resolve("counts"))) # {'red': 2, 'blue': 1}
```

## Features

- **Declarative Objects** - Instantiate any Python class with `_target_`
- **Smart References** - `@` for resolved values, `%` for raw YAML
- **Composition by Default** - Dicts merge, lists extend automatically
- **Explicit Control** - `=` to replace, `~` to delete
- **Python Expressions** - Dynamic values with `$`
- **Schema Validation** - Type-check with dataclasses

**[Get Started](https://project-lighter.github.io/sparkwheel/getting-started/quickstart/)** · **[Documentation](https://project-lighter.github.io/sparkwheel/)** · **[Quick Reference](https://project-lighter.github.io/sparkwheel/user-guide/quick-reference/)**

## Community

- [Discord](https://discord.gg/zJcnp6KrUp) · [YouTube](https://www.youtube.com/channel/UCef1oTpv2QEBrD2pZtrdk1Q) · [Issues](https://github.com/project-lighter/sparkwheel/issues)

## About

Sparkwheel is a hard fork of [MONAI Bundle](https://github.com/Project-MONAI/MONAI/tree/dev/monai/bundle)'s config system, with the goal of making a more general-purpose configuration library for Python projects. It combines the best of MONAI Bundle and [Hydra](http://hydra.cc/)/[OmegaConf](https://omegaconf.readthedocs.io/), while introducing new features and improvements not found in either.
`get()` reads the authored configuration. `resolve()` can import modules, evaluate
expressions and construct objects. Use configuration from sources you trust.

| I want to… | Start here |
|---|---|
| Run a complete Python/YAML example | [Quick start](docs/getting-started/quickstart.md) |
| Understand source, objects and edits | [Configuration model](docs/user-guide/basics.md) |
| Share objects or copy definitions | [References](docs/user-guide/references.md) |
| Use my own classes and functions | [Python authoring](docs/user-guide/instantiation.md) |
| Combine files and command-line changes | [Composition](docs/user-guide/operators.md) · [CLI](docs/user-guide/cli.md) |
| Diagnose a configuration | [Troubleshooting](docs/user-guide/troubleshooting.md) |
| Look up syntax or APIs | [Quick reference](docs/user-guide/quick-reference.md) · [Documentation](https://project-lighter.github.io/sparkwheel/) |

Sparkwheel grew from MONAI Bundle's configuration system and now serves
standalone Python projects and [Lighter](https://github.com/project-lighter/lighter).
It is licensed under Apache 2.0. Report issues on
[GitHub](https://github.com/project-lighter/sparkwheel/issues).
Loading
Loading