ripple-effect 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]*'
7
+
8
+ jobs:
9
+ publish:
10
+
11
+ runs-on: ubuntu-latest
12
+ environment: release
13
+ permissions:
14
+ id-token: write # mandatory for trusted publishing
15
+ contents: read
16
+
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ - uses: actions/setup-python@v6
20
+ with:
21
+ python-version: "3.14"
22
+
23
+ - uses: astral-sh/setup-uv@v7
24
+ - run: uvx --with tox-uv tox -e py,style,typecheck
25
+ - run: uv build
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,38 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.10", "3.14"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - uses: actions/setup-python@v6
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - uses: astral-sh/setup-uv@v7
25
+ - run: uvx --with tox-uv tox -e py
26
+
27
+ linters:
28
+
29
+ runs-on: ubuntu-latest
30
+
31
+ steps:
32
+ - uses: actions/checkout@v6
33
+ - uses: actions/setup-python@v6
34
+ with:
35
+ python-version: "3.14"
36
+
37
+ - uses: astral-sh/setup-uv@v7
38
+ - run: uvx --with tox-uv tox -e style,typecheck,readme
@@ -0,0 +1,18 @@
1
+ # Various
2
+ *~
3
+ \#*#
4
+ .\#*
5
+ .*cache
6
+ *.lock
7
+ .DS_Store*
8
+ .*version
9
+ .idea/
10
+
11
+ # Build artifacts
12
+ __pycache__/
13
+ *.py[cod]
14
+ *.egg*
15
+ .tox/
16
+ .venv*/
17
+ build/
18
+ dist/
@@ -0,0 +1,38 @@
1
+ ## Commands
2
+
3
+ ```bash
4
+ # Run all tests + coverage report + linters
5
+ tox
6
+
7
+ # Run a single test by name
8
+ .venv/bin/pytest -k test_help
9
+
10
+ # Run ruff linter (check only)
11
+ tox -e style
12
+
13
+ # Auto-fix linting/formatting with ruff
14
+ tox -e reformat
15
+
16
+ # Run type checking
17
+ tox -e typecheck
18
+ ```
19
+
20
+ ## Architecture
21
+
22
+ **`src/ripple_effect/cli.py`** — click CLI entry point. Accepts a YAML config file and
23
+ top-level flags (`--dryrun`, `--verbose`).
24
+
25
+ **Config format** (YAML):
26
+
27
+ ```yaml
28
+ location: /tmp/ripple-workspace # where to clone/cache downstream repos
29
+ projects:
30
+ - git+https://github.com/org/project.git@main # remote ref
31
+ - ~/dev/local-project # local folder
32
+ ```
33
+
34
+ For each project ripple-effect will:
35
+ 1. Clone (or `git fetch --reset`) the repo into `location/<name>/`
36
+ 2. Create a venv, install the project + the library under test in editable mode via `uv`
37
+ 3. Run `pytest tests/` against that venv
38
+ 4. Collect and report pass/fail per project
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zoran Simic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: ripple-effect
3
+ Version: 0.0.1
4
+ Summary: Know your blast radius before you push
5
+ Author-email: Zoran Simic <zoran@simicweb.com>
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/zsimic/ripple-effect
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/x-rst
23
+ License-File: LICENSE
24
+ Requires-Dist: click
25
+ Requires-Dist: PyYAML
26
+ Requires-Dist: runez
27
+ Dynamic: license-file
28
+
29
+ ripple-effect
30
+ =============
31
+
32
+ Know your blast radius before you push.
33
+
34
+ Test a Python library against the downstream projects that depend on it —
35
+ before you tag a release and find out the hard way.
36
+
37
+ Installation
38
+ ------------
39
+
40
+ .. code-block:: bash
41
+
42
+ uvx ripple-effect config.yml
43
+
44
+ Usage
45
+ -----
46
+
47
+ Create a YAML config describing your library and the downstream projects to test against:
48
+
49
+ .. code-block:: yaml
50
+
51
+ proving-grounds: /tmp/ripple-effect-workspace
52
+ downstream-projects:
53
+ - git+https://github.com/example/project-b.git@main
54
+ - git+https://github.com/example/project-c.git@main
55
+
56
+ Then run:
57
+
58
+ .. code-block:: bash
59
+
60
+ ripple-effect config.yml
61
+
62
+ ripple-effect will:
63
+
64
+ 1. Clone (or update) each downstream project into ``proving-grounds``
65
+ 2. Create a venv for each, with the downstream project + your library installed in editable mode
66
+ 3. Run each project's test suite
67
+ 4. Report which passed, which failed
68
+
69
+ For local iteration (e.g. testing against a local checkout of a downstream project):
70
+
71
+ .. code-block:: yaml
72
+
73
+ proving-grounds: ~/dev/my-ripple-workspace
74
+ downstream-projects:
75
+ - git+https://github.com/example/project-b.git@main
76
+ - ~/dev/project-c
@@ -0,0 +1,48 @@
1
+ ripple-effect
2
+ =============
3
+
4
+ Know your blast radius before you push.
5
+
6
+ Test a Python library against the downstream projects that depend on it —
7
+ before you tag a release and find out the hard way.
8
+
9
+ Installation
10
+ ------------
11
+
12
+ .. code-block:: bash
13
+
14
+ uvx ripple-effect config.yml
15
+
16
+ Usage
17
+ -----
18
+
19
+ Create a YAML config describing your library and the downstream projects to test against:
20
+
21
+ .. code-block:: yaml
22
+
23
+ proving-grounds: /tmp/ripple-effect-workspace
24
+ downstream-projects:
25
+ - git+https://github.com/example/project-b.git@main
26
+ - git+https://github.com/example/project-c.git@main
27
+
28
+ Then run:
29
+
30
+ .. code-block:: bash
31
+
32
+ ripple-effect config.yml
33
+
34
+ ripple-effect will:
35
+
36
+ 1. Clone (or update) each downstream project into ``proving-grounds``
37
+ 2. Create a venv for each, with the downstream project + your library installed in editable mode
38
+ 3. Run each project's test suite
39
+ 4. Report which passed, which failed
40
+
41
+ For local iteration (e.g. testing against a local checkout of a downstream project):
42
+
43
+ .. code-block:: yaml
44
+
45
+ proving-grounds: ~/dev/my-ripple-workspace
46
+ downstream-projects:
47
+ - git+https://github.com/example/project-b.git@main
48
+ - ~/dev/project-c
@@ -0,0 +1,236 @@
1
+ # ripple-effect — Spec
2
+
3
+ > Know your blast radius before you push.
4
+
5
+ Test a Python library against its downstream dependent projects — before you tag a release and find out the hard way.
6
+
7
+ ---
8
+
9
+ ## Concept
10
+
11
+ Given a library **A** and a set of downstream projects **B**, **C**, …, ripple-effect:
12
+
13
+ 1. Resolves and validates each project reference (library + downstream) via `uv-metadata`
14
+ 2. Clones/updates each downstream project into a local workspace (git URL) or uses it in place (local folder)
15
+ 3. Builds a venv for each project
16
+ 4. Injects library **A** in editable mode into that venv
17
+ 5. Runs the project's test suite
18
+ 6. Reports pass/fail per project
19
+
20
+ The inject step is the point of the tool and is not configurable — it always runs
21
+ `uv pip install -e <library>` after the venv is prepared.
22
+
23
+ ---
24
+
25
+ ## Invocation
26
+
27
+ ```bash
28
+ uvx ripple-effect config.yml # build venvs and run tests as described in config.yml
29
+ ripple-effect config.yml # same, if installed globally
30
+ ripple-effect --dryrun config.yml # show what would be done, without doing it
31
+ ripple-effect --verbose config.yml # show more output
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Project references
37
+
38
+ Only two forms are accepted — for both `upstream` and `downstream-projects` entries:
39
+
40
+ | Form | Example | Behaviour |
41
+ |--------------|------------------------------------------------------------------|--------------------------------------------------------|
42
+ | Local folder | `.` or `~/github/runez` or `/path/to/project` | Used as-is, no git operations |
43
+ | Git URL | Any URL form accepted by `uv-metadata` (see below) | Cloned/updated into `proving-grounds/<name>/` |
44
+
45
+ PyPI refs (`requests`, `requests<2`), wheels, and sdists are **not** accepted — ripple-effect
46
+ needs a `tests/` folder to run against, which only local folders and git repos can provide.
47
+
48
+ ### Validation via `uv-metadata`
49
+
50
+ Every project reference (`upstream` and each `downstream-projects` entry) is validated early via
51
+ [`uv-metadata`](https://github.com/zsimic/uv-metadata):
52
+
53
+ ```bash
54
+ uv-metadata <ref> # full metadata as JSON
55
+ uv-metadata <ref> --key name # just the project name
56
+ ```
57
+
58
+ ripple-effect passes the reference as-is to `uv-metadata` and uses the result:
59
+ - If `uv-metadata` returns valid metadata — the reference is accepted, whatever form it took
60
+ - If `uv-metadata` returns no valid metadata — fail early with a clear error
61
+
62
+ `uv-metadata` accepts many URL forms (`https://`, `git+https://`, etc.) and normalises them
63
+ internally. ripple-effect doesn't need to validate or rewrite URLs — it just hands them off.
64
+ The `name` field from the metadata is used to name the workspace subfolder for git repos.
65
+
66
+ ---
67
+
68
+ ## Config format (YAML)
69
+
70
+ ```yaml
71
+ # The upstream library under test. Accepts: local folder or git URL.
72
+ # Default: current working directory (.).
73
+ # Future: upstream-libs: [...] will allow multiple upstream libs in one run.
74
+ upstream: .
75
+
76
+ # Where to clone/cache downstream git repos.
77
+ # May use ~ and env vars. Subfolders are named after each project's name (from uv-metadata).
78
+ proving-grounds: /tmp/ripple-proving-grounds
79
+
80
+ # Optional global defaults, overridable per project.
81
+ defaults:
82
+ # How to build the venv. Default: auto-detected (see Three-step model below).
83
+ prepare: auto
84
+ # How to run the tests.
85
+ test: ".venv/bin/pytest tests/"
86
+
87
+ downstream-projects:
88
+ # Git URL (any form accepted by uv-metadata) — cloned/updated into proving-grounds/<name>/
89
+ - https://github.com/codrsquad/portable-python.git@main
90
+ - git+https://github.com/codrsquad/pickley.git@main
91
+
92
+ # Local folder — used as-is, no git operations
93
+ - ~/dev/my-other-project
94
+
95
+ # Per-project overrides (mapping form)
96
+ - url: https://github.com/org/project.git@main
97
+ prepare: "hatch env create" # project uses hatch instead of uv
98
+ test: ".venv/bin/pytest tests/ -x" # stop on first failure
99
+ ```
100
+
101
+ ### Project entry forms
102
+
103
+ A project entry can be a plain string (shorthand) or a mapping with explicit keys:
104
+
105
+ | Key | Description |
106
+ |-----------|-----------------------------------------------------------------------------|
107
+ | `url` | Any URL form accepted by `uv-metadata` — cloned/updated into `proving-grounds/<name>/` |
108
+ | `folder` | Local path — used as-is, no git operations |
109
+ | `prepare` | Override the global `defaults.prepare` for this project |
110
+ | `test` | Override the global `defaults.test` for this project |
111
+
112
+ Plain string entries are tried against `uv-metadata` first; if that succeeds and the ref
113
+ is not a local path, it is treated as a `url`. Local paths (`.`, `~`, `/`, or existing
114
+ directories) are treated as `folder` without calling out to `uv-metadata`.
115
+
116
+ ---
117
+
118
+ ## Three-step execution model
119
+
120
+ For each downstream project, ripple-effect runs three steps in order:
121
+
122
+ ```
123
+ prepare → inject → test
124
+ ```
125
+
126
+ ### Step 1 — prepare
127
+
128
+ Builds the `.venv/` for the downstream project. Configurable via `defaults.prepare` or
129
+ per-project `prepare`. When set to `auto` (the default), ripple-effect detects:
130
+
131
+ | Condition | Command |
132
+ |----------------------------|----------------------------------------------------------------|
133
+ | `uv.lock` present | `uv sync` |
134
+ | `requirements.txt` present | `uv venv && uv pip install -r requirements.txt` |
135
+ | neither | error — explicit `prepare` required |
136
+
137
+ Any string is accepted as the prepare command; ripple-effect just runs it and moves on.
138
+ This accommodates projects using other venv managers (e.g. `hatch env create`, `poetry install`,
139
+ `pipenv install`) without ripple-effect needing to know about them.
140
+
141
+ ### Step 2 — inject (not configurable)
142
+
143
+ ```bash
144
+ uv pip install -e <upstream>
145
+ ```
146
+
147
+ Injects the upstream library in editable mode into the venv built in step 1, overriding
148
+ whatever version was installed (pinned or otherwise).
149
+
150
+ **Special case — upstream is a local folder:**
151
+ Before running inject, ripple-effect checks `uv pip freeze` in the project's venv. If the
152
+ upstream is already installed in editable mode pointing at the right folder, inject is
153
+ skipped and the venv is reused as-is. If it is pinned/wrong, the venv is recreated from
154
+ scratch (prepare re-runs) then inject proceeds.
155
+
156
+ **Upstream identified by name:** `uv-metadata <upstream> --key name` gives the package name
157
+ used to find it in `uv pip freeze` output.
158
+
159
+ ### Step 3 — test
160
+
161
+ Runs the project's test suite against the prepared+injected venv. Configurable via
162
+ `defaults.test` or per-project `test`.
163
+
164
+ Default: `.venv/bin/pytest tests/`
165
+
166
+ ---
167
+
168
+ ## Known limitation: tox
169
+
170
+ **tox is intentionally out of scope for v1.**
171
+
172
+ When tox runs, it creates its own isolated envs from scratch — the `.venv/` that
173
+ ripple-effect prepared and injected into is invisible to tox's envs. There is no clean
174
+ universal way to inject an editable library into tox environments without file surgery
175
+ (editing `requirements.txt`, `pyproject.toml`, or similar).
176
+
177
+ Workaround for power users: write a thin wrapper script that performs the injection
178
+ before calling tox, then point `test:` at that script. ripple-effect doesn't need to
179
+ know it's there.
180
+
181
+ A future version could add first-class tox support with explicit strategies
182
+ (e.g. `requirements.txt` surgery + restore, or `[tool.uv.sources]` injection + restore).
183
+
184
+ ---
185
+
186
+ ## CI usage example (GitHub Actions)
187
+
188
+ ```yaml
189
+ - name: Run downstream integration tests
190
+ run: uvx ripple-effect ci-config.yml
191
+ ```
192
+
193
+ With a `ci-config.yml`:
194
+
195
+ ```yaml
196
+ upstream: .
197
+ proving-grounds: $RUNNER_TEMP/ripple-workspace
198
+ downstream-projects:
199
+ - https://github.com/codrsquad/portable-python.git@main
200
+ - https://github.com/codrsquad/pickley.git@main
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Local iteration example
206
+
207
+ ```yaml
208
+ upstream: ~/github/runez
209
+ proving-grounds: ~/dev/ripple-workspace
210
+
211
+ downstream-projects:
212
+ - https://github.com/codrsquad/portable-python.git@main
213
+ - ~/github/pickley # local checkout, no git pull
214
+ - folder: ~/github/some-hatch-project
215
+ prepare: "hatch env create" # project uses hatch rather than uv
216
+ ```
217
+
218
+ ---
219
+
220
+ ## v1 scope
221
+
222
+ - [ ] Config parsing (YAML)
223
+ - [ ] Project reference validation via `uv-metadata` (fail early on invalid refs)
224
+ - [ ] Project cloning/updating (`git clone` / `git fetch --reset`) for git URLs
225
+ - [ ] Auto-detection of prepare strategy (`uv.lock` vs `requirements.txt`)
226
+ - [ ] Editable-install check (`uv pip freeze`) before deciding to recreate venv
227
+ - [ ] `uv pip install -e <upstream>` injection
228
+ - [ ] Test runner (`.venv/bin/pytest tests/`)
229
+ - [ ] Per-project `prepare` / `test` overrides
230
+ - [ ] `--dryrun` mode
231
+ - [ ] Pass/fail summary report
232
+
233
+ Out of scope for v1:
234
+ - tox support
235
+ - Parallel project execution
236
+ - Caching venvs across runs
@@ -0,0 +1,169 @@
1
+ [build-system]
2
+ requires = ["setuptools", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools_scm]
6
+ local_scheme = "dirty-tag"
7
+
8
+ [project]
9
+ name = "ripple-effect"
10
+ dynamic = ["version"]
11
+ description = "Know your blast radius before you push"
12
+ readme = "README.rst"
13
+ requires-python = ">=3.10"
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ authors = [
17
+ {name = "Zoran Simic", email = "zoran@simicweb.com"},
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Software Development :: Testing",
32
+ "Topic :: Utilities",
33
+ ]
34
+ dependencies = [
35
+ "click",
36
+ "PyYAML",
37
+ "runez",
38
+ ]
39
+
40
+ [dependency-groups]
41
+ test = [
42
+ "pytest-cov",
43
+ ]
44
+ dev = [
45
+ { include-group = "test" },
46
+ ]
47
+
48
+ [project.scripts]
49
+ ripple-effect = "ripple_effect:main"
50
+
51
+ [project.urls]
52
+ Source = "https://github.com/zsimic/ripple-effect"
53
+
54
+
55
+ [tool.coverage.run]
56
+ data_file = ".tox/.coverage"
57
+ source = ["src"]
58
+ patch = ["subprocess"]
59
+ sigterm = true
60
+
61
+ [tool.coverage.xml]
62
+ output = ".tox/test-reports/coverage.xml"
63
+
64
+ [tool.coverage.html]
65
+ directory = ".tox/test-reports/htmlcov"
66
+
67
+ [tool.pytest]
68
+ cache_dir = ".tox/pytest_cache"
69
+ testpaths = ["tests"]
70
+
71
+ [tool.ruff]
72
+ cache-dir = ".tox/.ruff_cache"
73
+ line-length = 140
74
+
75
+ [tool.ruff.lint]
76
+ extend-select = [
77
+ "A", # flake8-builtins
78
+ "B", # flake8-bugbear
79
+ "C4", # flake8-comprehensions
80
+ "C90", # mccabe
81
+ "DTZ", # flake8-datetimez
82
+ "E", # pycodestyle errors
83
+ "ERA", # eradicate
84
+ "F", # pyflakes
85
+ "FLY", # flynt
86
+ "G", # flake8-logging-format
87
+ "I", # isort
88
+ "INT", # flake8-gettext
89
+ "PGH", # pygrep-hooks
90
+ "PIE", # flake8-pie
91
+ "PT", # flake8-pytest
92
+ "PYI", # flake8-pyi
93
+ "Q", # flake8-quotes
94
+ "RSE", # flake8-raise
95
+ "RUF", # ruff-specific
96
+ "S", # flake8-bandit
97
+ "SIM", # flake8-simplify
98
+ "SLOT", # flake8-slots
99
+ "T10", # flake8-debugger
100
+ "TID", # flake8-tidy-imports
101
+ "TCH", # flake8-type-checking
102
+ "TD", # flake8-todos
103
+ "W", # pycodestyle warnings
104
+ ]
105
+ [tool.ruff.lint.per-file-ignores]
106
+ "tests/**" = ["S101"]
107
+
108
+ [tool.ruff.lint.isort]
109
+ order-by-type = false
110
+
111
+ [tool.ruff.lint.mccabe]
112
+ max-complexity = 19
113
+
114
+ [tool.ruff.lint.pydocstyle]
115
+ convention = "numpy"
116
+
117
+ [tool.tox]
118
+ env_list = ["py310", "py314", "coverage", "style", "typecheck", "readme"]
119
+
120
+ [tool.tox.env_run_base]
121
+ use_develop = true
122
+ dependency_groups = ["test"]
123
+ commands = [
124
+ ["pytest", "-vv", "--cov=src/ripple_effect", "tests"]
125
+ ]
126
+
127
+ [tool.tox.env_run_base.set_env]
128
+ COVERAGE_FILE = "{toxworkdir}/.coverage.{envname}"
129
+ PYTHONDONTWRITEBYTECODE = "1"
130
+
131
+ [tool.tox.env.coverage]
132
+ skip_install = true
133
+ deps = ["coverage"]
134
+ set_env = { COVERAGE_FILE = "{toxworkdir}/.coverage" }
135
+ commands = [
136
+ ["coverage", "combine"],
137
+ ["coverage", "report", "-m"],
138
+ ["coverage", "xml"],
139
+ ["coverage", "html"]
140
+ ]
141
+
142
+ [tool.tox.env.style]
143
+ skip_install = true
144
+ deps = ["ruff"]
145
+ commands = [
146
+ ["ruff", "check"],
147
+ ["ruff", "format", "--diff"]
148
+ ]
149
+
150
+ [tool.tox.env.typecheck]
151
+ use_develop = true
152
+ deps = ["pyright"]
153
+ commands = [["pyright"]]
154
+
155
+ [tool.tox.env.readme]
156
+ skip_install = true
157
+ deps = ["uv", "twine"]
158
+ commands = [
159
+ ["uv", "build", "-q", "-o", "{env_dir}/dist"],
160
+ ["twine", "check", "{env_dir}/dist/*"]
161
+ ]
162
+
163
+ [tool.tox.env.reformat]
164
+ skip_install = true
165
+ deps = ["ruff"]
166
+ commands = [
167
+ ["ruff", "check", "--fix"],
168
+ ["ruff", "format"]
169
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """ripple-effect: Know your blast radius before you push."""
2
+
3
+ from ripple_effect.cli import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,16 @@
1
+ """CLI entry point for ripple-effect."""
2
+
3
+ import click
4
+
5
+
6
+ @click.command()
7
+ @click.argument("config", type=click.Path(exists=True, dir_okay=False))
8
+ @click.option("-n", "--dryrun", is_flag=True, help="Show what would be done without doing it")
9
+ @click.option("-v", "--verbose", is_flag=True, help="Show more output")
10
+ def main(config: str, dryrun: bool, verbose: bool) -> None:
11
+ """Test a Python library against its downstream dependents.
12
+
13
+ CONFIG is a YAML file describing the library under test and the downstream
14
+ projects to run against it.
15
+ """
16
+ click.echo("ripple-effect: not yet implemented")
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: ripple-effect
3
+ Version: 0.0.1
4
+ Summary: Know your blast radius before you push
5
+ Author-email: Zoran Simic <zoran@simicweb.com>
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/zsimic/ripple-effect
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/x-rst
23
+ License-File: LICENSE
24
+ Requires-Dist: click
25
+ Requires-Dist: PyYAML
26
+ Requires-Dist: runez
27
+ Dynamic: license-file
28
+
29
+ ripple-effect
30
+ =============
31
+
32
+ Know your blast radius before you push.
33
+
34
+ Test a Python library against the downstream projects that depend on it —
35
+ before you tag a release and find out the hard way.
36
+
37
+ Installation
38
+ ------------
39
+
40
+ .. code-block:: bash
41
+
42
+ uvx ripple-effect config.yml
43
+
44
+ Usage
45
+ -----
46
+
47
+ Create a YAML config describing your library and the downstream projects to test against:
48
+
49
+ .. code-block:: yaml
50
+
51
+ proving-grounds: /tmp/ripple-effect-workspace
52
+ downstream-projects:
53
+ - git+https://github.com/example/project-b.git@main
54
+ - git+https://github.com/example/project-c.git@main
55
+
56
+ Then run:
57
+
58
+ .. code-block:: bash
59
+
60
+ ripple-effect config.yml
61
+
62
+ ripple-effect will:
63
+
64
+ 1. Clone (or update) each downstream project into ``proving-grounds``
65
+ 2. Create a venv for each, with the downstream project + your library installed in editable mode
66
+ 3. Run each project's test suite
67
+ 4. Report which passed, which failed
68
+
69
+ For local iteration (e.g. testing against a local checkout of a downstream project):
70
+
71
+ .. code-block:: yaml
72
+
73
+ proving-grounds: ~/dev/my-ripple-workspace
74
+ downstream-projects:
75
+ - git+https://github.com/example/project-b.git@main
76
+ - ~/dev/project-c
@@ -0,0 +1,18 @@
1
+ .gitignore
2
+ DEVELOP.md
3
+ LICENSE
4
+ README.rst
5
+ SPEC.md
6
+ pyproject.toml
7
+ .github/workflows/release.yml
8
+ .github/workflows/tests.yml
9
+ src/ripple_effect/__init__.py
10
+ src/ripple_effect/cli.py
11
+ src/ripple_effect.egg-info/PKG-INFO
12
+ src/ripple_effect.egg-info/SOURCES.txt
13
+ src/ripple_effect.egg-info/dependency_links.txt
14
+ src/ripple_effect.egg-info/entry_points.txt
15
+ src/ripple_effect.egg-info/requires.txt
16
+ src/ripple_effect.egg-info/top_level.txt
17
+ tests/conftest.py
18
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ripple-effect = ripple_effect:main
@@ -0,0 +1,3 @@
1
+ click
2
+ PyYAML
3
+ runez
@@ -0,0 +1 @@
1
+ ripple_effect
@@ -0,0 +1,5 @@
1
+ from runez.conftest import cli, ClickRunner, temp_folder # noqa: F401, fixtures
2
+
3
+ from ripple_effect import main
4
+
5
+ ClickRunner.default_main = main
@@ -0,0 +1,19 @@
1
+ import runez
2
+
3
+
4
+ def test_help(cli):
5
+ cli.run("--help")
6
+ assert cli.succeeded
7
+ assert "downstream" in cli.logged.stdout
8
+
9
+
10
+ def test_missing_config(cli, temp_folder):
11
+ cli.run("no-such-config.yml")
12
+ assert cli.failed
13
+
14
+
15
+ def test_sample_config(cli, temp_folder):
16
+ runez.write("sample.yml", "# placeholder\n", logger=None)
17
+ cli.run("sample.yml")
18
+ assert cli.succeeded
19
+ assert "not yet implemented" in cli.logged.stdout