coverage-flashlight 1.0.0__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,119 @@
1
+ name: Release
2
+ run-name: Release ${{ inputs.release_type }} from ${{ github.ref_name }}
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ release_type:
7
+ description: Select release type
8
+ required: true
9
+ type: choice
10
+ options: [release-current, dev, release-patch, release-minor]
11
+ default: dev
12
+ concurrency:
13
+ group: release-${{ github.ref }}
14
+ cancel-in-progress: false
15
+ env:
16
+ UV_PYTHON: "3.13"
17
+ UV_VERSION: "0.10.6"
18
+ jobs:
19
+ publish:
20
+ name: Publish PyPI package and GitHub release
21
+ runs-on: ubuntu-latest
22
+ environment: pypi
23
+ permissions:
24
+ contents: write
25
+ id-token: write
26
+ steps:
27
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28
+ with:
29
+ fetch-depth: 0
30
+ persist-credentials: false
31
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
32
+ with:
33
+ version: ${{ env.UV_VERSION }}
34
+ enable-cache: true
35
+ cache-dependency-glob: uv.lock
36
+ - run: uv python install "$UV_PYTHON"
37
+ - name: Select version
38
+ id: version
39
+ env:
40
+ RELEASE_TYPE: ${{ inputs.release_type }}
41
+ run: |
42
+ CURRENT=$(uv version --short)
43
+ case "$RELEASE_TYPE" in
44
+ release-current) ;;
45
+ dev)
46
+ if [[ "$CURRENT" == *".dev"* ]]; then
47
+ uv version --bump dev
48
+ else
49
+ uv version --bump patch --bump dev
50
+ fi ;;
51
+ release-patch)
52
+ if [[ "$CURRENT" == *".dev"* ]]; then
53
+ uv version --bump stable
54
+ else
55
+ uv version --bump patch
56
+ fi ;;
57
+ release-minor) uv version --bump minor ;;
58
+ *) exit 1 ;;
59
+ esac
60
+ VERSION=$(uv version --short)
61
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
62
+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
63
+ if [[ "$VERSION" == *".dev"* ]]; then
64
+ echo "prerelease=true" >> "$GITHUB_OUTPUT"
65
+ else
66
+ echo "prerelease=false" >> "$GITHUB_OUTPUT"
67
+ fi
68
+ - run: uv sync --locked --all-extras
69
+ - run: uv run pytest
70
+ - run: uv run ruff check .
71
+ - run: uv run ruff format --check .
72
+ - run: uv build
73
+ - name: Verify installed wheel and packaged report assets
74
+ run: |
75
+ wheel=(dist/*.whl)
76
+ uv venv /tmp/flashlight-wheel
77
+ uv pip install --python /tmp/flashlight-wheel/bin/python "${wheel[0]}"
78
+ /tmp/flashlight-wheel/bin/flashlight --help
79
+ mkdir /tmp/flashlight-smoke
80
+ cd /tmp/flashlight-smoke
81
+ printf 'answer = 42\n' > sample.py
82
+ /tmp/flashlight-wheel/bin/flashlight run -- sample.py
83
+ /tmp/flashlight-wheel/bin/flashlight run --trace -- sample.py
84
+ test -s htmlcov/flashlight.html
85
+ test -s htmlcov/execution.html
86
+ - name: Commit and tag version
87
+ env:
88
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
89
+ VERSION: ${{ steps.version.outputs.version }}
90
+ TAG: ${{ steps.version.outputs.tag }}
91
+ RELEASE_TYPE: ${{ inputs.release_type }}
92
+ run: |
93
+ git config user.name "github-actions[bot]"
94
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
95
+ git add pyproject.toml uv.lock
96
+ git diff --cached --check
97
+ if [[ "$RELEASE_TYPE" == "release-current" ]]; then
98
+ git diff --cached --exit-code
99
+ else
100
+ git commit -m "$VERSION"
101
+ fi
102
+ git tag "$TAG"
103
+ gh auth setup-git
104
+ git push --atomic origin "HEAD:$GITHUB_REF_NAME" "refs/tags/$TAG"
105
+ - name: Publish to PyPI
106
+ run: uv publish --trusted-publishing always
107
+ - name: Create GitHub release
108
+ env:
109
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110
+ TAG: ${{ steps.version.outputs.tag }}
111
+ PRERELEASE: ${{ steps.version.outputs.prerelease }}
112
+ run: |
113
+ args=("$TAG" dist/* --verify-tag --generate-notes --title "$TAG")
114
+ if [[ "$PRERELEASE" == "true" ]]; then
115
+ args+=(--prerelease)
116
+ else
117
+ args+=(--latest)
118
+ fi
119
+ gh release create "${args[@]}"
@@ -0,0 +1,23 @@
1
+ name: Test
2
+ on: [push, pull_request]
3
+ permissions:
4
+ contents: read
5
+ jobs:
6
+ test:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ os: [ubuntu-latest, windows-latest]
11
+ python: ["3.11", "3.14"]
12
+ runs-on: ${{ matrix.os }}
13
+ steps:
14
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
16
+ with:
17
+ version: "0.10.6"
18
+ python-version: ${{ matrix.python }}
19
+ - run: uv sync --locked --all-extras
20
+ - run: uv run pytest
21
+ - run: uv run ruff check .
22
+ - run: uv run ruff format --check .
23
+ - run: uv build
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ .coverage.*
8
+ htmlcov/
9
+ dist/
10
+ build/
11
+ *.egg-info/
@@ -0,0 +1,23 @@
1
+ Boost Software License - Version 1.0 - August 17th, 2003
2
+
3
+ Permission is hereby granted, free of charge, to any person or organization
4
+ obtaining a copy of the software and accompanying documentation covered by
5
+ this license (the "Software") to use, reproduce, display, distribute,
6
+ execute, and transmit the Software, and to prepare derivative works of the
7
+ Software, and to permit third-parties to whom the Software is furnished to
8
+ do so, all subject to the following:
9
+
10
+ The copyright notices in the Software and this entire statement, including
11
+ the above license grant, this restriction and the following disclaimer,
12
+ must be included in all copies of the Software, in whole or in part, and
13
+ all derivative works of the Software, unless such copies or derivative
14
+ works are solely in the form of machine-executable object code generated by
15
+ a source language processor.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
20
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
21
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
22
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: coverage-flashlight
3
+ Version: 1.0.0
4
+ Summary: Explore Python coverage and replay execution with a code flashlight.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: coverage>=7.10.6
9
+ Provides-Extra: pytest
10
+ Requires-Dist: pytest>=8; extra == 'pytest'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Coverage flashlight
14
+
15
+ See which Python code ran, then replay its execution as a flashlight revealing
16
+ source through fog. Reports are standalone HTML with source snapshots, shared
17
+ light/dark themes, and no server or external assets.
18
+
19
+ ## Install and run
20
+
21
+ Install in the environment containing the program and its dependencies:
22
+
23
+ ```sh
24
+ uv add --dev 'coverage-flashlight[pytest]'
25
+ # Or: python -m pip install 'coverage-flashlight[pytest]'
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```sh
31
+ # Static per-test line/branch coverage; pytest arguments follow --.
32
+ flashlight pytest -- tests/ -k database
33
+
34
+ # Ordered playback, including threads and newly launched Python processes.
35
+ flashlight pytest --trace -- tests/ -k database
36
+
37
+ # A script, with its arguments, executed once.
38
+ flashlight run -- scripts/example.py --input sample.bin
39
+ flashlight run --trace -- scripts/example.py --input sample.bin
40
+
41
+ # Python modules work too, including other test runners.
42
+ flashlight run -m my_package --help
43
+ flashlight run --trace -m unittest discover
44
+
45
+ # Render existing coverage.py branch data without another run.
46
+ flashlight report
47
+ ```
48
+
49
+ Open **htmlcov/flashlight.html** for static coverage or
50
+ **htmlcov/execution.html** for playback. Use `--output path/to/report.html`
51
+ before `--` to choose another destination. `python -m coverage_flashlight`
52
+ is equivalent to `flashlight`.
53
+
54
+ Coverage and tracing are separate modes: a command never silently runs a script
55
+ twice. `--trace` selects execution recording instead of coverage.py. To create
56
+ both reports, run the two commands explicitly. The program's exit status is
57
+ preserved and failed runs still produce a report. Pytest mode continues recording
58
+ other cases after a failure and returns a nonzero status.
59
+
60
+ Pytest is an optional adapter. Static measurement uses coverage.py; ordered
61
+ playback uses Python trace events. Script/module execution requires no pytest
62
+ installation. Each pytest case runs in its own interpreter with its own context,
63
+ which also follows newly launched Python children. This includes fixture setup
64
+ and teardown. Isolation is slower than a normal suite run and can expose fixture
65
+ or order dependencies; continue running ordinary tests for correctness.
66
+
67
+ ## Source selection
68
+
69
+ Run from the target project's root. By default, the tool includes Python files
70
+ inside that root, excluding hidden directories, virtual environments, build
71
+ outputs, and node_modules. Narrow this with `--source src --source tests` or:
72
+
73
+ ```toml
74
+ [tool.coverage-flashlight]
75
+ source = ["src/my_package"]
76
+ trace-source = ["src/my_package", "tests"]
77
+ omit = ["*/generated/*"]
78
+ ```
79
+
80
+ Paths are project-relative files or directories, not import names. `trace-source`
81
+ defaults to `source`; absent both, the project root is used. `--source` overrides
82
+ the configured selection for the current command. Keep source files inside the
83
+ project root. Selected but unexecuted files remain visible in the reports.
84
+
85
+ Coverage mode writes fresh data to `.coverage` after collection, replacing its
86
+ previous contents. Use `--data-file path` to keep a separate database. It enables
87
+ branch measurement and subprocess/`os._exit()` coverage itself; no coverage.py
88
+ configuration is required. Existing report exclusion rules are respected when
89
+ rendering. `flashlight report` can also render a combined database collected by
90
+ coverage.py or pytest-cov, preserving its context names.
91
+
92
+ Raw recordings and manifests are kept beside the report under `runs/<run-id>/`.
93
+ The default trace cap is 100,000 events per process; increase it with
94
+ `--max-events 500000`. Capture caps, interrupted hooks, source I/O failures, and
95
+ missing process completion markers are explicitly reported.
96
+
97
+ ## Reading the reports
98
+
99
+ Static coverage distinguishes executed lines, branch gaps, execution in another
100
+ scope, and unexecuted statements. Select a line to inspect its contexts and missing
101
+ branch destinations. Coverage is evidence of execution, not proof of assertions.
102
+
103
+ Playback offers Play/Pause, stepping, timeline scrubbing, Next new line, a pytest
104
+ Test body shortcut, Whole run, and process/thread selection. Unrevealed ranges
105
+ collapse to grey `…` rows; Reveal source context expands them without marking
106
+ them visited. Selecting a module pauses playback; Follow execution restores
107
+ automatic navigation. The module map and source scroll inside the window.
108
+
109
+ The timeline records observed event order, not variable history or cross-thread
110
+ causality. Speed is events per second, not original elapsed time. Generator
111
+ suspension produces Python return/call events. Native code, generated code with
112
+ synthetic filenames, pre-existing processes, and threads created outside Python's
113
+ threading module are outside capture. A forcibly killed process can lose its
114
+ buffered tail. A skipped test can have no events. The source snapshot must remain
115
+ consistent across cases; changed source is rejected rather than misattributed.
116
+
117
+ Tracing changes timing and cannot share a trace hook with coverage.py or a
118
+ debugger. Pytest-xdist parallel execution is rejected: per-case subprocess
119
+ isolation already supplies attribution. Python children using `-S` or `-I`
120
+ can bypass the opt-in bootstrap. No startup hook is installed globally.
121
+
122
+ ## Development and releases
123
+
124
+ ```sh
125
+ uv sync --all-extras
126
+ uv run pytest
127
+ uv run ruff check .
128
+ uv run ruff format --check .
129
+ uv build
130
+ ```
131
+
132
+ Tests use disposable Python projects, real threads/subprocesses, failing and
133
+ skipped cases, arbitrary pytest layouts, and actual script/module entry points.
134
+ They require no IDA installation. HTML, CSS and the opt-in bootstrap ship in the
135
+ wheel and source distribution.
136
+
137
+ The manual `Release` GitHub Actions workflow supports `release-current`, `dev`,
138
+ `release-patch`, and `release-minor`. It tests, builds and checks the wheel, commits
139
+ an optional version bump, creates a tag, publishes with `uv publish`, and creates
140
+ a GitHub release. Versioning uses `uv version`; there is only one package version.
141
+
142
+ To enable publication, create the GitHub repository and configure a PyPI trusted
143
+ publisher for project **coverage-flashlight**, your repository owner/name,
144
+ workflow **release.yml**, and environment **pypi**. Create that GitHub environment
145
+ and allow the workflow to push release commits/tags. No PyPI API token is needed.
146
+ These setup steps do not publish anything until the workflow is dispatched.
147
+
148
+ References: [uv publishing](https://docs.astral.sh/uv/guides/package/),
149
+ [coverage.py subprocess measurement](https://coverage.readthedocs.io/en/latest/subprocess.html),
150
+ [Python tracing](https://docs.python.org/3/library/sys.html#sys.settrace).
@@ -0,0 +1,138 @@
1
+ # Coverage flashlight
2
+
3
+ See which Python code ran, then replay its execution as a flashlight revealing
4
+ source through fog. Reports are standalone HTML with source snapshots, shared
5
+ light/dark themes, and no server or external assets.
6
+
7
+ ## Install and run
8
+
9
+ Install in the environment containing the program and its dependencies:
10
+
11
+ ```sh
12
+ uv add --dev 'coverage-flashlight[pytest]'
13
+ # Or: python -m pip install 'coverage-flashlight[pytest]'
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```sh
19
+ # Static per-test line/branch coverage; pytest arguments follow --.
20
+ flashlight pytest -- tests/ -k database
21
+
22
+ # Ordered playback, including threads and newly launched Python processes.
23
+ flashlight pytest --trace -- tests/ -k database
24
+
25
+ # A script, with its arguments, executed once.
26
+ flashlight run -- scripts/example.py --input sample.bin
27
+ flashlight run --trace -- scripts/example.py --input sample.bin
28
+
29
+ # Python modules work too, including other test runners.
30
+ flashlight run -m my_package --help
31
+ flashlight run --trace -m unittest discover
32
+
33
+ # Render existing coverage.py branch data without another run.
34
+ flashlight report
35
+ ```
36
+
37
+ Open **htmlcov/flashlight.html** for static coverage or
38
+ **htmlcov/execution.html** for playback. Use `--output path/to/report.html`
39
+ before `--` to choose another destination. `python -m coverage_flashlight`
40
+ is equivalent to `flashlight`.
41
+
42
+ Coverage and tracing are separate modes: a command never silently runs a script
43
+ twice. `--trace` selects execution recording instead of coverage.py. To create
44
+ both reports, run the two commands explicitly. The program's exit status is
45
+ preserved and failed runs still produce a report. Pytest mode continues recording
46
+ other cases after a failure and returns a nonzero status.
47
+
48
+ Pytest is an optional adapter. Static measurement uses coverage.py; ordered
49
+ playback uses Python trace events. Script/module execution requires no pytest
50
+ installation. Each pytest case runs in its own interpreter with its own context,
51
+ which also follows newly launched Python children. This includes fixture setup
52
+ and teardown. Isolation is slower than a normal suite run and can expose fixture
53
+ or order dependencies; continue running ordinary tests for correctness.
54
+
55
+ ## Source selection
56
+
57
+ Run from the target project's root. By default, the tool includes Python files
58
+ inside that root, excluding hidden directories, virtual environments, build
59
+ outputs, and node_modules. Narrow this with `--source src --source tests` or:
60
+
61
+ ```toml
62
+ [tool.coverage-flashlight]
63
+ source = ["src/my_package"]
64
+ trace-source = ["src/my_package", "tests"]
65
+ omit = ["*/generated/*"]
66
+ ```
67
+
68
+ Paths are project-relative files or directories, not import names. `trace-source`
69
+ defaults to `source`; absent both, the project root is used. `--source` overrides
70
+ the configured selection for the current command. Keep source files inside the
71
+ project root. Selected but unexecuted files remain visible in the reports.
72
+
73
+ Coverage mode writes fresh data to `.coverage` after collection, replacing its
74
+ previous contents. Use `--data-file path` to keep a separate database. It enables
75
+ branch measurement and subprocess/`os._exit()` coverage itself; no coverage.py
76
+ configuration is required. Existing report exclusion rules are respected when
77
+ rendering. `flashlight report` can also render a combined database collected by
78
+ coverage.py or pytest-cov, preserving its context names.
79
+
80
+ Raw recordings and manifests are kept beside the report under `runs/<run-id>/`.
81
+ The default trace cap is 100,000 events per process; increase it with
82
+ `--max-events 500000`. Capture caps, interrupted hooks, source I/O failures, and
83
+ missing process completion markers are explicitly reported.
84
+
85
+ ## Reading the reports
86
+
87
+ Static coverage distinguishes executed lines, branch gaps, execution in another
88
+ scope, and unexecuted statements. Select a line to inspect its contexts and missing
89
+ branch destinations. Coverage is evidence of execution, not proof of assertions.
90
+
91
+ Playback offers Play/Pause, stepping, timeline scrubbing, Next new line, a pytest
92
+ Test body shortcut, Whole run, and process/thread selection. Unrevealed ranges
93
+ collapse to grey `…` rows; Reveal source context expands them without marking
94
+ them visited. Selecting a module pauses playback; Follow execution restores
95
+ automatic navigation. The module map and source scroll inside the window.
96
+
97
+ The timeline records observed event order, not variable history or cross-thread
98
+ causality. Speed is events per second, not original elapsed time. Generator
99
+ suspension produces Python return/call events. Native code, generated code with
100
+ synthetic filenames, pre-existing processes, and threads created outside Python's
101
+ threading module are outside capture. A forcibly killed process can lose its
102
+ buffered tail. A skipped test can have no events. The source snapshot must remain
103
+ consistent across cases; changed source is rejected rather than misattributed.
104
+
105
+ Tracing changes timing and cannot share a trace hook with coverage.py or a
106
+ debugger. Pytest-xdist parallel execution is rejected: per-case subprocess
107
+ isolation already supplies attribution. Python children using `-S` or `-I`
108
+ can bypass the opt-in bootstrap. No startup hook is installed globally.
109
+
110
+ ## Development and releases
111
+
112
+ ```sh
113
+ uv sync --all-extras
114
+ uv run pytest
115
+ uv run ruff check .
116
+ uv run ruff format --check .
117
+ uv build
118
+ ```
119
+
120
+ Tests use disposable Python projects, real threads/subprocesses, failing and
121
+ skipped cases, arbitrary pytest layouts, and actual script/module entry points.
122
+ They require no IDA installation. HTML, CSS and the opt-in bootstrap ship in the
123
+ wheel and source distribution.
124
+
125
+ The manual `Release` GitHub Actions workflow supports `release-current`, `dev`,
126
+ `release-patch`, and `release-minor`. It tests, builds and checks the wheel, commits
127
+ an optional version bump, creates a tag, publishes with `uv publish`, and creates
128
+ a GitHub release. Versioning uses `uv version`; there is only one package version.
129
+
130
+ To enable publication, create the GitHub repository and configure a PyPI trusted
131
+ publisher for project **coverage-flashlight**, your repository owner/name,
132
+ workflow **release.yml**, and environment **pypi**. Create that GitHub environment
133
+ and allow the workflow to push release commits/tags. No PyPI API token is needed.
134
+ These setup steps do not publish anything until the workflow is dispatched.
135
+
136
+ References: [uv publishing](https://docs.astral.sh/uv/guides/package/),
137
+ [coverage.py subprocess measurement](https://coverage.readthedocs.io/en/latest/subprocess.html),
138
+ [Python tracing](https://docs.python.org/3/library/sys.html#sys.settrace).
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "coverage-flashlight"
3
+ version = "1.0.0"
4
+ description = "Explore Python coverage and replay execution with a code flashlight."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = ["coverage>=7.10.6"]
10
+
11
+ [project.optional-dependencies]
12
+ pytest = ["pytest>=8"]
13
+
14
+ [project.scripts]
15
+ flashlight = "coverage_flashlight.cli:main"
16
+
17
+ [dependency-groups]
18
+ dev = ["pytest>=8", "ruff>=0.12.0"]
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/coverage_flashlight"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+
30
+ [tool.coverage-flashlight]
31
+ source = ["src/coverage_flashlight"]
32
+ trace-source = ["src/coverage_flashlight", "tests"]
@@ -0,0 +1 @@
1
+ """Coverage reports and opt-in ordered Python execution recording."""
@@ -0,0 +1,3 @@
1
+ from coverage_flashlight.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,20 @@
1
+ """Opt-in child-process bootstrap; used only by the execution flashlight runner."""
2
+
3
+ import importlib.machinery
4
+ import importlib.util
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Preserve the environment's own startup customization.
10
+ _here = Path(__file__).parent.resolve()
11
+ _existing = importlib.machinery.PathFinder.find_spec(
12
+ "sitecustomize", [path for path in sys.path if Path(path).resolve() != _here]
13
+ )
14
+ if _existing is not None and _existing.loader is not None:
15
+ _existing.loader.exec_module(importlib.util.module_from_spec(_existing))
16
+
17
+ if os.environ.get("COVERAGE_FLASHLIGHT_DIR"):
18
+ from coverage_flashlight.trace import install
19
+
20
+ install()
@@ -0,0 +1,107 @@
1
+ """The flashlight command; target arguments are passed through after --."""
2
+
3
+ import argparse
4
+ import subprocess
5
+ from importlib.metadata import version
6
+ from pathlib import Path
7
+
8
+ from coverage.exceptions import CoverageException
9
+
10
+ from coverage_flashlight.config import project_sources
11
+ from coverage_flashlight.runner import measure, render_coverage
12
+
13
+
14
+ def main(argv=None) -> int:
15
+ parser = argparse.ArgumentParser(
16
+ prog="flashlight",
17
+ description="Explore Python coverage or replay executed lines.",
18
+ )
19
+ parser.add_argument(
20
+ "--version", action="version", version=version("coverage-flashlight")
21
+ )
22
+ commands = parser.add_subparsers(dest="command", required=True)
23
+ for name, help_text in [
24
+ ("pytest", "Measure each selected pytest case"),
25
+ ("run", "Execute a Python script or module once"),
26
+ ]:
27
+ command = commands.add_parser(name, help=help_text)
28
+ command.add_argument(
29
+ "--trace",
30
+ action="store_true",
31
+ help="record ordered execution instead of line/branch coverage",
32
+ )
33
+ command.add_argument(
34
+ "--source",
35
+ action="append",
36
+ help="project-relative source file or directory; repeat for multiple paths",
37
+ )
38
+ command.add_argument(
39
+ "--output",
40
+ type=Path,
41
+ help="HTML report path (default: htmlcov/flashlight.html or execution.html)",
42
+ )
43
+ command.add_argument(
44
+ "--data-file",
45
+ type=Path,
46
+ default=Path(".coverage"),
47
+ help="coverage database to replace (coverage mode only)",
48
+ )
49
+ command.add_argument(
50
+ "--max-events",
51
+ type=int,
52
+ default=100_000,
53
+ help="maximum trace events per process",
54
+ )
55
+ if name == "run":
56
+ command.add_argument(
57
+ "-m",
58
+ "--module",
59
+ action="store_true",
60
+ help="run the target as a Python module",
61
+ )
62
+ command.add_argument(
63
+ "arguments", nargs=argparse.REMAINDER, help="target arguments, after --"
64
+ )
65
+ report = commands.add_parser(
66
+ "report", help="Render existing branch coverage without executing code"
67
+ )
68
+ report.add_argument("--data-file", type=Path, default=Path(".coverage"))
69
+ report.add_argument("--output", type=Path, default=Path("htmlcov/flashlight.html"))
70
+ args = parser.parse_args(argv)
71
+ try:
72
+ if args.command == "report":
73
+ if not args.data_file.is_file():
74
+ parser.error(f"Coverage database does not exist: {args.data_file}")
75
+ render_coverage(args.data_file, args.output, Path.cwd())
76
+ return 0
77
+ arguments = (
78
+ args.arguments[1:] if args.arguments[:1] == ["--"] else args.arguments
79
+ )
80
+ if args.command == "run" and not arguments:
81
+ parser.error("run requires a script path or module name")
82
+ if args.max_events <= 0:
83
+ parser.error("--max-events must be positive")
84
+ sources = project_sources(Path.cwd(), trace=args.trace, override=args.source)
85
+ return measure(
86
+ arguments,
87
+ args.output
88
+ or Path(
89
+ "htmlcov/execution.html" if args.trace else "htmlcov/flashlight.html"
90
+ ),
91
+ sources,
92
+ trace=args.trace,
93
+ pytest=args.command == "pytest",
94
+ module=getattr(args, "module", False),
95
+ max_events=args.max_events,
96
+ data_file=args.data_file,
97
+ )
98
+ except subprocess.CalledProcessError as error:
99
+ return error.returncode
100
+ except (ValueError, OSError, CoverageException) as error:
101
+ parser.error(str(error))
102
+ except KeyboardInterrupt:
103
+ return 130
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())