pytest-plugin-utils 0.1.0__tar.gz → 0.2.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.
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytest-plugin-utils
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Reusable configuration and artifact utilities for building pytest plugins
5
5
  Keywords: pytest,plugin,testing,utilities
6
6
  Author: Michael Bianco
7
7
  Author-email: Michael Bianco <mike@mikebian.co>
8
- Requires-Dist: structlog-config>=0.10.0
9
8
  Requires-Python: >=3.12
10
9
  Project-URL: Repository, https://github.com/iloveitaly/pytest-plugin-utils
11
10
  Description-Content-Type: text/markdown
@@ -31,7 +30,9 @@ uv add pytest-plugin-utils
31
30
 
32
31
  ### Configuration Options
33
32
 
34
- Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference:
33
+ Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference.
34
+
35
+ #### For Plugin Authors
35
36
 
36
37
  ```python
37
38
  from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
@@ -55,6 +56,38 @@ def pytest_configure(config):
55
56
  api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
56
57
  ```
57
58
 
59
+ #### For Plugin Users
60
+
61
+ Once a plugin has registered options using this package, users can configure them in three ways (in order of precedence):
62
+
63
+ 1. **Command Line** (highest priority):
64
+ ```bash
65
+ pytest --api-url=https://prod.example.com
66
+ ```
67
+
68
+ 2. **INI Configuration** (medium priority):
69
+
70
+ In `pytest.ini`:
71
+ ```ini
72
+ [pytest]
73
+ api_url = https://staging.example.com
74
+ ```
75
+
76
+ Or in `pyproject.toml`:
77
+ ```toml
78
+ [tool.pytest.ini_options]
79
+ api_url = "https://staging.example.com"
80
+ ```
81
+
82
+ 3. **Runtime/Programmatic** (via conftest.py):
83
+ ```python
84
+ def pytest_configure(config):
85
+ # Override at runtime
86
+ config.option.api_url = "https://custom.example.com"
87
+ ```
88
+
89
+ The value resolution follows this precedence chain, with each level overriding the next: Runtime > CLI > INI > Default.
90
+
58
91
  ### Artifact Directory Management
59
92
 
60
93
  Create per-test artifact directories with sanitized names:
@@ -81,6 +114,13 @@ def pytest_runtest_setup(item):
81
114
  * Per-test artifact directory creation and resolution
82
115
  * Type-safe configuration retrieval with warnings on mismatches
83
116
 
117
+ ## Related Projects
118
+
119
+ * [pytest-playwright-visual-snapshot](https://github.com/iloveitaly/pytest-playwright-visual-snapshot): Easy pytest visual regression testing using playwright.
120
+ * [pytest-line-runner](https://github.com/iloveitaly/pytest-line-runner): Run pytest tests by line number instead of exact test name.
121
+ * [pytest-celery-utils](https://github.com/iloveitaly/pytest-celery-utils): Pytest plugin for inspecting Celery task queues in Redis during tests.
122
+ * [pytest-playwright-artifacts](https://github.com/iloveitaly/pytest-playwright-artifacts): Pytest plugin that captures HTML, screenshots, and console logs on Playwright test failures.
123
+
84
124
  ## [MIT License](LICENSE.md)
85
125
 
86
126
  ---
@@ -19,7 +19,9 @@ uv add pytest-plugin-utils
19
19
 
20
20
  ### Configuration Options
21
21
 
22
- Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference:
22
+ Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference.
23
+
24
+ #### For Plugin Authors
23
25
 
24
26
  ```python
25
27
  from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
@@ -43,6 +45,38 @@ def pytest_configure(config):
43
45
  api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
44
46
  ```
45
47
 
48
+ #### For Plugin Users
49
+
50
+ Once a plugin has registered options using this package, users can configure them in three ways (in order of precedence):
51
+
52
+ 1. **Command Line** (highest priority):
53
+ ```bash
54
+ pytest --api-url=https://prod.example.com
55
+ ```
56
+
57
+ 2. **INI Configuration** (medium priority):
58
+
59
+ In `pytest.ini`:
60
+ ```ini
61
+ [pytest]
62
+ api_url = https://staging.example.com
63
+ ```
64
+
65
+ Or in `pyproject.toml`:
66
+ ```toml
67
+ [tool.pytest.ini_options]
68
+ api_url = "https://staging.example.com"
69
+ ```
70
+
71
+ 3. **Runtime/Programmatic** (via conftest.py):
72
+ ```python
73
+ def pytest_configure(config):
74
+ # Override at runtime
75
+ config.option.api_url = "https://custom.example.com"
76
+ ```
77
+
78
+ The value resolution follows this precedence chain, with each level overriding the next: Runtime > CLI > INI > Default.
79
+
46
80
  ### Artifact Directory Management
47
81
 
48
82
  Create per-test artifact directories with sanitized names:
@@ -69,6 +103,13 @@ def pytest_runtest_setup(item):
69
103
  * Per-test artifact directory creation and resolution
70
104
  * Type-safe configuration retrieval with warnings on mismatches
71
105
 
106
+ ## Related Projects
107
+
108
+ * [pytest-playwright-visual-snapshot](https://github.com/iloveitaly/pytest-playwright-visual-snapshot): Easy pytest visual regression testing using playwright.
109
+ * [pytest-line-runner](https://github.com/iloveitaly/pytest-line-runner): Run pytest tests by line number instead of exact test name.
110
+ * [pytest-celery-utils](https://github.com/iloveitaly/pytest-celery-utils): Pytest plugin for inspecting Celery task queues in Redis during tests.
111
+ * [pytest-playwright-artifacts](https://github.com/iloveitaly/pytest-playwright-artifacts): Pytest plugin that captures HTML, screenshots, and console logs on Playwright test failures.
112
+
72
113
  ## [MIT License](LICENSE.md)
73
114
 
74
115
  ---
@@ -1,18 +1,18 @@
1
1
  [project]
2
2
  name = "pytest-plugin-utils"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Reusable configuration and artifact utilities for building pytest plugins"
5
5
  keywords = ["pytest", "plugin", "testing", "utilities"]
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.12"
8
- dependencies = ["structlog-config>=0.10.0"]
8
+ dependencies = []
9
9
  authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
10
10
  urls = { "Repository" = "https://github.com/iloveitaly/pytest-plugin-utils" }
11
11
 
12
12
  # additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
13
13
 
14
14
  [build-system]
15
- requires = ["uv_build>=0.10.0"]
15
+ requires = ["uv_build>=0.10.0,<0.11"]
16
16
  build-backend = "uv_build"
17
17
 
18
18
  [tool.uv.build-backend]
@@ -30,7 +30,7 @@ dev = [
30
30
  ]
31
31
 
32
32
  [tool.pyright]
33
- exclude = ["examples/", "playground/", "tmp/", ".venv/", "tests/"]
33
+ exclude = ["examples/", "playground/", "tmp/", ".venv/"]
34
34
 
35
35
  [tool.pytest.ini_options]
36
36
  addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
@@ -1,7 +1,6 @@
1
1
  from pytest_plugin_utils.artifacts import (
2
2
  get_artifact_dir as get_artifact_dir,
3
3
  sanitize_for_artifacts as sanitize_for_artifacts,
4
- set_artifact_dir_option as set_artifact_dir_option,
5
4
  )
6
5
  from pytest_plugin_utils.config import (
7
6
  get_pytest_option as get_pytest_option,
@@ -0,0 +1,64 @@
1
+ """
2
+ Path handling utilities for pytest artifact management.
3
+
4
+ This module contains logic for determining where artifacts should be stored
5
+ for individual tests, including sanitization of test names and resolution
6
+ of output directories.
7
+ """
8
+
9
+ import re
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+
15
+ def sanitize_for_artifacts(text: str) -> str:
16
+ """
17
+ Sanitize a test nodeid or name for use as a directory name.
18
+
19
+ This function replaces characters that are not alphanumeric or hyphens
20
+ with a single hyphen, and removes leading/trailing hyphens. This ensures
21
+ that the resulting string is safe to use as a directory name on most
22
+ file systems.
23
+
24
+ Example:
25
+ >>> sanitize_for_artifacts("test_file.py::test_func[param]")
26
+ 'test-file-py-test-func-param'
27
+
28
+ Args:
29
+ text: The text to sanitize (e.g., a test nodeid).
30
+
31
+ Returns:
32
+ A sanitized string safe for use as a directory name.
33
+ """
34
+ sanitized = re.sub(r"[^A-Za-z0-9]+", "-", text)
35
+ sanitized = re.sub(r"-+", "-", sanitized).strip("-")
36
+ return sanitized or "unknown-test"
37
+
38
+
39
+ def get_artifact_dir(
40
+ item: pytest.Item, base_dir: Path, *, create: bool = False
41
+ ) -> Path:
42
+ """
43
+ Get or create the artifact directory for a specific test item.
44
+
45
+ This function determines the subdirectory for the specific test item
46
+ using its sanitized nodeid, relative to the provided base_dir.
47
+
48
+ Args:
49
+ item: The pytest.Item (test case) for which to get the directory.
50
+ base_dir: The root output directory for artifacts.
51
+ create: If True, creates the artifact directory and its parents if they do not exist.
52
+
53
+ Returns:
54
+ A pathlib.Path object pointing to the specific test's artifact directory.
55
+ """
56
+ if create:
57
+ base_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ per_test_dir = base_dir / sanitize_for_artifacts(item.nodeid)
60
+
61
+ if create:
62
+ per_test_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ return per_test_dir
@@ -10,11 +10,11 @@ import warnings
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
12
 
13
- import structlog
13
+ import logging
14
14
  from _pytest.config import Config
15
15
  from _pytest.config.argparsing import Parser
16
16
 
17
- log = structlog.get_logger(logger_name=__package__)
17
+ log = logging.getLogger(__package__)
18
18
 
19
19
 
20
20
  @dataclass
@@ -136,7 +136,12 @@ def register_pytest_options(namespace: str, parser: Parser) -> None:
136
136
  if opt.available in ("all", "cli_option"):
137
137
  cli_name = f"--{opt.name.replace('_', '-')}"
138
138
  # CRITICAL: We set default=None here so CLI allows fallback to INI/Runtime
139
- parser.addoption(cli_name, action="store", default=None, help=help_text)
139
+ if opt.type_hint is bool:
140
+ parser.addoption(
141
+ cli_name, action="store_true", default=None, help=help_text
142
+ )
143
+ else:
144
+ parser.addoption(cli_name, action="store", default=None, help=help_text)
140
145
 
141
146
  # INI Registration
142
147
  if opt.available in ("all", "ini"):
@@ -150,7 +155,7 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
150
155
  This handles cases where CLI arguments (always strings) need conversion,
151
156
  or where default values might not match the strict type.
152
157
  """
153
- log.debug("casting value", raw_value=value, target_type=type_hint)
158
+ log.debug("casting value raw_value=%s target_type=%s", value, type_hint)
154
159
 
155
160
  if type_hint is None:
156
161
  return value
@@ -173,14 +178,14 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
173
178
  # Casting logic for strings (from CLI or raw defaults)
174
179
  if type_hint is bool and isinstance(value, str):
175
180
  result = value.lower() in ("true", "1", "yes", "on")
176
- log.debug("converted string to bool", converted_value=result)
181
+ log.debug("converted string to bool converted_value=%s", result)
177
182
  return result
178
183
 
179
184
  if origin is list and isinstance(value, str):
180
185
  # list("foo") produces ['f', 'o', 'o'], so handle string-to-list specially
181
186
  # by splitting on newlines (CLI args or raw strings from config)
182
187
  result = [v.strip() for v in value.splitlines() if v.strip()]
183
- log.debug("converted string to list", converted_value=result)
188
+ log.debug("converted string to list converted_value=%s", result)
184
189
  return result
185
190
 
186
191
  # Generic fallback: call type_hint(value) as constructor
@@ -189,18 +194,30 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
189
194
  result = t.cast(type, origin)(value)
190
195
  else:
191
196
  result = t.cast(type, type_hint)(value)
192
- log.debug("converted using type constructor", converted_value=result)
197
+ log.debug("converted using type constructor converted_value=%s", result)
193
198
  return result
194
199
  except (TypeError, ValueError) as e:
195
- log.debug("failed to convert value", error=str(e))
200
+ log.debug("failed to convert value error=%s", str(e))
196
201
  raise TypeError(
197
202
  f"Cannot cast value of type {type(value)} to {type_hint}"
198
203
  ) from e
199
204
 
200
205
 
206
+ @t.overload
201
207
  def get_pytest_option[T](
202
- namespace: str, config: Config, key: str, *, type_hint: type[T] | None = None
203
- ) -> T | t.Any | None:
208
+ namespace: str, config: Config, key: str, *, type_hint: type[T]
209
+ ) -> T | None: ...
210
+
211
+
212
+ @t.overload
213
+ def get_pytest_option(
214
+ namespace: str, config: Config, key: str, *, type_hint: None = None
215
+ ) -> t.Any | None: ...
216
+
217
+
218
+ def get_pytest_option(
219
+ namespace: str, config: Config, key: str, *, type_hint: t.Any | None = None
220
+ ) -> t.Any | None:
204
221
  """
205
222
  Retrieve a configuration value from runtime overrides, CLI, or INI files.
206
223
 
@@ -219,7 +236,10 @@ def get_pytest_option[T](
219
236
  The resolved value, optionally casted. Returns None if not found.
220
237
  """
221
238
  log.debug(
222
- "getting pytest option", namespace=namespace, key=key, type_hint=type_hint
239
+ "getting pytest option namespace=%s key=%s type_hint=%s",
240
+ namespace,
241
+ key,
242
+ type_hint,
223
243
  )
224
244
 
225
245
  normalized_key = key.replace("-", "_")
@@ -261,7 +281,7 @@ def get_pytest_option[T](
261
281
  val = opt.default
262
282
  source = "default"
263
283
 
264
- log.debug("resolved raw value", key=key, raw_value=val, source=source)
284
+ log.debug("resolved raw value key=%s raw_value=%s source=%s", key, val, source)
265
285
 
266
286
  # Determine effective type hint
267
287
  effective_type_hint = type_hint
@@ -272,16 +292,18 @@ def get_pytest_option[T](
272
292
  if val is not None and effective_type_hint is not None:
273
293
  try:
274
294
  result = _smart_cast(val, effective_type_hint)
275
- log.debug("returning converted value", key=key, converted_value=result)
295
+ log.debug(
296
+ "returning converted value key=%s converted_value=%s", key, result
297
+ )
276
298
  return result
277
299
  except TypeError as e:
278
300
  # warning? or just return val?
279
301
  # Let's log a warning and return val to be safe
280
302
  warnings.warn(f"Failed to cast option '{key}': {e}")
281
303
  log.debug(
282
- "returning raw value after conversion failure", key=key, value=val
304
+ "returning raw value after conversion failure key=%s value=%s", key, val
283
305
  )
284
306
  return val
285
307
 
286
- log.debug("returning raw value", key=key, value=val)
308
+ log.debug("returning raw value key=%s value=%s", key, val)
287
309
  return val
@@ -1,119 +0,0 @@
1
- """
2
- Path handling utilities for pytest artifact management.
3
-
4
- This module contains logic for determining where artifacts should be stored
5
- for individual tests, including sanitization of test names and resolution
6
- of output directories. The artifact directory option name can be customized
7
- via set_artifact_dir_option().
8
- """
9
-
10
- import re
11
- from pathlib import Path
12
-
13
- import pytest
14
-
15
- from .config import get_pytest_option
16
-
17
- _artifact_dir_options: dict[str, str] = {}
18
-
19
-
20
- def set_artifact_dir_option(namespace: str, option_name: str) -> None:
21
- """
22
- Set the pytest option name used for the artifact output directory.
23
-
24
- This function should typically be called in pytest_configure() to customize
25
- the option name before any tests run. It allows this module to be reused
26
- by other pytest plugins that need different option names.
27
-
28
- Example:
29
- # In your conftest.py or plugin module:
30
- from pytest_plugin_utils.artifacts import set_artifact_dir_option
31
- from pytest_plugin_utils.config import set_pytest_option
32
-
33
- def pytest_configure(config):
34
- # Register your custom option
35
- set_pytest_option(
36
- __package__,
37
- "my_artifacts_output",
38
- default="my-test-results",
39
- help="Directory for test artifacts",
40
- available="cli_option",
41
- type_hint=str,
42
- )
43
- # Configure paths module to use it
44
- set_artifact_dir_option(__package__, "my_artifacts_output")
45
-
46
- Args:
47
- namespace: Unique namespace for this plugin (typically __package__).
48
- option_name: The pytest option name (without '--' prefix, with underscores).
49
- """
50
- _artifact_dir_options[namespace] = option_name
51
-
52
-
53
- def get_artifact_dir_option(namespace: str) -> str:
54
- """
55
- Get the currently configured artifact directory option name.
56
-
57
- Args:
58
- namespace: Unique namespace for this plugin (typically __package__).
59
-
60
- Returns:
61
- The pytest option name used for the artifact output directory.
62
- """
63
- assert namespace in _artifact_dir_options, (
64
- f"call set_artifact_dir_option({namespace!r}, ...) before using get_artifact_dir_option()"
65
- )
66
- return _artifact_dir_options[namespace]
67
-
68
-
69
- def sanitize_for_artifacts(text: str) -> str:
70
- """
71
- Sanitize a test nodeid or name for use as a directory name.
72
-
73
- This function replaces characters that are not alphanumeric or hyphens
74
- with a single hyphen, and removes leading/trailing hyphens. This ensures
75
- that the resulting string is safe to use as a directory name on most
76
- file systems.
77
-
78
- Example:
79
- >>> sanitize_for_artifacts("test_file.py::test_func[param]")
80
- 'test-file-py-test-func-param'
81
-
82
- Args:
83
- text: The text to sanitize (e.g., a test nodeid).
84
-
85
- Returns:
86
- A sanitized string safe for use as a directory name.
87
- """
88
- sanitized = re.sub(r"[^A-Za-z0-9]+", "-", text)
89
- sanitized = re.sub(r"-+", "-", sanitized).strip("-")
90
- return sanitized or "unknown-test"
91
-
92
-
93
- def get_artifact_dir(namespace: str, item: pytest.Item) -> Path:
94
- """
95
- Get or create the artifact directory for a specific test item.
96
-
97
- This function determines the root output directory based on the configured
98
- artifact directory option (see set_artifact_dir_option). It then creates
99
- a subdirectory for the specific test item using its sanitized nodeid.
100
-
101
- Args:
102
- namespace: Unique namespace for this plugin (typically __package__).
103
- item: The pytest.Item (test case) for which to get the directory.
104
-
105
- Returns:
106
- A pathlib.Path object pointing to the specific test's artifact directory.
107
- The directory and its parents are created if they do not exist.
108
- """
109
- assert namespace in _artifact_dir_options, (
110
- f"call set_artifact_dir_option({namespace!r}, ...) before using get_artifact_dir()"
111
- )
112
- option_name = _artifact_dir_options[namespace]
113
- output_path = get_pytest_option(namespace, item.config, option_name, type_hint=Path)
114
- assert output_path
115
- output_path.mkdir(parents=True, exist_ok=True)
116
-
117
- per_test_dir = output_path / sanitize_for_artifacts(item.nodeid)
118
- per_test_dir.mkdir(parents=True, exist_ok=True)
119
- return per_test_dir