improcv 0.1.0a1__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.
Files changed (59) hide show
  1. improcv-0.1.0a1/.github/workflows/release.yml +265 -0
  2. improcv-0.1.0a1/.github/workflows/test.yml +240 -0
  3. improcv-0.1.0a1/.gitignore +16 -0
  4. improcv-0.1.0a1/CHANGELOG.md +251 -0
  5. improcv-0.1.0a1/LICENSE +21 -0
  6. improcv-0.1.0a1/PKG-INFO +277 -0
  7. improcv-0.1.0a1/README.md +243 -0
  8. improcv-0.1.0a1/ROADMAP.md +53 -0
  9. improcv-0.1.0a1/pyproject.toml +83 -0
  10. improcv-0.1.0a1/scripts/verify_release_version.py +43 -0
  11. improcv-0.1.0a1/src/improcv/__init__.py +264 -0
  12. improcv-0.1.0a1/src/improcv/_compat/__init__.py +0 -0
  13. improcv-0.1.0a1/src/improcv/_compat/opencv.py +58 -0
  14. improcv-0.1.0a1/src/improcv/_validation.py +363 -0
  15. improcv-0.1.0a1/src/improcv/analysis.py +524 -0
  16. improcv-0.1.0a1/src/improcv/barcode.py +207 -0
  17. improcv-0.1.0a1/src/improcv/color.py +124 -0
  18. improcv-0.1.0a1/src/improcv/contours.py +355 -0
  19. improcv-0.1.0a1/src/improcv/detectors.py +471 -0
  20. improcv-0.1.0a1/src/improcv/drawing.py +451 -0
  21. improcv-0.1.0a1/src/improcv/edges.py +210 -0
  22. improcv-0.1.0a1/src/improcv/features.py +779 -0
  23. improcv-0.1.0a1/src/improcv/filters.py +276 -0
  24. improcv-0.1.0a1/src/improcv/hough.py +689 -0
  25. improcv-0.1.0a1/src/improcv/morphology.py +269 -0
  26. improcv-0.1.0a1/src/improcv/pixels.py +281 -0
  27. improcv-0.1.0a1/src/improcv/py.typed +0 -0
  28. improcv-0.1.0a1/src/improcv/qrcode.py +270 -0
  29. improcv-0.1.0a1/src/improcv/regions.py +474 -0
  30. improcv-0.1.0a1/src/improcv/restoration.py +129 -0
  31. improcv-0.1.0a1/src/improcv/segmentation.py +193 -0
  32. improcv-0.1.0a1/src/improcv/transforms.py +632 -0
  33. improcv-0.1.0a1/src/improcv/types.py +49 -0
  34. improcv-0.1.0a1/src/improcv/visualization/__init__.py +25 -0
  35. improcv-0.1.0a1/src/improcv/visualization/image.py +196 -0
  36. improcv-0.1.0a1/tests/conftest.py +17 -0
  37. improcv-0.1.0a1/tests/test_analysis.py +630 -0
  38. improcv-0.1.0a1/tests/test_barcode.py +458 -0
  39. improcv-0.1.0a1/tests/test_color.py +125 -0
  40. improcv-0.1.0a1/tests/test_compat_opencv.py +72 -0
  41. improcv-0.1.0a1/tests/test_contours.py +510 -0
  42. improcv-0.1.0a1/tests/test_detectors.py +681 -0
  43. improcv-0.1.0a1/tests/test_drawing.py +651 -0
  44. improcv-0.1.0a1/tests/test_edges.py +177 -0
  45. improcv-0.1.0a1/tests/test_features.py +1491 -0
  46. improcv-0.1.0a1/tests/test_filters.py +309 -0
  47. improcv-0.1.0a1/tests/test_hough.py +829 -0
  48. improcv-0.1.0a1/tests/test_morphology.py +235 -0
  49. improcv-0.1.0a1/tests/test_package_import.py +60 -0
  50. improcv-0.1.0a1/tests/test_pixels.py +399 -0
  51. improcv-0.1.0a1/tests/test_qrcode.py +456 -0
  52. improcv-0.1.0a1/tests/test_regions.py +555 -0
  53. improcv-0.1.0a1/tests/test_restoration.py +263 -0
  54. improcv-0.1.0a1/tests/test_segmentation.py +261 -0
  55. improcv-0.1.0a1/tests/test_transforms.py +799 -0
  56. improcv-0.1.0a1/tests/test_types.py +11 -0
  57. improcv-0.1.0a1/tests/test_validation.py +547 -0
  58. improcv-0.1.0a1/tests/test_visualization.py +279 -0
  59. improcv-0.1.0a1/uv.lock +825 -0
@@ -0,0 +1,265 @@
1
+ name: release
2
+
3
+ # Triggered only by pushing a tag matching v* (e.g. v0.1.0a1). test.yml also
4
+ # reacts to the same tag push independently -- this workflow does not rely on
5
+ # that parallel run to gate publishing; it re-runs the full test matrix itself
6
+ # (via a reusable workflow call to test.yml) and adds its own release-specific
7
+ # checks (tag/version/HEAD verification, exact-artifact build+install+smoke
8
+ # test) before anything is uploaded or published.
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ # Deny-by-default at the workflow level; each job below grants only the
15
+ # scope it actually needs. In particular, only the two publish jobs get
16
+ # `id-token: write` (required for PyPI Trusted Publishing) -- the job that
17
+ # builds and verifies the artifact never receives OIDC credentials.
18
+ permissions: {}
19
+
20
+ # Two tags pushed close together must not publish concurrently, and an
21
+ # in-progress release must never be cancelled by a later trigger (which could
22
+ # leave TestPyPI and PyPI in different states, or abort mid-publish).
23
+ #
24
+ # `queue: max` (GitHub Actions concurrency, shipped 2026-05-07) lets up to 100
25
+ # runs wait in this group instead of the `queue: single` default, which only
26
+ # holds one pending run and cancels/replaces it if another arrives -- so a
27
+ # third tag pushed while a second is still queued behind a running first
28
+ # would otherwise never run at all. Verified directly against GitHub's own
29
+ # "Using concurrency" docs (fetched 2026-07-23): `queue` accepts `single`
30
+ # (default) or `max` (up to 100 pending). NOTE: `actionlint` v1.7.12 (current
31
+ # latest release as of this writing) does not yet recognize the `queue` key
32
+ # and will report `unexpected key "queue" for "concurrency" section` --
33
+ # this is a known, tracked actionlint gap (rhysd/actionlint#654, #657, #661,
34
+ # all open), not evidence that the key is invalid. Confirmed via GitHub's
35
+ # current documentation, which is authoritative here over a linter that
36
+ # hasn't caught up yet.
37
+ concurrency:
38
+ group: release
39
+ queue: max
40
+ cancel-in-progress: false
41
+
42
+ jobs:
43
+ check-tag:
44
+ name: Verify tag matches origin/main HEAD and pyproject.toml version
45
+ runs-on: ubuntu-latest
46
+ permissions:
47
+ contents: read
48
+ steps:
49
+ - name: Checkout full history
50
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
51
+ with:
52
+ fetch-depth: 0
53
+
54
+ - name: Set up Python
55
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
56
+ with:
57
+ python-version: "3.11"
58
+
59
+ - name: Verify the tag points at origin/main's current HEAD
60
+ # Comparing the tag's version to pyproject.toml alone is not enough:
61
+ # 0.1.0a1 has been the version string for many commits, so an old
62
+ # commit would also pass a version-only check. Requiring an exact
63
+ # match against origin/main's current HEAD (not "is an ancestor of
64
+ # main") ensures the release is always cut from the actual latest
65
+ # reviewed state of the default branch.
66
+ run: |
67
+ git fetch origin main --quiet
68
+ tag_commit="$(git rev-parse "${GITHUB_SHA}^{commit}")"
69
+ main_commit="$(git rev-parse origin/main)"
70
+ if [ "$tag_commit" != "$main_commit" ]; then
71
+ echo "::error::Tag commit ($tag_commit) does not match origin/main HEAD ($main_commit). Refusing to release from a commit that is not main's current HEAD."
72
+ exit 1
73
+ fi
74
+ echo "Tag commit matches origin/main HEAD: $main_commit"
75
+
76
+ - name: Verify the tag matches pyproject.toml's version exactly (PEP 440)
77
+ run: python3 scripts/verify_release_version.py
78
+
79
+ test-matrix:
80
+ name: Full Python/OpenCV test matrix (reused from test.yml)
81
+ needs: check-tag
82
+ permissions:
83
+ contents: read
84
+ uses: ./.github/workflows/test.yml
85
+
86
+ build-and-verify:
87
+ name: Build and verify the exact artifacts to be published
88
+ needs: test-matrix
89
+ runs-on: ubuntu-latest
90
+ # No id-token permission here on purpose: this job builds and smoke-tests
91
+ # the artifact but never publishes, so it never needs OIDC credentials.
92
+ permissions:
93
+ contents: read
94
+ env:
95
+ MPLBACKEND: "Agg"
96
+ steps:
97
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
98
+
99
+ - name: Install uv
100
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
101
+ with:
102
+ version: "0.11.31"
103
+ python-version: "3.11"
104
+
105
+ - name: Build sdist and wheel
106
+ run: uv build
107
+
108
+ - name: Verify dist/ contains exactly one wheel and one sdist
109
+ run: |
110
+ shopt -s nullglob
111
+ wheels=(dist/*.whl)
112
+ sdists=(dist/*.tar.gz)
113
+ echo "wheels: ${#wheels[@]}, sdists: ${#sdists[@]}"
114
+ ls -la dist/
115
+ if [ "${#wheels[@]}" -ne 1 ] || [ "${#sdists[@]}" -ne 1 ]; then
116
+ echo "::error::Expected exactly one wheel and one sdist in dist/, found ${#wheels[@]} wheel(s) and ${#sdists[@]} sdist(s)."
117
+ exit 1
118
+ fi
119
+
120
+ - name: Check package metadata and description rendering (pinned Twine)
121
+ run: uvx "twine==6.2.0" check dist/*
122
+
123
+ - name: Install the wheel into a clean environment (no extras) and smoke-test
124
+ # uv venv/uv pip --python pin the interpreter explicitly: setup-uv's
125
+ # own python-version input sets UV_PYTHON for uv's *own* commands,
126
+ # but does not guarantee a plain `python`/`python3` on PATH resolves
127
+ # to that same interpreter.
128
+ run: |
129
+ uv venv --python 3.11 /tmp/smoke-wheel
130
+ uv pip install --python /tmp/smoke-wheel/bin/python dist/*.whl "opencv-python-headless>=4.9,<6"
131
+ /tmp/smoke-wheel/bin/python -c "
132
+ import os
133
+ import sys
134
+
135
+ import numpy as np
136
+ import improcv as im
137
+
138
+ expected_version = os.environ['GITHUB_REF_NAME']
139
+ if expected_version.startswith('v'):
140
+ expected_version = expected_version[1:]
141
+ assert im.__version__ == expected_version, (im.__version__, expected_version)
142
+
143
+ image = np.zeros((20, 20, 3), dtype=np.uint8)
144
+ result = im.resize(image, width=10)
145
+ assert result.shape == (10, 10, 3), result.shape
146
+ assert 'matplotlib' not in sys.modules, 'import improcv must never import matplotlib'
147
+ print('smoke test OK (wheel, no extras):', im.__version__, result.shape)
148
+ "
149
+
150
+ - name: Verify improcv.visualization fails clearly without the viz extra (wheel)
151
+ run: |
152
+ /tmp/smoke-wheel/bin/python -c "
153
+ try:
154
+ import improcv.visualization
155
+ raise SystemExit('expected ImportError, none was raised')
156
+ except ImportError as exc:
157
+ assert 'improcv[viz]' in str(exc), str(exc)
158
+ print('smoke test OK: clear ImportError without the viz extra (wheel)')
159
+ "
160
+
161
+ - name: Install the wheel with the viz extra into a separate clean environment and smoke-test
162
+ run: |
163
+ uv venv --python 3.11 /tmp/smoke-wheel-viz
164
+ uv pip install --python /tmp/smoke-wheel-viz/bin/python "$(echo dist/*.whl)[viz]" "opencv-python-headless>=4.9,<6"
165
+ /tmp/smoke-wheel-viz/bin/python -c "
166
+ import os
167
+
168
+ import numpy as np
169
+ import improcv as im
170
+ import improcv.visualization as viz
171
+
172
+ expected_version = os.environ['GITHUB_REF_NAME']
173
+ if expected_version.startswith('v'):
174
+ expected_version = expected_version[1:]
175
+ assert im.__version__ == expected_version, (im.__version__, expected_version)
176
+
177
+ image = np.zeros((20, 20, 3), dtype=np.uint8)
178
+ viz.show_image(image)
179
+ viz.plot_histogram(image)
180
+ print('smoke test OK: show_image and plot_histogram both ran under Agg (wheel+viz)', im.__version__)
181
+ "
182
+
183
+ - name: Install the sdist into a separate clean environment and smoke-test
184
+ run: |
185
+ uv venv --python 3.11 /tmp/smoke-sdist
186
+ uv pip install --python /tmp/smoke-sdist/bin/python dist/*.tar.gz "opencv-python-headless>=4.9,<6"
187
+ /tmp/smoke-sdist/bin/python -c "
188
+ import os
189
+
190
+ import numpy as np
191
+ import improcv as im
192
+
193
+ expected_version = os.environ['GITHUB_REF_NAME']
194
+ if expected_version.startswith('v'):
195
+ expected_version = expected_version[1:]
196
+ assert im.__version__ == expected_version, (im.__version__, expected_version)
197
+
198
+ image = np.zeros((20, 20, 3), dtype=np.uint8)
199
+ result = im.resize(image, width=10)
200
+ assert result.shape == (10, 10, 3), result.shape
201
+ print('smoke test OK (sdist):', im.__version__, result.shape)
202
+ "
203
+
204
+ - name: Upload the verified dist/ as the release artifact
205
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
206
+ with:
207
+ name: release-dist
208
+ path: dist/
209
+ # Short, but long enough to survive the manual TestPyPI
210
+ # verification + `pypi` environment approval step in between
211
+ # publish-testpypi and publish-pypi -- 1 day was too tight if that
212
+ # approval doesn't happen immediately.
213
+ retention-days: 7
214
+ if-no-files-found: error
215
+
216
+ publish-testpypi:
217
+ name: Publish to TestPyPI
218
+ needs: build-and-verify
219
+ runs-on: ubuntu-latest
220
+ environment: testpypi
221
+ permissions:
222
+ id-token: write
223
+ steps:
224
+ - name: Download the verified release artifact
225
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
226
+ with:
227
+ name: release-dist
228
+ path: dist/
229
+
230
+ - name: Publish to TestPyPI
231
+ uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
232
+ with:
233
+ repository-url: https://test.pypi.org/legacy/
234
+ packages-dir: dist/
235
+ # TestPyPI-only: lets a re-run after a partial upload or a
236
+ # transient TestPyPI outage succeed safely instead of erroring on
237
+ # a version that's already (partially) there. Deliberately not
238
+ # set on publish-pypi below -- a duplicate there should still
239
+ # fail loudly.
240
+ skip-existing: true
241
+
242
+ publish-pypi:
243
+ name: Publish to PyPI
244
+ # Order is TestPyPI -> PyPI: the real-PyPI job only starts after the
245
+ # TestPyPI publish (the dry run of the mechanism itself) has succeeded.
246
+ needs: publish-testpypi
247
+ runs-on: ubuntu-latest
248
+ # Production PyPI stays behind the `pypi` GitHub Environment, which
249
+ # should be configured with a required reviewer (see the design doc's
250
+ # manual-steps section) -- this job pauses for manual approval before
251
+ # running.
252
+ environment: pypi
253
+ permissions:
254
+ id-token: write
255
+ steps:
256
+ - name: Download the verified release artifact
257
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
258
+ with:
259
+ name: release-dist
260
+ path: dist/
261
+
262
+ - name: Publish to PyPI
263
+ uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
264
+ with:
265
+ packages-dir: dist/
@@ -0,0 +1,240 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: ["main"]
9
+ # Lets release.yml invoke this workflow's own `test`/`lowest-deps` matrix as
10
+ # part of the release pipeline, so a release can't be published without the
11
+ # full test matrix passing inside that same run -- rather than only relying
12
+ # on this workflow's own independent, parallel run against the same tag
13
+ # push, whose failure would not block publishing.
14
+ workflow_call:
15
+
16
+ # Deny-by-default beyond read access to repo contents: standalone runs of
17
+ # this workflow (push/pull_request) only ever need to check out the code,
18
+ # and the reusable-workflow-call path from release.yml explicitly caps this
19
+ # further via its own `permissions: contents: read` on the calling job.
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ test:
25
+ runs-on: ubuntu-latest
26
+ strategy:
27
+ matrix:
28
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
29
+ opencv: ["4", "5"]
30
+ include:
31
+ - opencv: "4"
32
+ opencv-spec: "opencv-python-headless>=4.9,<5"
33
+ - opencv: "5"
34
+ opencv-spec: "opencv-python-headless>=5,<6"
35
+ env:
36
+ OMP_NUM_THREADS: "1"
37
+ OPENBLAS_NUM_THREADS: "1"
38
+ MKL_NUM_THREADS: "1"
39
+ MPLBACKEND: "Agg"
40
+ steps:
41
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
45
+ with:
46
+ version: "0.11.31"
47
+ python-version: ${{ matrix.python-version }}
48
+
49
+ - name: Install dependencies
50
+ run: uv sync --locked --all-groups
51
+
52
+ - name: Install pinned OpenCV major version (${{ matrix.opencv }}.x)
53
+ run: uv pip install "${{ matrix.opencv-spec }}"
54
+
55
+ # --no-sync everywhere below: plain `uv run` re-syncs the environment
56
+ # against pyproject/uv.lock first, which would silently reinstall the
57
+ # locked (latest, 5.x) opencv-python-headless and undo the pinned
58
+ # override above — verified this actually happens before adding
59
+ # --no-sync, it's not a hypothetical concern.
60
+ - name: Lint
61
+ run: uv run --no-sync ruff check .
62
+
63
+ - name: Check formatting
64
+ run: uv run --no-sync ruff format --check .
65
+
66
+ - name: Type check
67
+ run: uv run --no-sync pyright
68
+
69
+ - name: Test
70
+ run: uv run --no-sync pytest
71
+
72
+ # Representative, non-Linux platform coverage (brief §8 requires "at least
73
+ # representative Windows and macOS configurations") -- deliberately just
74
+ # one job per platform, not the full Python x OpenCV matrix duplicated
75
+ # twice more: the goal is catching platform-specific problems (path
76
+ # handling, OpenCV wheel behavior, thread-library differences), not
77
+ # tripling CI runtime. Each of the two platforms takes one end of the
78
+ # supported Python range and the opposite OpenCV major line from the
79
+ # other, so between them all four "extremes" (oldest/newest Python,
80
+ # both OpenCV lines) get at least one non-Linux run. Verified directly
81
+ # (PyPI file listing for the latest opencv-python-headless release on
82
+ # both the 4.x and 5.x lines) that both ship `cp37-abi3` wheels for
83
+ # `win_amd64` and both `macosx_*_arm64`/`macosx_*_x86_64` -- so wheel
84
+ # availability isn't a constraint on either platform or Python choice.
85
+ # No lint/type-check here: both are platform-independent by nature and
86
+ # already covered above -- only `pytest` runs, which is the check that
87
+ # can actually differ by platform.
88
+ test-windows:
89
+ name: Representative test (windows-latest, Python 3.11, OpenCV 4.x)
90
+ runs-on: windows-latest
91
+ env:
92
+ MPLBACKEND: "Agg"
93
+ steps:
94
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
95
+
96
+ - name: Install uv
97
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
98
+ with:
99
+ version: "0.11.31"
100
+ python-version: "3.11"
101
+
102
+ - name: Install dependencies
103
+ run: uv sync --locked --all-groups
104
+
105
+ - name: Install pinned OpenCV major version (4.x)
106
+ run: uv pip install "opencv-python-headless>=4.9,<5"
107
+
108
+ - name: Test
109
+ run: uv run --no-sync pytest
110
+
111
+ test-macos:
112
+ name: Representative test (macos-latest, Python 3.14, OpenCV 5.x)
113
+ runs-on: macos-latest
114
+ env:
115
+ MPLBACKEND: "Agg"
116
+ steps:
117
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
118
+
119
+ - name: Install uv
120
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
121
+ with:
122
+ version: "0.11.31"
123
+ python-version: "3.14"
124
+
125
+ - name: Install dependencies
126
+ run: uv sync --locked --all-groups
127
+
128
+ - name: Install pinned OpenCV major version (5.x)
129
+ run: uv pip install "opencv-python-headless>=5,<6"
130
+
131
+ - name: Test
132
+ run: uv run --no-sync pytest
133
+
134
+ lowest-deps:
135
+ # Everything above always resolves via uv.lock, which picks the
136
+ # newest version satisfying each constraint — so numpy>=1.24,
137
+ # opencv-python>=4.9, and matplotlib>=3.8 in pyproject.toml were never
138
+ # actually tested at those floors. This job pins the exact declared
139
+ # minimums (on the oldest supported Python, since numpy 1.24 has no
140
+ # wheels for 3.12+) to catch a floor that's declared but doesn't
141
+ # actually work.
142
+ runs-on: ubuntu-latest
143
+ env:
144
+ MPLBACKEND: "Agg"
145
+ steps:
146
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
147
+
148
+ - name: Install uv
149
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
150
+ with:
151
+ version: "0.11.31"
152
+ python-version: "3.11"
153
+
154
+ - name: Create a venv and install the declared minimum versions
155
+ # Exact floors, not `.*` wildcards: `numpy==1.24.*`/`matplotlib==3.8.*`
156
+ # resolve to the newest patch on that branch, not the actual declared
157
+ # minimum -- verified this job was never really testing 1.24.0/3.8.0.
158
+ # pytest is pinned to the version currently locked in uv.lock so this
159
+ # release-critical job doesn't depend on an arbitrary future pytest.
160
+ run: |
161
+ uv venv --python 3.11
162
+ uv pip install "numpy==1.24.0" "opencv-python-headless==4.9.0.80" "matplotlib==3.8.0" "pytest==9.1.1"
163
+ uv pip install -e . --no-deps
164
+
165
+ - name: Test against the lowest declared dependency versions
166
+ run: uv run --no-sync pytest
167
+
168
+ build:
169
+ runs-on: ubuntu-latest
170
+ steps:
171
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
172
+
173
+ - name: Install uv
174
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
175
+ with:
176
+ version: "0.11.31"
177
+ python-version: "3.11"
178
+
179
+ - name: Build sdist and wheel
180
+ run: uv build
181
+
182
+ - name: Check package metadata and description rendering
183
+ run: uvx "twine==6.2.0" check dist/*
184
+
185
+ - name: Install the built wheel into a clean environment (no viz extra)
186
+ # uv venv/uv pip --python pin the interpreter explicitly: setup-uv's
187
+ # own python-version input sets UV_PYTHON for uv's *own* commands,
188
+ # but does not guarantee a plain `python`/`python3` on PATH resolves
189
+ # to that same interpreter.
190
+ run: |
191
+ uv venv --python 3.11 /tmp/smoke-env
192
+ uv pip install --python /tmp/smoke-env/bin/python dist/*.whl "opencv-python-headless>=4.9,<6"
193
+
194
+ - name: Smoke-test the installed wheel (not the source tree)
195
+ run: |
196
+ cd /tmp
197
+ /tmp/smoke-env/bin/python -c "
198
+ import sys
199
+
200
+ import numpy as np
201
+ import improcv as im
202
+
203
+ image = np.zeros((20, 20, 3), dtype=np.uint8)
204
+ result = im.resize(image, width=10)
205
+ assert result.shape == (10, 10, 3), result.shape
206
+ assert 'matplotlib' not in sys.modules, 'import improcv must never import matplotlib'
207
+ print('smoke test OK:', im.__name__, result.shape)
208
+ "
209
+
210
+ - name: Smoke-test that improcv.visualization fails clearly without the viz extra
211
+ run: |
212
+ cd /tmp
213
+ /tmp/smoke-env/bin/python -c "
214
+ try:
215
+ import improcv.visualization
216
+ raise SystemExit('expected ImportError, none was raised')
217
+ except ImportError as exc:
218
+ assert 'improcv[viz]' in str(exc), str(exc)
219
+ print('smoke test OK: clear ImportError without the viz extra')
220
+ "
221
+
222
+ - name: Install the built wheel with the viz extra into a separate clean environment
223
+ run: |
224
+ uv venv --python 3.11 /tmp/smoke-env-viz
225
+ uv pip install --python /tmp/smoke-env-viz/bin/python "$(echo dist/*.whl)[viz]" "opencv-python-headless>=4.9,<6"
226
+
227
+ - name: Smoke-test improcv.visualization with the viz extra installed
228
+ env:
229
+ MPLBACKEND: "Agg"
230
+ run: |
231
+ cd /tmp
232
+ /tmp/smoke-env-viz/bin/python -c "
233
+ import numpy as np
234
+ import improcv.visualization as viz
235
+
236
+ image = np.zeros((20, 20, 3), dtype=np.uint8)
237
+ ax = viz.show_image(image)
238
+ ax2 = viz.plot_histogram(image)
239
+ print('smoke test OK: show_image and plot_histogram both ran under Agg')
240
+ "
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+
10
+ # Private planning/working docs — internal agreements and process notes,
11
+ # not meant to be visible in the public repo. Public docs (once we have
12
+ # them) live elsewhere under docs/, so only this subfolder is ignored.
13
+ CLAUDE.md
14
+ IMPROCV_PROJECT_BRIEF.md
15
+ .claude/
16
+ docs/superpowers/