lib-detect-testenv 3.0.4__tar.gz → 3.0.5__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 (43) hide show
  1. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.github/workflows/default_cicd_public.yml +42 -6
  2. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.gitignore +8 -5
  3. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/CHANGELOG.md +13 -0
  4. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/CLAUDE.md +5 -85
  5. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/Makefile +38 -4
  6. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/PKG-INFO +49 -17
  7. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/README.md +38 -8
  8. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/pyproject.toml +14 -11
  9. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/__init__conf__.py +1 -1
  10. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/safe_console.py +9 -4
  11. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_safe_console.py +25 -0
  12. lib_detect_testenv-3.0.5/uv.lock +1776 -0
  13. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.devcontainer/devcontainer.json +0 -0
  14. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.devcontainer/settings.json +0 -0
  15. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.env.example +0 -0
  16. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.github/actions/extract-metadata/action.yml +0 -0
  17. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.github/dependabot.yml +0 -0
  18. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.github/workflows/codeql.yml +0 -0
  19. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.github/workflows/default_release_public.yml +0 -0
  20. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.qlty/qlty.toml +0 -0
  21. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/.snyk +0 -0
  22. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/CONTRIBUTING.md +0 -0
  23. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/DEVELOPMENT.md +0 -0
  24. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/INSTALL.md +0 -0
  25. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/LICENSE +0 -0
  26. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/codecov.yml +0 -0
  27. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/concept.md +0 -0
  28. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/docs/systemdesign/module_reference.md +0 -0
  29. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/notebooks/Quickstart.ipynb +0 -0
  30. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/reset_git_history.sh +0 -0
  31. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/__init__.py +0 -0
  32. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/__main__.py +0 -0
  33. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/behaviors.py +0 -0
  34. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/cli.py +0 -0
  35. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/lib_detect_testenv.py +0 -0
  36. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/py.typed +0 -0
  37. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/src/lib_detect_testenv/typed_click.py +0 -0
  38. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/conftest.py +0 -0
  39. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_behaviors.py +0 -0
  40. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_cli.py +0 -0
  41. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_lib_detect_testenv.py +0 -0
  42. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_metadata.py +0 -0
  43. {lib_detect_testenv-3.0.4 → lib_detect_testenv-3.0.5}/tests/test_module_entry.py +0 -0
@@ -124,9 +124,36 @@ jobs:
124
124
  print("No CI system dependencies configured for macOS.")
125
125
 
126
126
  - name: Install dev deps
127
- shell: bash
127
+ shell: python
128
128
  run: |
129
- uv pip install -e .[dev] --system
129
+ import os, subprocess, tempfile
130
+ from pathlib import Path
131
+ try:
132
+ import tomllib
133
+ except ModuleNotFoundError:
134
+ import pip._vendor.tomli as tomllib
135
+
136
+ # Push and pull_request runs install exactly the tree uv.lock pins - versions
137
+ # plus file hashes, which uv verifies - so CI tests what the lock ships and no
138
+ # release published upstream since the last lock refresh can enter the build.
139
+ # The nightly schedule run resolves afresh instead (still inside the project's
140
+ # [tool.uv] exclude-newer cooldown, which uv honours here): it is the canary
141
+ # that reports upstream breakage before a lock refresh adopts it. A project
142
+ # without a uv.lock keeps the unlocked install.
143
+ with open("pyproject.toml", "rb") as f:
144
+ data = tomllib.load(f)
145
+ # Asking uv for an extra the project does not declare is a hard error.
146
+ extra = ["--extra", "dev"] if "dev" in data.get("project", {}).get("optional-dependencies", {}) else []
147
+ target = ["--system"] # setup-python's interpreter; the job runs no venv
148
+ locked = Path("uv.lock").is_file() and os.environ["GITHUB_EVENT_NAME"] != "schedule"
149
+ if locked:
150
+ req = Path(tempfile.mkdtemp()) / "locked-requirements.txt"
151
+ # --locked fails when pyproject.toml changed without a lock refresh.
152
+ subprocess.check_call(["uv", "export", "--locked", "--no-emit-project", *extra, "-o", str(req), "--quiet"])
153
+ subprocess.check_call(["uv", "pip", "install", *target, "-r", str(req)])
154
+ subprocess.check_call(["uv", "pip", "install", *target, "--no-deps", "-e", "."])
155
+ else:
156
+ subprocess.check_call(["uv", "pip", "install", *target, "-e", ".[dev]" if extra else "."])
130
157
 
131
158
  - name: Install CI system dependencies (Windows/choco)
132
159
  if: runner.os == 'Windows'
@@ -236,7 +263,7 @@ jobs:
236
263
  continue-on-error: true
237
264
  shell: python
238
265
  run: |
239
- import subprocess, sys, tempfile
266
+ import os, subprocess, sys, tempfile
240
267
  import rtoml
241
268
  from pathlib import Path
242
269
 
@@ -263,9 +290,18 @@ jobs:
263
290
 
264
291
  tmp = Path(tempfile.mkdtemp())
265
292
  req = tmp / "compiled-requirements.txt"
266
- subprocess.check_call(
267
- ["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
268
- )
293
+ # With a uv.lock the install step put exactly the locked tree on the
294
+ # runner, so audit that tree; the schedule canary and lock-less
295
+ # projects resolve pyproject.toml afresh, as their install did.
296
+ if Path("uv.lock").is_file() and os.environ["GITHUB_EVENT_NAME"] != "schedule":
297
+ subprocess.check_call(
298
+ ["uv", "export", "--locked", "--no-emit-project", "--no-hashes", *selector,
299
+ "-o", str(req), "--quiet"]
300
+ )
301
+ else:
302
+ subprocess.check_call(
303
+ ["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
304
+ )
269
305
 
270
306
  # Drop direct URL references ("pkg @ git+https://...") before auditing.
271
307
  # pip-audit cannot audit those (they are not on PyPI) but, given a
@@ -47,7 +47,8 @@ tmp_*
47
47
  # *.py[cod]: catches .pyc, .pyo, .pyd
48
48
  __pycache__/
49
49
  *.py[cod]
50
- *$py.class # other compiled artifacts
50
+ # other compiled artifacts
51
+ *$py.class
51
52
 
52
53
  # pytest, coverage, MyPy, Hypothesis
53
54
  .pytest_cache/
@@ -73,12 +74,15 @@ coverage.xml
73
74
  # source/package builds
74
75
  build/
75
76
  dist/
76
- *.egg-info/ # metadata for pip installs
77
+ # metadata for pip installs
78
+ *.egg-info/
77
79
  *.egg
78
80
  *.whl
79
81
  pip-wheel-metadata/
80
- *.manifest # PyInstaller metadata
81
- *.spec # PyInstaller spec files
82
+ # PyInstaller metadata
83
+ *.manifest
84
+ # PyInstaller spec files
85
+ *.spec
82
86
 
83
87
  # Eggs, parts, wheels, sdist, etc.
84
88
  develop-eggs/
@@ -178,7 +182,6 @@ result
178
182
  # Optional lockfiles (for alternative managers)
179
183
  poetry.lock
180
184
  pdm.lock
181
- uv.lock
182
185
 
183
186
  # Local, per-developer settings (Claude Code etc.) - never commit
184
187
  *.local.json
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.5] 2026-10-15 14:11:33
11
+
12
+ ### Added
13
+ - Python 3.15 is a supported version: declared in the classifiers, so CI and `make test-all` run it.
14
+ - `uv.lock` is committed, and `[tool.uv] exclude-newer` sets a cooldown that keeps releases younger
15
+ than it out of the lock. CI installs exactly the locked tree on push and pull request; the nightly
16
+ scheduled run still resolves afresh to catch upstream breakage early.
17
+
18
+ ### Changed
19
+ - The install section explains why installing with uv protects against supply-chain attacks and why
20
+ a plain `pip install` from PyPI does not.
21
+ - `rtoml` floor raised to 0.14.0, the first release with Python 3.15 wheels.
22
+
10
23
  ## [3.0.4] 2026-08-01 00:14:59
11
24
  ### Fixed
12
25
  - **Console output no longer crashes on a legacy codepage.** A Windows console at codepage 1252
@@ -1,36 +1,5 @@
1
1
  # Claude Code Guidelines for lib_detect_testenv
2
2
 
3
- ## Session Initialization
4
-
5
- When starting a new session, read and apply the following system prompt files from `/media/srv-main-softdev/projects/softwarestack/systemprompts`:
6
-
7
- ### Core Guidelines (Always Apply)
8
- - `core_programming_solid.md`
9
-
10
- ### Bash-Specific Guidelines
11
- When working with Bash scripts:
12
- - use skill `bitranox:bash-reference` when in doubt of bash features or syntax
13
- - `core_programming_solid.md`
14
- - use skill `bitranox:bash-clean-architecture`
15
- - `bash_clean_code.md`
16
- - `bash_small_functions.md`
17
-
18
- ### Python-Specific Guidelines
19
- When working with Python code:
20
- - `core_programming_solid.md`
21
- - `python_solid_architecture_enforcer.md`
22
- - use skill `bitranox:python-clean-architecture`
23
- - `python_clean_code.md`
24
- - `python_small_functions_style.md`
25
- - use skill `bitranox:python-use-modern-libraries`
26
- - `python_structure_template.md`
27
-
28
- ### Additional Guidelines
29
- - `self_documenting.md`
30
- - `self_documenting_template.md`
31
- - `python_jupyter_notebooks.md`
32
- - `python_testing.md`
33
-
34
3
  ## Project Structure
35
4
 
36
5
  ```
@@ -98,32 +67,14 @@ lib_detect_testenv/
98
67
  - **Single Source of Truth**: Package version is in `pyproject.toml` (`[project].version`)
99
68
  - **Version Bumps**: update `pyproject.toml`, `CHANGELOG.md` and update the constants in `src/lib_detect_testenv/__init__conf__.py` according to `pyproject.toml`
100
69
  - Automation rewrites `src/lib_detect_testenv/__init__conf__.py` from `pyproject.toml`, so runtime code imports generated constants instead of querying `importlib.metadata`.
101
- - After updating project metadata (version, summary, URLs, authors) run `make test` (or `python -m scripts.test`) to regenerate the metadata module before committing.
70
+ - bmk regenerates that metadata module itself - it runs as a stage of `make bump`, and again before every `make commit` / `make push`. `make test` does not regenerate it, and the generated constants are never hand-edited.
102
71
  - **Release Tags**: Format is `vX.Y.Z` (push tags for CI to build and publish)
103
72
 
104
- ## Common Make Targets
73
+ ## Make targets specific to this repo
105
74
 
106
- | Target | Description |
107
- |-------------------|---------------------------------------------------------------------------------|
108
- | `build` | Build wheel/sdist artifacts |
109
- | `bump` | Bump version (VERSION=X.Y.Z or PART=major\|minor\|patch) and update changelog |
110
- | `bump-major` | Increment major version ((X+1).0.0) |
111
- | `bump-minor` | Increment minor version (X.Y.Z → X.(Y+1).0) |
112
- | `bump-patch` | Increment patch version (X.Y.Z → X.Y.(Z+1)) |
113
- | `clean` | Remove caches, coverage, and build artifacts (includes `dist/` and `build/`) |
114
- | `dev` | Install package with dev extras |
115
- | `help` | Show make targets |
116
- | `install` | Editable install |
117
- | `menu` | Interactive TUI menu |
118
- | `push` | Commit changes and push to GitHub (no CI monitoring) |
119
- | `release` | Tag vX.Y.Z, push, sync packaging, run gh release if available |
120
- | `run` | Run module entry (`python -m ... --help`) |
121
- | `test` | Lint, format, type-check, run tests with coverage, upload to Codecov |
122
- | `version-current` | Print current version from `pyproject.toml` |
75
+ - `make run` - Run module entry (`python -m ... --help`)
123
76
 
124
- ## Coding Style & Naming Conventions
125
-
126
- Follow the guidelines in `python_clean_code.md` for all Python code.
77
+ Everything else is the generated bmk list: run `make help`.
127
78
 
128
79
  ## Architecture Overview
129
80
 
@@ -137,41 +88,10 @@ Import rules (enforced by import-linter):
137
88
  - `cli` depends on `lib_detect_testenv` and `behaviors`
138
89
  - Core detection functions have no internal dependencies
139
90
 
140
- Apply principles use skill `bitranox:python-clean-architecture` when designing and implementing features.
141
-
142
- ## Security & Configuration
143
-
144
- - `.env` files are for local tooling only (CodeCov tokens, etc.)
145
- - **NEVER** commit secrets to version control
146
- - Rich logging should sanitize payloads before rendering
147
-
148
- ## Documentation & Translations
149
-
150
- ### Web Documentation
151
- - Update only English docs under `/website/docs`
152
- - Other languages are translated automatically
153
- - When in doubt, ask before modifying non-English documentation
154
-
155
- ### App UI Strings (i18n)
156
- - Update only `sources/_locales/en` for string changes
157
- - Other languages are translated automatically
158
- - When in doubt, ask before modifying non-English locales
91
+ Apply principles use skill `bitranox:coding-python-clean-architecture` when designing and implementing features.
159
92
 
160
93
  ## Commit & Push Policy
161
94
 
162
95
  ### Pre-Push Requirements
163
96
  - **Always run `make test` before pushing** to avoid lint/test breakage
164
97
  - Ensure all tests pass and code is properly formatted
165
-
166
- ### Post-Push Monitoring
167
- - Monitor GitHub Actions for errors after pushing
168
- - Attempt to correct any CI/CD errors that appear
169
-
170
- ## Claude Code Workflow
171
-
172
- When working on this project:
173
- 1. Read relevant system prompts at session start
174
- 2. Apply appropriate coding guidelines based on file type
175
- 3. Run `make test` before commits
176
- 4. Follow versioning guidelines for releases
177
- 5. Monitor CI after pushing changes
@@ -1,4 +1,4 @@
1
- # BMK MAKEFILE 3.14.0
1
+ # BMK MAKEFILE 3.21.1
2
2
  # do not alter this file - it might be overwritten on new versions of BMK
3
3
  # if You want to alter it, remove the first line # BMK MAKEFILE 1.0 - then it is a custom makefile and will not be overwritten
4
4
  # bmk Makefile - thin wrapper using `uv tool upgrade` for persistent bmk
@@ -186,16 +186,50 @@ endif
186
186
  # BMK_MIN floors the FALLBACK install. The upgrade path does not consult it: uv re-resolves
187
187
  # the requirement recorded at install time, which is itself a `bmk>=...`, so it always lands
188
188
  # on the newest release and clears the floor anyway.
189
- BMK_MIN := 3.14.0
189
+ BMK_MIN := 3.21.1
190
190
 
191
191
  # Absent env, missing check, or damaged env - all three mean "do not upgrade, rebuild".
192
192
  # `test -x` keeps a fresh machine quiet: there is no interpreter to run yet.
193
193
  BMK_INTACT := $(if $(BMK_PY),test -x "$(BMK_PY)" && "$(BMK_PY)" -m bmk_selfcheck,false)
194
194
 
195
+ # The env this recipe mutates is shared by EVERY repo on the machine, so the mutation has to
196
+ # wait for the bmk processes running out of it. Every bmk started from that env holds a
197
+ # SHARED lock on <uv tool dir>/.bmk-tool.lock for its whole lifetime; these wrappers take
198
+ # the same lock EXCLUSIVE for the length of the upgrade. Without it, a make here deleted the
199
+ # site-packages out from under a bmk minutes into a test suite in an UNRELATED repo, and it
200
+ # surfaced as an ImportError inside bmk's OWN dependencies that cleared on a re-run - so it
201
+ # read as a flake rather than as the environment vanishing mid-gate.
202
+ #
203
+ # `bmk_toollock` is a second stdlib-only TOP-LEVEL module bmk ships, run on the tool env's
204
+ # own interpreter. That is not circular: a uv tool env's bin/python is a SYMLINK to a base
205
+ # interpreter OUTSIDE the tool dir, so the stdlib the guard runs on is not part of the tree
206
+ # uv is about to replace, and the guard is read and compiled before uv is ever spawned.
207
+ #
208
+ # NOT uv's own <uv tool dir>/.lock, which uv does honour: `uv tool upgrade` and `uv tool
209
+ # list` block on it too (measured), and the upgrade runs before every target - so a
210
+ # gate-lifetime shared lock there would make every make on the machine, and every unrelated
211
+ # `uv tool install`, wait for the longest-running gate.
212
+ #
213
+ # $(wildcard) rather than a shell test: on a machine with no tool env yet these expand to
214
+ # NOTHING and the install runs bare, which is exactly what a first install needs.
215
+ #
216
+ # The bounded wait is load-bearing in both directions. An unbounded one would serialise
217
+ # every repo's make behind the longest gate on the machine. A SKIPPED upgrade costs nothing
218
+ # (the next make picks it up), so the upgrade skips. The REPAIR may not skip: proceeding
219
+ # would leave a damaged env in place, so it fails and drops through to the retry.
220
+ #
221
+ # The LAST attempt is deliberately NEVER guarded, and a test pins that. The guard lives
222
+ # inside the env being repaired, so on the path where that env is absent or broken the guard
223
+ # may be broken too; an unguarded last resort is what stops a defective guard from making
224
+ # `make` unrunnable in every repo at once. If bmk_toollock ever falls out of the wheel, every
225
+ # make everywhere drops to a full reinstall - there is a packaging test for exactly that.
226
+ BMK_LOCK := $(if $(wildcard $(BMK_PY)),"$(BMK_PY)" -m bmk_toollock --exclusive --timeout 10 --on-timeout skip --,)
227
+ BMK_LOCK_REPAIR := $(if $(wildcard $(BMK_PY)),"$(BMK_PY)" -m bmk_toollock --exclusive --timeout 60 --on-timeout fail --,)
228
+
195
229
  .PHONY: _ensure_bmk
196
230
  _ensure_bmk:
197
- @$(BMK_INTACT) && uv tool upgrade bmk \
198
- || uv tool install --reinstall --force "bmk>=$(BMK_MIN)" \
231
+ @$(BMK_INTACT) && $(BMK_LOCK) uv tool upgrade bmk \
232
+ || $(BMK_LOCK_REPAIR) uv tool install --reinstall --force "bmk>=$(BMK_MIN)" \
199
233
  || uv tool install --reinstall --force "bmk>=$(BMK_MIN)"
200
234
 
201
235
  # ──────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_detect_testenv
3
- Version: 3.0.4
3
+ Version: 3.0.5
4
4
  Summary: Detect test environment - pytest, doctest, unittest, or regular execution
5
5
  Project-URL: Homepage, https://github.com/bitranox/lib_detect_testenv
6
6
  Project-URL: Repository, https://github.com/bitranox/lib_detect_testenv.git
@@ -18,24 +18,26 @@ Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
21
23
  Classifier: Typing :: Typed
22
24
  Requires-Python: >=3.10
23
- Requires-Dist: rich-click>=1.9.8
25
+ Requires-Dist: rich-click>=1.9.9
24
26
  Provides-Extra: dev
25
27
  Requires-Dist: bandit<1.9,>=1.8.6; (python_version < '3.10') and extra == 'dev'
26
28
  Requires-Dist: bandit>=1.9.4; (python_version >= '3.10') and extra == 'dev'
27
- Requires-Dist: build>=1.5.0; extra == 'dev'
28
- Requires-Dist: click>=8.4.2; extra == 'dev'
29
+ Requires-Dist: build>=1.6.1; extra == 'dev'
30
+ Requires-Dist: click>=8.5.0; extra == 'dev'
29
31
  Requires-Dist: import-linter<2.6,>=2.5.2; (python_version < '3.10') and extra == 'dev'
30
- Requires-Dist: import-linter>=2.13; (python_version >= '3.10') and extra == 'dev'
32
+ Requires-Dist: import-linter>=2.15; (python_version >= '3.10') and extra == 'dev'
31
33
  Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
32
- Requires-Dist: pyright[nodejs]>=1.1.411; extra == 'dev'
34
+ Requires-Dist: pyright[nodejs]>=1.1.414; extra == 'dev'
33
35
  Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
34
36
  Requires-Dist: pytest<9,>=8.4.2; (python_version < '3.10') and extra == 'dev'
35
37
  Requires-Dist: pytest>=9.1.1; (python_version >= '3.10') and extra == 'dev'
36
- Requires-Dist: rtoml>=0.13.0; extra == 'dev'
37
- Requires-Dist: ruff>=0.16.1; extra == 'dev'
38
- Requires-Dist: setuptools>=83.0.0; extra == 'dev'
38
+ Requires-Dist: rtoml>=0.14.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.17.0; extra == 'dev'
40
+ Requires-Dist: setuptools>=84.0.0; extra == 'dev'
39
41
  Requires-Dist: textual>=8.2.8; extra == 'dev'
40
42
  Requires-Dist: twine>=7.0.0; extra == 'dev'
41
43
  Description-Content-Type: text/markdown
@@ -72,7 +74,7 @@ This library provides utility functions to detect whether your code is running i
72
74
 
73
75
  ### Recommended: Install via UV
74
76
 
75
- UV is an ultrafast Python package installer written in Rust (10–20× faster than pip/poetry):
77
+ UV is an ultrafast Python package installer written in Rust (10-20× faster than pip/poetry):
76
78
 
77
79
  ```bash
78
80
  # Install uv
@@ -107,9 +109,39 @@ For more installation methods (pipx, source builds, etc.), see [INSTALL.md](INST
107
109
 
108
110
  ### Python Version Requirements
109
111
 
110
- - **Python 3.9+** is required
112
+ - **Python 3.10+** is required
111
113
  - Tested on Linux, macOS, and Windows
112
- - CI runs on CPython 3.9+ and PyPy
114
+ - CI runs on CPython 3.10 to 3.15
115
+
116
+ ### Why we recommend uv: supply-chain protection
117
+
118
+ This repository commits a `uv.lock`. It pins every package the project needs, transitive
119
+ dependencies included, to an exact version and to the hashes of that version's files. The
120
+ `exclude-newer` setting under `[tool.uv]` in `pyproject.toml` adds a cooldown: uv leaves any
121
+ release younger than that out of the lock. CI installs from the lock, so a hijacked release that
122
+ gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
123
+ match is refused.
124
+
125
+ That protection stays in this repository. The installable package declares only lower bounds
126
+ (`>=`) for its dependencies, so a plain `pip install` takes the newest release of every transitive
127
+ dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
128
+ hijacked version. `pipx install` and `uv tool install` without options do the same.
129
+
130
+ To install the command line tool with a cooldown of your own (uv then skips any release younger
131
+ than 7 days, transitive ones included), or to run the exact tree our CI tests:
132
+
133
+ ```bash
134
+ uv tool install --exclude-newer "7 days" lib_detect_testenv
135
+ git clone https://github.com/bitranox/lib_detect_testenv.git && cd lib_detect_testenv && uv sync --locked && uv run lib_detect_testenv --help
136
+ ```
137
+
138
+ If you depend on lib_detect_testenv in your own project, give that project the same cooldown (7 days here, as
139
+ an example) and commit the `uv.lock` that `uv add lib_detect_testenv` writes:
140
+
141
+ ```toml
142
+ [tool.uv]
143
+ exclude-newer = "7 days"
144
+ ```
113
145
 
114
146
  ## Usage
115
147
 
@@ -121,11 +153,11 @@ The package provides a CLI for shell scripting and CI/CD integration. All comman
121
153
 
122
154
  All detection commands use a **3-level exit code system**:
123
155
 
124
- | Code | Meaning | Use Case |
125
- |------|---------|----------|
126
- | **0** | Test environment **detected** | Success - test env found |
127
- | **1** | Test environment **not detected** | Normal negative result (not an error) |
128
- | **2** | **Error occurred** | Command failed (bad arguments, exception) |
156
+ | Code | Meaning | Use Case |
157
+ |-------|-----------------------------------|-------------------------------------------|
158
+ | **0** | Test environment **detected** | Success - test env found |
159
+ | **1** | Test environment **not detected** | Normal negative result (not an error) |
160
+ | **2** | **Error occurred** | Command failed (bad arguments, exception) |
129
161
 
130
162
  This design follows Unix conventions and makes shell scripting intuitive:
131
163
 
@@ -30,7 +30,7 @@ This library provides utility functions to detect whether your code is running i
30
30
 
31
31
  ### Recommended: Install via UV
32
32
 
33
- UV is an ultrafast Python package installer written in Rust (10–20× faster than pip/poetry):
33
+ UV is an ultrafast Python package installer written in Rust (10-20× faster than pip/poetry):
34
34
 
35
35
  ```bash
36
36
  # Install uv
@@ -65,9 +65,39 @@ For more installation methods (pipx, source builds, etc.), see [INSTALL.md](INST
65
65
 
66
66
  ### Python Version Requirements
67
67
 
68
- - **Python 3.9+** is required
68
+ - **Python 3.10+** is required
69
69
  - Tested on Linux, macOS, and Windows
70
- - CI runs on CPython 3.9+ and PyPy
70
+ - CI runs on CPython 3.10 to 3.15
71
+
72
+ ### Why we recommend uv: supply-chain protection
73
+
74
+ This repository commits a `uv.lock`. It pins every package the project needs, transitive
75
+ dependencies included, to an exact version and to the hashes of that version's files. The
76
+ `exclude-newer` setting under `[tool.uv]` in `pyproject.toml` adds a cooldown: uv leaves any
77
+ release younger than that out of the lock. CI installs from the lock, so a hijacked release that
78
+ gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
79
+ match is refused.
80
+
81
+ That protection stays in this repository. The installable package declares only lower bounds
82
+ (`>=`) for its dependencies, so a plain `pip install` takes the newest release of every transitive
83
+ dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
84
+ hijacked version. `pipx install` and `uv tool install` without options do the same.
85
+
86
+ To install the command line tool with a cooldown of your own (uv then skips any release younger
87
+ than 7 days, transitive ones included), or to run the exact tree our CI tests:
88
+
89
+ ```bash
90
+ uv tool install --exclude-newer "7 days" lib_detect_testenv
91
+ git clone https://github.com/bitranox/lib_detect_testenv.git && cd lib_detect_testenv && uv sync --locked && uv run lib_detect_testenv --help
92
+ ```
93
+
94
+ If you depend on lib_detect_testenv in your own project, give that project the same cooldown (7 days here, as
95
+ an example) and commit the `uv.lock` that `uv add lib_detect_testenv` writes:
96
+
97
+ ```toml
98
+ [tool.uv]
99
+ exclude-newer = "7 days"
100
+ ```
71
101
 
72
102
  ## Usage
73
103
 
@@ -79,11 +109,11 @@ The package provides a CLI for shell scripting and CI/CD integration. All comman
79
109
 
80
110
  All detection commands use a **3-level exit code system**:
81
111
 
82
- | Code | Meaning | Use Case |
83
- |------|---------|----------|
84
- | **0** | Test environment **detected** | Success - test env found |
85
- | **1** | Test environment **not detected** | Normal negative result (not an error) |
86
- | **2** | **Error occurred** | Command failed (bad arguments, exception) |
112
+ | Code | Meaning | Use Case |
113
+ |-------|-----------------------------------|-------------------------------------------|
114
+ | **0** | Test environment **detected** | Success - test env found |
115
+ | **1** | Test environment **not detected** | Normal negative result (not an error) |
116
+ | **2** | **Error occurred** | Command failed (bad arguments, exception) |
87
117
 
88
118
  This design follows Unix conventions and makes shell scripting intuitive:
89
119
 
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "lib_detect_testenv"
3
- version = "3.0.4"
3
+ version = "3.0.5"
4
4
  description = "Detect test environment - pytest, doctest, unittest, or regular execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "rich-click>=1.9.8",
8
+ "rich-click>=1.9.9",
9
9
  ]
10
10
  license = { text = "MIT" }
11
11
  authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
@@ -17,6 +17,8 @@ classifiers = [
17
17
  "Programming Language :: Python :: 3.11",
18
18
  "Programming Language :: Python :: 3.12",
19
19
  "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: 3.15",
20
22
  "License :: OSI Approved :: MIT License",
21
23
  "Environment :: Console",
22
24
  "Operating System :: OS Independent",
@@ -33,13 +35,11 @@ dev = [
33
35
  "pytest>=8.4.2,<9; python_version<'3.10'",
34
36
  "pytest>=9.1.1; python_version>='3.10'",
35
37
  "pytest-cov>=7.1.0",
36
- "ruff>=0.16.1",
37
- "pyright[nodejs]>=1.1.411",
38
+ "ruff>=0.17.0",
39
+ "pyright[nodejs]>=1.1.414",
38
40
  "bandit>=1.8.6,<1.9; python_version<'3.10'",
39
41
  "bandit>=1.9.4; python_version>='3.10'",
40
- # 1.5.1 was yanked upstream (unintended breaking changes), so 1.5.0 is the newest usable
41
- # release; a >=1.5.1 floor has ZERO candidates and makes this extra unresolvable.
42
- "build>=1.5.0",
42
+ "build>=1.6.1",
43
43
  "twine>=7.0.0",
44
44
  # codecov-cli is disabled, not dropped: it requires click<8.3.0, which drags click back to
45
45
  # 8.2.1 - the version with CVE-2026-7246 (command injection in click.edit(), fixed in 8.3.3).
@@ -49,13 +49,13 @@ dev = [
49
49
  # Re-enable once codecov-cli relaxes its click<8.3.0 pin.
50
50
  # "codecov-cli>=11.2.8",
51
51
  # Transitive CVE pins
52
- "click>=8.4.2", # CVE-2026-7246
52
+ "click>=8.5.0", # CVE-2026-7246
53
53
  "pip-audit>=2.10.1",
54
54
  "textual>=8.2.8",
55
55
  "import-linter>=2.5.2,<2.6; python_version<'3.10'",
56
- "import-linter>=2.13; python_version>='3.10'",
57
- "setuptools>=83.0.0",
58
- "rtoml>=0.13.0",
56
+ "import-linter>=2.15; python_version>='3.10'",
57
+ "setuptools>=84.0.0",
58
+ "rtoml>=0.14.0",
59
59
  ]
60
60
 
61
61
  [project.scripts]
@@ -200,3 +200,6 @@ ignore-vulns = ["GHSA-4xh5-x5gv-qwph", "PYSEC-2022-42969", "CVE-2026-25990", "CV
200
200
  [tool.bashate]
201
201
  max-line-length = 120
202
202
  ignores = ["E003"]
203
+
204
+ [tool.uv]
205
+ exclude-newer = "1 day"
@@ -26,7 +26,7 @@ name = "lib_detect_testenv"
26
26
  #: Human-readable summary shown in CLI help output.
27
27
  title = "Detect test environment - pytest, doctest, unittest, or regular execution"
28
28
  #: Current release version pulled from ``pyproject.toml`` by automation.
29
- version = "3.0.4"
29
+ version = "3.0.5"
30
30
  #: Repository homepage presented to users.
31
31
  homepage = "https://github.com/bitranox/lib_detect_testenv"
32
32
  #: Author attribution surfaced in CLI output.
@@ -64,13 +64,19 @@ ASCII_FALLBACKS: Final[dict[str, str]] = {
64
64
  _UNIVERSAL_ENCODINGS: Final[frozenset[str]] = frozenset({"utf-8", "utf8", "utf-16", "utf16", "utf-32", "utf32"})
65
65
 
66
66
 
67
- def _stream_encoding(file: IO[Any] | None) -> str | None:
67
+ def _stream_encoding(file: IO[Any] | None, *, err: bool = False) -> str | None:
68
68
  """Return the target stream's encoding, or None when it cannot be determined.
69
69
 
70
70
  An unknown encoding means the caller gets the original text: guessing would
71
71
  degrade output that may well have been fine.
72
+
73
+ With no explicit `file` the answer comes from ``sys.stdout``/``sys.stderr``,
74
+ which is what :func:`click.echo` resolves its own default target from. click
75
+ exposes no supported way to ask for that stream: ``get_text_stream`` was
76
+ deprecated in click 8.5.0 and is removed in 9.0, its documented replacement
77
+ being to let ``echo`` resolve the stream itself.
72
78
  """
73
- stream = file if file is not None else click.get_text_stream("stdout")
79
+ stream = file if file is not None else (sys.stderr if err else sys.stdout)
74
80
  encoding = getattr(stream, "encoding", None)
75
81
  return encoding if isinstance(encoding, str) else None
76
82
 
@@ -135,8 +141,7 @@ def echo(message: object = "", *, file: IO[Any] | None = None, err: bool = False
135
141
  Writes to the given stream.
136
142
  """
137
143
  text = message if isinstance(message, str) else str(message)
138
- target = file if file is not None else click.get_text_stream("stderr" if err else "stdout")
139
- click.echo(encode_safe(text, _stream_encoding(target)), file=target, nl=nl)
144
+ click.echo(encode_safe(text, _stream_encoding(file, err=err)), file=file, err=err, nl=nl)
140
145
 
141
146
 
142
147
  class _SafeWriter:
@@ -8,6 +8,7 @@ keeps the next output line someone adds covered by construction.
8
8
  from __future__ import annotations
9
9
 
10
10
  import io
11
+ import sys
11
12
  from pathlib import Path
12
13
 
13
14
  import pytest
@@ -78,6 +79,30 @@ class TestEchoOnAUtf8Console:
78
79
  assert "\u2713" in buffer.getvalue().decode("utf-8")
79
80
 
80
81
 
82
+ class TestTheDefaultTargetFollowsTheStreamEchoWritesTo:
83
+ """With no `file`, the encoding must come from the stream `echo` lands on.
84
+
85
+ click resolves that target itself from ``sys.stdout``/``sys.stderr``. Judge
86
+ the wrong stream and a legacy-codepage console gets exactly the crash this
87
+ module exists to prevent, while every test passing an explicit `file` stays
88
+ green and says nothing about it.
89
+ """
90
+
91
+ def test_a_cp1252_stdout_degrades_the_glyph(self, monkeypatch: pytest.MonkeyPatch) -> None:
92
+ stream = _cp1252_stream()
93
+ monkeypatch.setattr(sys, "stdout", stream)
94
+ safe_console.echo("✓ deployed")
95
+ assert "[OK]" in _read_back(stream)
96
+
97
+ def test_err_is_judged_against_stderr_not_stdout(self, monkeypatch: pytest.MonkeyPatch) -> None:
98
+ """A stdout that could take the glyph must not excuse a cp1252 stderr."""
99
+ stream = _cp1252_stream()
100
+ monkeypatch.setattr(sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="utf-8", newline=""))
101
+ monkeypatch.setattr(sys, "stderr", stream)
102
+ safe_console.echo("✓ deployed", err=True)
103
+ assert "[OK]" in _read_back(stream)
104
+
105
+
81
106
  class TestSafeStreamProtectsRich:
82
107
  """Rich raises on a legacy codepage too; it renders through its own writer."""
83
108