lib-detect-testenv 3.0.5__tar.gz → 3.0.7__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 (45) hide show
  1. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/CHANGELOG.md +36 -0
  2. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/PKG-INFO +13 -6
  3. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/README.md +6 -2
  4. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/docs/systemdesign/module_reference.md +11 -8
  5. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/pyproject.toml +27 -5
  6. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/__init__conf__.py +1 -1
  7. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/lib_detect_testenv.py +19 -2
  8. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/conftest.py +18 -0
  9. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_lib_detect_testenv.py +39 -0
  10. lib_detect_testenv-3.0.7/tests/test_packaging.py +127 -0
  11. lib_detect_testenv-3.0.7/tests/test_platform_markers.py +31 -0
  12. lib_detect_testenv-3.0.5/.devcontainer/devcontainer.json +0 -22
  13. lib_detect_testenv-3.0.5/.devcontainer/settings.json +0 -6
  14. lib_detect_testenv-3.0.5/.env.example +0 -11
  15. lib_detect_testenv-3.0.5/.github/actions/extract-metadata/action.yml +0 -108
  16. lib_detect_testenv-3.0.5/.github/dependabot.yml +0 -22
  17. lib_detect_testenv-3.0.5/.github/workflows/codeql.yml +0 -39
  18. lib_detect_testenv-3.0.5/.github/workflows/default_cicd_public.yml +0 -472
  19. lib_detect_testenv-3.0.5/.github/workflows/default_release_public.yml +0 -95
  20. lib_detect_testenv-3.0.5/.qlty/qlty.toml +0 -2
  21. lib_detect_testenv-3.0.5/.snyk +0 -5
  22. lib_detect_testenv-3.0.5/CLAUDE.md +0 -97
  23. lib_detect_testenv-3.0.5/CONTRIBUTING.md +0 -48
  24. lib_detect_testenv-3.0.5/DEVELOPMENT.md +0 -134
  25. lib_detect_testenv-3.0.5/INSTALL.md +0 -193
  26. lib_detect_testenv-3.0.5/Makefile +0 -487
  27. lib_detect_testenv-3.0.5/codecov.yml +0 -28
  28. lib_detect_testenv-3.0.5/concept.md +0 -312
  29. lib_detect_testenv-3.0.5/notebooks/Quickstart.ipynb +0 -165
  30. lib_detect_testenv-3.0.5/reset_git_history.sh +0 -55
  31. lib_detect_testenv-3.0.5/uv.lock +0 -1776
  32. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/.gitignore +0 -0
  33. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/LICENSE +0 -0
  34. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/__init__.py +0 -0
  35. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/__main__.py +0 -0
  36. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/behaviors.py +0 -0
  37. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/cli.py +0 -0
  38. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/py.typed +0 -0
  39. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/safe_console.py +0 -0
  40. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/src/lib_detect_testenv/typed_click.py +0 -0
  41. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_behaviors.py +0 -0
  42. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_cli.py +0 -0
  43. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_metadata.py +0 -0
  44. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_module_entry.py +0 -0
  45. {lib_detect_testenv-3.0.5 → lib_detect_testenv-3.0.7}/tests/test_safe_console.py +0 -0
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.7] 2026-10-29 00:44:33
11
+
12
+ ### Fixed
13
+
14
+ - `is_pytest_active()` and `is_testenv_active()` called without an `arg_string` return `True`
15
+ inside a pytest-xdist worker. A worker runs as `python -c`, so its `sys.argv` names neither
16
+ pytest nor a test file and both returned `False` there; they now also check the
17
+ `PYTEST_XDIST_WORKER` variable xdist sets in every worker. A call given an `arg_string` is still
18
+ judged on that string alone.
19
+
20
+ ### Tests
21
+
22
+ - The suite runs in parallel: `pytest-xdist` joins the dev extra and the `workers = 1` setting that
23
+ kept `make test` serial is gone.
24
+
25
+ ## [3.0.6] 2026-10-15 16:35:25
26
+
27
+ ### Fixed
28
+
29
+ - The source distribution ships only the package, its tests, its docs, `README.md`, `LICENSE`,
30
+ `CHANGELOG.md` and `pyproject.toml` (an include list). Earlier sdists also carried every other
31
+ root file and, in a local build, anything hidden only through `.git/info/exclude` or a nested
32
+ `.gitignore`. `tests/test_packaging.py` builds the sdist and wheel and checks both; `hatchling`
33
+ joins the dev extra for it.
34
+
35
+ ### Changed
36
+
37
+ - Ruff selects `C90` (mccabe), so a function over cyclomatic complexity 10, the house hard limit,
38
+ now fails `make test`; before, nothing enforced it.
39
+
40
+ ### Tests
41
+
42
+ - The `os_windows`, `os_macos`, `os_linux` and `os_posix` markers now skip a test off its platform
43
+ (`pytest_runtest_setup` in `tests/conftest.py`); registered in `pyproject.toml` alone they skipped
44
+ nothing, so a test trusting its marker ran everywhere. `tests/test_platform_markers.py` pins it.
45
+
10
46
  ## [3.0.5] 2026-10-15 14:11:33
11
47
 
12
48
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_detect_testenv
3
- Version: 3.0.5
3
+ Version: 3.0.7
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
@@ -27,17 +27,20 @@ Provides-Extra: dev
27
27
  Requires-Dist: bandit<1.9,>=1.8.6; (python_version < '3.10') and extra == 'dev'
28
28
  Requires-Dist: bandit>=1.9.4; (python_version >= '3.10') and extra == 'dev'
29
29
  Requires-Dist: build>=1.6.1; extra == 'dev'
30
+ Requires-Dist: certifi>=2026.10.23; extra == 'dev'
30
31
  Requires-Dist: click>=8.5.0; extra == 'dev'
32
+ Requires-Dist: hatchling>=1.32.4; extra == 'dev'
31
33
  Requires-Dist: import-linter<2.6,>=2.5.2; (python_version < '3.10') and extra == 'dev'
32
34
  Requires-Dist: import-linter>=2.15; (python_version >= '3.10') and extra == 'dev'
33
35
  Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
34
36
  Requires-Dist: pyright[nodejs]>=1.1.414; extra == 'dev'
35
37
  Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
38
+ Requires-Dist: pytest-xdist>=3.8.0; extra == 'dev'
36
39
  Requires-Dist: pytest<9,>=8.4.2; (python_version < '3.10') and extra == 'dev'
37
- Requires-Dist: pytest>=9.1.1; (python_version >= '3.10') and extra == 'dev'
40
+ Requires-Dist: pytest>=9.2.0; (python_version >= '3.10') and extra == 'dev'
38
41
  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'
42
+ Requires-Dist: ruff>=0.17.3; extra == 'dev'
43
+ Requires-Dist: setuptools>=84.2.1; extra == 'dev'
41
44
  Requires-Dist: textual>=8.2.8; extra == 'dev'
42
45
  Requires-Dist: twine>=7.0.0; extra == 'dev'
43
46
  Description-Content-Type: text/markdown
@@ -253,7 +256,9 @@ else:
253
256
  Returns `True` if any test environment is detected (pytest, doctest, or setup.py test).
254
257
 
255
258
  **Parameters:**
256
- - `arg_string` (optional): If `None`, uses `str(sys.argv())`
259
+ - `arg_string` (optional): If `None`, uses `str(sys.argv())`, and a pytest-xdist worker
260
+ (`PYTEST_XDIST_WORKER` set) counts as pytest: its `sys.argv` does not name pytest. A given
261
+ `arg_string` is judged on its own.
257
262
 
258
263
  **Returns:**
259
264
  - `True` if test environment is detected
@@ -296,7 +301,9 @@ assert is_doctest_active(arg_string="doctest.py") == True
296
301
  Returns `True` if pytest is detected.
297
302
 
298
303
  **Parameters:**
299
- - `arg_string` (optional): If `None`, uses `str(sys.argv())`
304
+ - `arg_string` (optional): If `None`, uses `str(sys.argv())`, and a pytest-xdist worker
305
+ (`PYTEST_XDIST_WORKER` set) counts as pytest: its `sys.argv` does not name pytest. A given
306
+ `arg_string` is judged on its own.
300
307
 
301
308
  **Returns:**
302
309
  - `True` if pytest is detected
@@ -209,7 +209,9 @@ else:
209
209
  Returns `True` if any test environment is detected (pytest, doctest, or setup.py test).
210
210
 
211
211
  **Parameters:**
212
- - `arg_string` (optional): If `None`, uses `str(sys.argv())`
212
+ - `arg_string` (optional): If `None`, uses `str(sys.argv())`, and a pytest-xdist worker
213
+ (`PYTEST_XDIST_WORKER` set) counts as pytest: its `sys.argv` does not name pytest. A given
214
+ `arg_string` is judged on its own.
213
215
 
214
216
  **Returns:**
215
217
  - `True` if test environment is detected
@@ -252,7 +254,9 @@ assert is_doctest_active(arg_string="doctest.py") == True
252
254
  Returns `True` if pytest is detected.
253
255
 
254
256
  **Parameters:**
255
- - `arg_string` (optional): If `None`, uses `str(sys.argv())`
257
+ - `arg_string` (optional): If `None`, uses `str(sys.argv())`, and a pytest-xdist worker
258
+ (`PYTEST_XDIST_WORKER` set) counts as pytest: its `sys.argv` does not name pytest. A given
259
+ `arg_string` is judged on its own.
256
260
 
257
261
  **Returns:**
258
262
  - `True` if pytest is detected
@@ -77,11 +77,12 @@ Behaviors Layer (behaviors.py) ← Demo commands only
77
77
  **Purpose:** Detect if any test environment is active (pytest, doctest, or setup.py test).
78
78
 
79
79
  **Input:**
80
- - `arg_string` (optional): Custom argument string to check. If None, uses `str(sys.argv)`.
80
+ - `arg_string` (optional): Custom argument string to check. If None, uses `str(sys.argv)`, and a
81
+ pytest-xdist worker (`PYTEST_XDIST_WORKER` set) counts as pytest.
81
82
 
82
83
  **Output:** `True` if test environment detected, `False` otherwise.
83
84
 
84
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:9
85
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:10
85
86
 
86
87
  **Example:**
87
88
  ```python
@@ -102,12 +103,14 @@ if is_testenv_active():
102
103
  - Checks for `/pytest/__main__.py` in arguments
103
104
  - Checks for `python -m pytest` pattern
104
105
  - Checks `PYTEST_IS_RUNNING` environment variable
106
+ - Without an `arg_string`, also checks `PYTEST_XDIST_WORKER`: a pytest-xdist worker runs as
107
+ `python -c`, so its `sys.argv` does not name pytest
105
108
 
106
109
  **Input:** `arg_string` (optional): Custom argument string to check.
107
110
 
108
111
  **Output:** `True` if pytest detected, `False` otherwise.
109
112
 
110
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:68
113
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:70
111
114
 
112
115
  **Example:**
113
116
  ```python
@@ -132,7 +135,7 @@ if is_pytest_active():
132
135
 
133
136
  **Output:** `True` if doctest detected, `False` otherwise.
134
137
 
135
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:38
138
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:42
136
139
 
137
140
  ---
138
141
 
@@ -147,7 +150,7 @@ if is_pytest_active():
147
150
 
148
151
  **Output:** `True` if setup.py detected, `False` otherwise.
149
152
 
150
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:105
153
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:108
151
154
 
152
155
  ---
153
156
 
@@ -162,7 +165,7 @@ if is_pytest_active():
162
165
 
163
166
  **Output:** `True` if `setup.py test` detected, `False` otherwise.
164
167
 
165
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:132
168
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:135
166
169
 
167
170
  ---
168
171
 
@@ -174,7 +177,7 @@ if is_pytest_active():
174
177
 
175
178
  **Output:** `True` if docrunner.py found, `False` otherwise.
176
179
 
177
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:164
180
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:167
178
181
 
179
182
  ---
180
183
 
@@ -193,7 +196,7 @@ if is_pytest_active():
193
196
 
194
197
  **Output:** None (modifies sys.path in place)
195
198
 
196
- **Location:** src/lib_detect_testenv/lib_detect_testenv.py:188
199
+ **Location:** src/lib_detect_testenv/lib_detect_testenv.py:198
197
200
 
198
201
  **Example:**
199
202
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lib_detect_testenv"
3
- version = "3.0.5"
3
+ version = "3.0.7"
4
4
  description = "Detect test environment - pytest, doctest, unittest, or regular execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -33,13 +33,15 @@ Issues = "https://github.com/bitranox/lib_detect_testenv/issues"
33
33
  [project.optional-dependencies]
34
34
  dev = [
35
35
  "pytest>=8.4.2,<9; python_version<'3.10'",
36
- "pytest>=9.1.1; python_version>='3.10'",
36
+ "pytest>=9.2.0; python_version>='3.10'",
37
37
  "pytest-cov>=7.1.0",
38
- "ruff>=0.17.0",
38
+ "pytest-xdist>=3.8.0",
39
+ "ruff>=0.17.3",
39
40
  "pyright[nodejs]>=1.1.414",
40
41
  "bandit>=1.8.6,<1.9; python_version<'3.10'",
41
42
  "bandit>=1.9.4; python_version>='3.10'",
42
43
  "build>=1.6.1",
44
+ "hatchling>=1.32.4",
43
45
  "twine>=7.0.0",
44
46
  # codecov-cli is disabled, not dropped: it requires click<8.3.0, which drags click back to
45
47
  # 8.2.1 - the version with CVE-2026-7246 (command injection in click.edit(), fixed in 8.3.3).
@@ -54,8 +56,9 @@ dev = [
54
56
  "textual>=8.2.8",
55
57
  "import-linter>=2.5.2,<2.6; python_version<'3.10'",
56
58
  "import-linter>=2.15; python_version>='3.10'",
57
- "setuptools>=84.0.0",
59
+ "setuptools>=84.2.1",
58
60
  "rtoml>=0.14.0",
61
+ "certifi>=2026.10.23", # PYSEC-2026-4320
59
62
  ]
60
63
 
61
64
  [project.scripts]
@@ -71,6 +74,21 @@ build-backend = "hatchling.build"
71
74
  packages = ["src/lib_detect_testenv"]
72
75
  include = ["src/lib_detect_testenv/py.typed"]
73
76
 
77
+ # An allowlist, never a denylist: hatchling reads only the root .gitignore, so tracked working
78
+ # files and files hidden through .git/info/exclude or a nested .gitignore (session records,
79
+ # review notes, helper scripts) would ship. tests/test_packaging.py builds the sdist and
80
+ # refuses any other top-level entry.
81
+ [tool.hatch.build.targets.sdist]
82
+ include = [
83
+ "/src/lib_detect_testenv",
84
+ "/tests/*.py",
85
+ "/docs",
86
+ "/README.md",
87
+ "/LICENSE",
88
+ "/CHANGELOG.md",
89
+ "/pyproject.toml",
90
+ ]
91
+
74
92
  [tool.ruff]
75
93
  line-length = 160
76
94
  target-version = "py39"
@@ -85,6 +103,7 @@ select = [
85
103
  "B", # bugbear
86
104
  "A", # builtins shadowing
87
105
  "C4", # comprehension simplification
106
+ "C90", # mccabe: cyclomatic complexity <= 10 (the house hard limit)
88
107
  "SIM", # simplification
89
108
  "TCH", # type-checking imports
90
109
  "RUF", # ruff-specific
@@ -105,7 +124,10 @@ convention = "google"
105
124
 
106
125
  [tool.ruff.lint.per-file-ignores]
107
126
  # ignore the "unused import sys" in the jupyter notebook - which we need (it is a linter hickup)
108
- "notebooks/*.ipynb" = ["F401"]
127
+ # A `!cmd` never raises, so CI sees a failing shell command only through
128
+ # `assert _exit_code == N` after it: IPython injects `_exit_code` at run time (F821)
129
+ # and those asserts are the gate (S101).
130
+ "notebooks/*.ipynb" = ["F401", "F821", "S101"]
109
131
  # Tests: assert is standard, test fixtures have passwords, magic values are clearer inline, temp paths are fine.
110
132
  # PLC0415: local/deferred imports are a deliberate test idiom - several tests import inside the test
111
133
  # body to exercise import-time and cache behaviour, so moving them to module top would defeat intent.
@@ -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.5"
29
+ version = "3.0.7"
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.
@@ -14,7 +14,8 @@ def is_testenv_active(arg_string: Optional[str] = None) -> bool:
14
14
 
15
15
  Parameter
16
16
  ----------
17
- arg_string : optional, if None : str(sys.argv())
17
+ arg_string : optional, if None : str(sys.argv()), and a pytest-xdist worker
18
+ (PYTEST_XDIST_WORKER set) counts as pytest
18
19
 
19
20
 
20
21
  Result
@@ -32,6 +33,8 @@ def is_testenv_active(arg_string: Optional[str] = None) -> bool:
32
33
 
33
34
  >>> assert is_testenv_active() == True
34
35
  """
36
+ if arg_string is None and _is_pytest_xdist_worker():
37
+ return True
35
38
  arg_string = _get_sys_argv_str(arg_string)
36
39
  return is_doctest_active(arg_string=arg_string) or is_pytest_active(arg_string=arg_string) or is_setup_test_active(arg_string=arg_string)
37
40
 
@@ -71,7 +74,8 @@ def is_pytest_active(arg_string: Optional[str] = None) -> bool:
71
74
 
72
75
  Parameter
73
76
  ----------
74
- arg_string : optional, if None : str(sys.argv())
77
+ arg_string : optional, if None : str(sys.argv()), and a pytest-xdist worker
78
+ (PYTEST_XDIST_WORKER set) counts as pytest
75
79
 
76
80
 
77
81
  Result
@@ -88,6 +92,8 @@ def is_pytest_active(arg_string: Optional[str] = None) -> bool:
88
92
 
89
93
  """
90
94
 
95
+ if arg_string is None and _is_pytest_xdist_worker():
96
+ return True
91
97
  arg_string = _get_sys_argv_str(arg_string)
92
98
  # this is used in our tests when we test cli-commands
93
99
  if os.getenv("PYTEST_IS_RUNNING"):
@@ -167,6 +173,17 @@ def is_doctest_in_arg_string(arg_string: str) -> bool:
167
173
  return "docrunner.py" in arg_string
168
174
 
169
175
 
176
+ def _is_pytest_xdist_worker() -> bool:
177
+ """
178
+ returns True inside a pytest-xdist worker process
179
+
180
+ A worker is started by execnet as "python -c <bootstrap>", so its sys.argv names neither
181
+ pytest nor a test file; pytest-xdist sets PYTEST_XDIST_WORKER (gw0, gw1, ...) in every worker.
182
+ Only a call that reads the real process asks this: an explicit arg_string is judged alone.
183
+ """
184
+ return bool(os.getenv("PYTEST_XDIST_WORKER"))
185
+
186
+
170
187
  def _get_sys_argv_str(arg_string: Optional[str] = None) -> str:
171
188
  """
172
189
  gets the sys.argv as string. backslashes in Windows are replaced with slashes
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
+ import sys
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  import pytest
@@ -48,3 +49,20 @@ def strip_ansi() -> Callable[[str], str]:
48
49
  return _remove_ansi_codes(value)
49
50
 
50
51
  return _strip
52
+
53
+
54
+ #: Which platforms each ``os_*`` marker lets a test run on. Registering a marker in pyproject only
55
+ #: silences pytest's unknown-marker warning; without this table a marked test runs everywhere.
56
+ _PLATFORM_MARKERS: dict[str, Callable[[str], bool]] = {
57
+ "os_windows": lambda platform: platform == "win32",
58
+ "os_macos": lambda platform: platform == "darwin",
59
+ "os_linux": lambda platform: platform.startswith("linux"),
60
+ "os_posix": lambda platform: platform != "win32",
61
+ }
62
+
63
+
64
+ def pytest_runtest_setup(item: pytest.Item) -> None:
65
+ """Skip a test whose ``os_*`` marker names a platform other than the running one."""
66
+ for name, runs_on in _PLATFORM_MARKERS.items():
67
+ if item.get_closest_marker(name) is not None and not runs_on(sys.platform):
68
+ pytest.skip(f"marked {name}; this is {sys.platform}")
@@ -113,6 +113,45 @@ def test_is_pytest_active_with_pytest_env_var(monkeypatch: pytest.MonkeyPatch) -
113
113
  assert is_pytest_active(arg_string="") is True
114
114
 
115
115
 
116
+ #: The argv a pytest-xdist worker process runs with: execnet starts it as ``python -c <bootstrap>``.
117
+ _XDIST_WORKER_ARGV = ["-c"]
118
+
119
+
120
+ @pytest.fixture
121
+ def worker_process_argv(monkeypatch: pytest.MonkeyPatch) -> pytest.MonkeyPatch:
122
+ """Make the process look like an xdist worker's argv, with neither pytest variable set."""
123
+ monkeypatch.setattr(sys, "argv", _XDIST_WORKER_ARGV)
124
+ monkeypatch.delenv("PYTEST_IS_RUNNING", raising=False)
125
+ monkeypatch.delenv("PYTEST_XDIST_WORKER", raising=False)
126
+ return monkeypatch
127
+
128
+
129
+ @pytest.mark.os_agnostic
130
+ def test_a_worker_argv_alone_does_not_read_as_pytest(worker_process_argv: pytest.MonkeyPatch) -> None:
131
+ """The control: with the argv replaced and no xdist variable, nothing names pytest."""
132
+ assert is_pytest_active() is False
133
+ assert is_testenv_active() is False
134
+
135
+
136
+ @pytest.mark.os_agnostic
137
+ def test_an_xdist_worker_reads_as_pytest(worker_process_argv: pytest.MonkeyPatch) -> None:
138
+ """A pytest-xdist worker's argv does not name pytest; xdist sets PYTEST_XDIST_WORKER in it."""
139
+ worker_process_argv.setenv("PYTEST_XDIST_WORKER", "gw0")
140
+
141
+ assert is_pytest_active() is True
142
+ assert is_testenv_active() is True
143
+
144
+
145
+ @pytest.mark.os_agnostic
146
+ def test_an_explicit_arg_string_ignores_the_xdist_worker_variable(worker_process_argv: pytest.MonkeyPatch) -> None:
147
+ """Only a call reading the real process consults the worker variable; a given string is judged alone."""
148
+ worker_process_argv.setenv("PYTEST_XDIST_WORKER", "gw0")
149
+
150
+ assert is_pytest_active(arg_string="") is False
151
+ assert is_pytest_active(arg_string="python test.py") is False
152
+ assert is_testenv_active(arg_string="python script.py") is False
153
+
154
+
116
155
  # ============================================================================
117
156
  # Tests for is_doctest_active()
118
157
  # ============================================================================
@@ -0,0 +1,127 @@
1
+ """The source distribution carries the package, its tests and its docs, and nothing else.
2
+
3
+ A working tree holds files that must never be published: session records, backlogs, scratch
4
+ review notes and helper scripts. Some are tracked, some are hidden only through
5
+ ``.git/info/exclude`` or a nested ``.gitignore``, which hatchling does not read. The sdist is
6
+ therefore built from an allowlist, and this test builds one from a copy of the tree that also
7
+ holds such decoys. The wheel must still carry the package data the runtime loads.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import tarfile
14
+ import zipfile
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+ from hatchling.builders.sdist import SdistBuilder
19
+ from hatchling.builders.wheel import WheelBuilder
20
+
21
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
22
+ PACKAGE = "lib_detect_testenv"
23
+
24
+ # hatchling force-includes the root .gitignore into every sdist (so a build from the unpacked
25
+ # sdist applies the same exclusions); no include/exclude setting removes it.
26
+ ALLOWED_TOP_LEVEL = frozenset(
27
+ {
28
+ "PKG-INFO",
29
+ "pyproject.toml",
30
+ "README.md",
31
+ "LICENSE",
32
+ "CHANGELOG.md",
33
+ ".gitignore",
34
+ "src",
35
+ "tests",
36
+ "docs",
37
+ }
38
+ )
39
+ REQUIRED_TOP_LEVEL = frozenset({"PKG-INFO", "pyproject.toml", "src", "tests"})
40
+
41
+ # Files and directories that exist in working trees and must never ship. Tracked ones are
42
+ # named in the repository; the others are hidden locally through .git/info/exclude.
43
+ DECOY_FILES = ("handover.md", "OPEN-WORK.md", "EXECUTION-USER-REVIEW.md", "reset_git_history.sh", ".private/notes.txt")
44
+
45
+ REQUIRED_WHEEL_DATA = ("lib_detect_testenv/py.typed",)
46
+
47
+
48
+ def _copy_tree_with_decoys(destination: Path) -> Path:
49
+ """Copy the buildable tree and plant the files a working tree carries but must not ship."""
50
+ root = destination / "tree"
51
+ root.mkdir()
52
+ for name in ("pyproject.toml", "README.md", "LICENSE", "CHANGELOG.md", ".gitignore"):
53
+ shutil.copy2(PROJECT_ROOT / name, root / name)
54
+ for name in ("src", "docs", "tests"):
55
+ shutil.copytree(PROJECT_ROOT / name, root / name, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
56
+ for decoy in DECOY_FILES:
57
+ path = root / decoy
58
+ path.parent.mkdir(parents=True, exist_ok=True)
59
+ path.write_text("decoy\n", encoding="utf-8")
60
+ return root
61
+
62
+
63
+ def _build_sdist(root: Path, directory: Path) -> Path:
64
+ built = list(SdistBuilder(str(root)).build(directory=str(directory), versions=["standard"]))
65
+ assert len(built) == 1, f"expected one sdist, got {built}"
66
+ return Path(built[0])
67
+
68
+
69
+ def _strip_root(names: list[str]) -> list[str]:
70
+ """Drop the ``<name>-<version>/`` prefix every member carries."""
71
+ return [name.split("/", 1)[1] for name in names if "/" in name]
72
+
73
+
74
+ @pytest.fixture(scope="module")
75
+ def built_tree(tmp_path_factory: pytest.TempPathFactory) -> Path:
76
+ return _copy_tree_with_decoys(tmp_path_factory.mktemp("packaging"))
77
+
78
+
79
+ @pytest.fixture(scope="module")
80
+ def sdist_members(built_tree: Path, tmp_path_factory: pytest.TempPathFactory) -> list[str]:
81
+ sdist = _build_sdist(built_tree, tmp_path_factory.mktemp("sdist"))
82
+ with tarfile.open(sdist, "r:gz") as archive:
83
+ members = _strip_root(archive.getnames())
84
+ assert members, "the sdist is empty, so nothing below would be judged"
85
+ return members
86
+
87
+
88
+ @pytest.fixture(scope="module")
89
+ def wheel_members(built_tree: Path, tmp_path_factory: pytest.TempPathFactory) -> list[str]:
90
+ built = list(WheelBuilder(str(built_tree)).build(directory=str(tmp_path_factory.mktemp("wheel")), versions=["standard"]))
91
+ assert len(built) == 1, f"expected one wheel, got {built}"
92
+ with zipfile.ZipFile(built[0]) as archive:
93
+ members = archive.namelist()
94
+ assert members, "the wheel is empty, so nothing below would be judged"
95
+ return members
96
+
97
+
98
+ @pytest.mark.os_agnostic
99
+ def test_the_sdist_holds_only_allowlisted_top_level_entries(sdist_members: list[str]) -> None:
100
+ top_level = {member.split("/", 1)[0] for member in sdist_members}
101
+
102
+ unexpected = sorted(top_level - ALLOWED_TOP_LEVEL)
103
+ missing = sorted(REQUIRED_TOP_LEVEL - top_level)
104
+
105
+ assert not unexpected, f"the sdist ships files outside the allowlist: {unexpected}"
106
+ assert not missing, f"the sdist lacks required entries: {missing}"
107
+
108
+
109
+ @pytest.mark.os_agnostic
110
+ def test_the_sdist_ships_only_the_package_under_src(sdist_members: list[str]) -> None:
111
+ under_src = {member.split("/")[1] for member in sdist_members if member.startswith("src/") and member.count("/") >= 1}
112
+
113
+ assert under_src == {PACKAGE}
114
+
115
+
116
+ @pytest.mark.os_agnostic
117
+ def test_the_sdist_carries_no_compiled_bytecode(sdist_members: list[str]) -> None:
118
+ compiled = [member for member in sdist_members if "__pycache__" in member or member.endswith(".pyc")]
119
+
120
+ assert not compiled, f"the sdist ships bytecode: {compiled[:5]}"
121
+
122
+
123
+ @pytest.mark.os_agnostic
124
+ def test_the_wheel_carries_the_package_data_the_runtime_loads(wheel_members: list[str]) -> None:
125
+ missing = [name for name in REQUIRED_WHEEL_DATA if name not in wheel_members]
126
+
127
+ assert not missing, f"the wheel lacks package data: {missing}"
@@ -0,0 +1,31 @@
1
+ """The ``os_*`` markers skip a test off its platform; registering them alone would skip nothing.
2
+
3
+ Each test asserts the platform its own marker names, so it fails wherever conftest lets it run by
4
+ mistake, and passes where the marker lets it run.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ import pytest
12
+
13
+
14
+ @pytest.mark.os_windows
15
+ def test_an_os_windows_test_runs_only_on_windows() -> None:
16
+ assert sys.platform == "win32"
17
+
18
+
19
+ @pytest.mark.os_macos
20
+ def test_an_os_macos_test_runs_only_on_macos() -> None:
21
+ assert sys.platform == "darwin"
22
+
23
+
24
+ @pytest.mark.os_linux
25
+ def test_an_os_linux_test_runs_only_on_linux() -> None:
26
+ assert sys.platform.startswith("linux")
27
+
28
+
29
+ @pytest.mark.os_posix
30
+ def test_an_os_posix_test_never_runs_on_windows() -> None:
31
+ assert sys.platform != "win32"
@@ -1,22 +0,0 @@
1
- {
2
- "image": "mcr.microsoft.com/devcontainers/python:3.13",
3
-
4
- "postCreateCommand": "bash -lc '\nPY=/usr/local/bin/python;\n$PY -m pip install -U pip && \\\n$PY -m pip install ipykernel && \\\n$PY -m pip install -e . && \\\n# register kernel using this exact interpreter\n$PY -m ipykernel install --name=python3 --display-name=\"Python 3 (3.13)\" --user && \\\n# patch the notebook to point to the python3 kernelspec (VS Code will bind to /usr/local/bin/python)\n$PY - <<\"PY\"\nimport json, pathlib\np = pathlib.Path(\"notebooks/Quickstart.ipynb\")\nif p.exists():\n nb = json.loads(p.read_text(encoding=\"utf-8\"))\n md = nb.setdefault(\"metadata\", {})\n md[\"kernelspec\"] = {\n \"name\": \"python3\",\n \"display_name\": \"Python 3 (3.13)\",\n \"language\": \"python\"\n }\n md[\"language_info\"] = {\"name\": \"python\"}\n p.write_text(json.dumps(nb, ensure_ascii=False, indent=1), encoding=\"utf-8\")\n print(\"Pinned kernelspec to python3 in\", p)\nelse:\n print(\"Notebook not found:\", p)\nPY'\n",
5
-
6
- "customizations": {
7
- "vscode": {
8
- "extensions": ["ms-toolsai.jupyter", "ms-python.python"],
9
- "settings": {
10
- "workbench.startupEditor": "none",
11
- "jupyter.alwaysTrustNotebooks": true,
12
- "jupyter.kernelPickerType": "OnlyRecommended",
13
- "python.defaultInterpreterPath": "/usr/local/bin/python"
14
- }
15
- },
16
- "codespaces": {
17
- "openFiles": ["README.md"]
18
- }
19
- },
20
-
21
- "postAttachCommand": "bash -lc 'if [ -f notebooks/Quickstart.ipynb ]; then code -r notebooks/Quickstart.ipynb; fi'"
22
- }
@@ -1,6 +0,0 @@
1
- {
2
- "python.defaultInterpreterPath": "/usr/local/bin/python3.13",
3
- "jupyter.alwaysTrustNotebooks": true,
4
- "jupyter.kernelPickerType": "OnlyRecommended",
5
- "jupyter.askForKernelRestart": false
6
- }
@@ -1,11 +0,0 @@
1
- # Copy to .env and fill values as needed.
2
-
3
- # Codecov upload token (required for private repos; optional for public repos)
4
- CODECOV_TOKEN=
5
-
6
- # PyPI API token for release workflow (.github/workflows/release.yml)
7
- # Format: pypi-AgENdGVzdC5weXBpLm9yZwIk...
8
- PYPI_API_TOKEN=
9
-
10
- # Optional: GitHub token for API-limited tasks
11
- GITHUB_TOKEN=