pytest-plugin-utils 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-plugin-utils
3
+ Version: 0.1.0
4
+ Summary: Reusable configuration and artifact utilities for building pytest plugins
5
+ Keywords: pytest,plugin,testing,utilities
6
+ Author: Michael Bianco
7
+ Author-email: Michael Bianco <mike@mikebian.co>
8
+ Requires-Dist: structlog-config>=0.10.0
9
+ Requires-Python: >=3.12
10
+ Project-URL: Repository, https://github.com/iloveitaly/pytest-plugin-utils
11
+ Description-Content-Type: text/markdown
12
+
13
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/pytest-plugin-utils)](https://github.com/iloveitaly/pytest-plugin-utils/releases)
14
+ [![Downloads](https://static.pepy.tech/badge/pytest-plugin-utils/month)](https://pepy.tech/project/pytest-plugin-utils)
15
+ ![GitHub CI Status](https://github.com/iloveitaly/pytest-plugin-utils/actions/workflows/build_and_publish.yml/badge.svg)
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+ # Reusable pytest Plugin Utilities
19
+
20
+ Building pytest plugins means dealing with the same problems repeatedly: managing configuration options with proper precedence (CLI vs INI vs defaults), creating per-test artifact directories, and sanitizing test names for filesystem paths. This package extracts those common patterns into reusable utilities.
21
+
22
+ I created this after extracting the config and path handling logic from `pytest-playwright-artifacts`. Rather than reinvent option handling in every plugin, you can use these utilities to get consistent behavior across pytest plugins.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ uv add pytest-plugin-utils
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Configuration Options
33
+
34
+ Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference:
35
+
36
+ ```python
37
+ from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
38
+
39
+ def pytest_addoption(parser):
40
+ # Define your options (use __package__ for namespace)
41
+ set_pytest_option(
42
+ __package__,
43
+ "api_url",
44
+ default="http://localhost:3000",
45
+ help="API base URL",
46
+ available="all", # Expose via CLI and INI
47
+ type_hint=str,
48
+ )
49
+
50
+ # Register them with pytest
51
+ register_pytest_options(__package__, parser)
52
+
53
+ def pytest_configure(config):
54
+ # Retrieve with automatic type casting
55
+ api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
56
+ ```
57
+
58
+ ### Artifact Directory Management
59
+
60
+ Create per-test artifact directories with sanitized names:
61
+
62
+ ```python
63
+ from pytest_plugin_utils import set_artifact_dir_option, get_artifact_dir
64
+
65
+ def pytest_configure(config):
66
+ # Configure which option name to use (use __package__ for namespace)
67
+ set_artifact_dir_option(__package__, "my_plugin_output")
68
+
69
+ def pytest_runtest_setup(item):
70
+ # Get a clean directory for this specific test
71
+ artifact_dir = get_artifact_dir(__package__, item)
72
+ # Returns: /output/test-file-py-test-name-param/
73
+ ```
74
+
75
+ ## Features
76
+
77
+ * Centralized option registry with runtime, CLI, and INI support
78
+ * Automatic INI type inference from Python type hints (bool, str, list[str], list[Path])
79
+ * Smart value casting with fallback precedence handling
80
+ * Filesystem-safe test name sanitization for artifact paths
81
+ * Per-test artifact directory creation and resolution
82
+ * Type-safe configuration retrieval with warnings on mismatches
83
+
84
+ ## [MIT License](LICENSE.md)
85
+
86
+ ---
87
+
88
+ *This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
89
+
@@ -0,0 +1,77 @@
1
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/pytest-plugin-utils)](https://github.com/iloveitaly/pytest-plugin-utils/releases)
2
+ [![Downloads](https://static.pepy.tech/badge/pytest-plugin-utils/month)](https://pepy.tech/project/pytest-plugin-utils)
3
+ ![GitHub CI Status](https://github.com/iloveitaly/pytest-plugin-utils/actions/workflows/build_and_publish.yml/badge.svg)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ # Reusable pytest Plugin Utilities
7
+
8
+ Building pytest plugins means dealing with the same problems repeatedly: managing configuration options with proper precedence (CLI vs INI vs defaults), creating per-test artifact directories, and sanitizing test names for filesystem paths. This package extracts those common patterns into reusable utilities.
9
+
10
+ I created this after extracting the config and path handling logic from `pytest-playwright-artifacts`. Rather than reinvent option handling in every plugin, you can use these utilities to get consistent behavior across pytest plugins.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ uv add pytest-plugin-utils
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Configuration Options
21
+
22
+ Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference:
23
+
24
+ ```python
25
+ from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
26
+
27
+ def pytest_addoption(parser):
28
+ # Define your options (use __package__ for namespace)
29
+ set_pytest_option(
30
+ __package__,
31
+ "api_url",
32
+ default="http://localhost:3000",
33
+ help="API base URL",
34
+ available="all", # Expose via CLI and INI
35
+ type_hint=str,
36
+ )
37
+
38
+ # Register them with pytest
39
+ register_pytest_options(__package__, parser)
40
+
41
+ def pytest_configure(config):
42
+ # Retrieve with automatic type casting
43
+ api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
44
+ ```
45
+
46
+ ### Artifact Directory Management
47
+
48
+ Create per-test artifact directories with sanitized names:
49
+
50
+ ```python
51
+ from pytest_plugin_utils import set_artifact_dir_option, get_artifact_dir
52
+
53
+ def pytest_configure(config):
54
+ # Configure which option name to use (use __package__ for namespace)
55
+ set_artifact_dir_option(__package__, "my_plugin_output")
56
+
57
+ def pytest_runtest_setup(item):
58
+ # Get a clean directory for this specific test
59
+ artifact_dir = get_artifact_dir(__package__, item)
60
+ # Returns: /output/test-file-py-test-name-param/
61
+ ```
62
+
63
+ ## Features
64
+
65
+ * Centralized option registry with runtime, CLI, and INI support
66
+ * Automatic INI type inference from Python type hints (bool, str, list[str], list[Path])
67
+ * Smart value casting with fallback precedence handling
68
+ * Filesystem-safe test name sanitization for artifact paths
69
+ * Per-test artifact directory creation and resolution
70
+ * Type-safe configuration retrieval with warnings on mismatches
71
+
72
+ ## [MIT License](LICENSE.md)
73
+
74
+ ---
75
+
76
+ *This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
77
+
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "pytest-plugin-utils"
3
+ version = "0.1.0"
4
+ description = "Reusable configuration and artifact utilities for building pytest plugins"
5
+ keywords = ["pytest", "plugin", "testing", "utilities"]
6
+ readme = "README.md"
7
+ requires-python = ">=3.12"
8
+ dependencies = ["structlog-config>=0.10.0"]
9
+ authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
10
+ urls = { "Repository" = "https://github.com/iloveitaly/pytest-plugin-utils" }
11
+
12
+ # additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.10.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [tool.uv.build-backend]
19
+ # avoids the src/ directory structure
20
+ module-root = ""
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pytest>=8.3.4",
25
+ "pyright[nodejs]>=1.1.408",
26
+ "ruff>=0.15.0",
27
+ "coverage>=7.13.4",
28
+ "pytest-cov>=7.0.0",
29
+ "covdefaults>=2.3.0",
30
+ ]
31
+
32
+ [tool.pyright]
33
+ exclude = ["examples/", "playground/", "tmp/", ".venv/", "tests/"]
34
+
35
+ [tool.pytest.ini_options]
36
+ addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
37
+
38
+ [tool.coverage.run]
39
+ plugins = ["covdefaults"]
40
+ source = ["pytest_plugin_utils"]
41
+
42
+ [tool.coverage.report]
43
+ fail_under = 50
44
+
45
+ [tool.ruff]
46
+ extend-exclude = ["playground.py", "playground/"]
@@ -0,0 +1,10 @@
1
+ from pytest_plugin_utils.artifacts import (
2
+ get_artifact_dir as get_artifact_dir,
3
+ sanitize_for_artifacts as sanitize_for_artifacts,
4
+ set_artifact_dir_option as set_artifact_dir_option,
5
+ )
6
+ from pytest_plugin_utils.config import (
7
+ get_pytest_option as get_pytest_option,
8
+ register_pytest_options as register_pytest_options,
9
+ set_pytest_option as set_pytest_option,
10
+ )
@@ -0,0 +1,119 @@
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
@@ -0,0 +1,287 @@
1
+ """
2
+ Pytest option registry and resolution helpers for this plugin.
3
+
4
+ Options are registered once, then resolved at read time with a consistent
5
+ precedence: runtime overrides > INI > defaults from the registry.
6
+ """
7
+
8
+ import typing as t
9
+ import warnings
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ import structlog
14
+ from _pytest.config import Config
15
+ from _pytest.config.argparsing import Parser
16
+
17
+ log = structlog.get_logger(logger_name=__package__)
18
+
19
+
20
+ @dataclass
21
+ class OptionDef:
22
+ """
23
+ Internal representation of the options this plugin wants to expose to pytest.
24
+ """
25
+
26
+ name: str
27
+ default: t.Any
28
+ help_text: str
29
+ available: t.Literal["all", "ini", "cli_option", None]
30
+ type_hint: t.Any | None
31
+ ini_type: (
32
+ t.Literal[
33
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
34
+ ]
35
+ | None
36
+ )
37
+
38
+
39
+ REGISTRY: dict[str, list[OptionDef]] = {}
40
+ "configuration options this plugin wants to expose to pytest, keyed by namespace"
41
+
42
+
43
+ def _infer_ini_type(
44
+ type_hint: t.Any,
45
+ ) -> (
46
+ t.Literal["string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"]
47
+ | None
48
+ ):
49
+ """
50
+ Infer the pytest INI type string from a Python type hint.
51
+
52
+ Supported mappings:
53
+ - bool -> "bool"
54
+ - str -> "string"
55
+ - list[str] -> "linelist"
56
+ - list[Path] -> "paths"
57
+
58
+ Unsupported/Not inferred:
59
+ - "args" (list of whitespace-separated strings)
60
+ - "pathlist" (legacy alias for "paths")
61
+ """
62
+ if type_hint is bool:
63
+ return "bool"
64
+ if type_hint is str:
65
+ return "string"
66
+
67
+ origin = t.get_origin(type_hint)
68
+ args = t.get_args(type_hint)
69
+
70
+ if origin is list:
71
+ if args and args[0] is str:
72
+ return "linelist"
73
+ if args and issubclass(args[0], Path):
74
+ return "paths"
75
+
76
+ return None
77
+
78
+
79
+ def set_pytest_option(
80
+ namespace: str,
81
+ name: str,
82
+ *,
83
+ default: t.Any = None,
84
+ help: str = "",
85
+ available: t.Literal["all", "ini", "cli_option", None] = None,
86
+ type_hint: t.Any | None = None,
87
+ ) -> None:
88
+ """
89
+ Define a pytest option.
90
+
91
+ This queues the option for registration (hook_addoption) and
92
+ configuration (hook_configure).
93
+
94
+ Args:
95
+ namespace: Unique namespace for this plugin (typically __package__).
96
+ name: The key name (e.g. "api_url"). Use underscores.
97
+ default: The fallback value if not provided via CLI or INI.
98
+ help: Help text for the CLI/INI description.
99
+ available: Where this option should be exposed to the user.
100
+ - 'cli_option': Adds a --flag.
101
+ - 'ini': Adds a value to pytest.ini.
102
+ - 'all': Adds both.
103
+ - None: Purely internal/runtime (set via code only).
104
+ type_hint: Optional Python type hint (e.g. bool, list[str]) used for
105
+ validation and INI type inference.
106
+ """
107
+ ini_type = _infer_ini_type(type_hint)
108
+ if namespace not in REGISTRY:
109
+ REGISTRY[namespace] = []
110
+ REGISTRY[namespace].append(
111
+ OptionDef(
112
+ name=name,
113
+ default=default,
114
+ help_text=help,
115
+ available=available,
116
+ type_hint=type_hint,
117
+ ini_type=ini_type,
118
+ )
119
+ )
120
+
121
+
122
+ def register_pytest_options(namespace: str, parser: Parser) -> None:
123
+ """
124
+ Must be called within `pytest_addoption` to register CLI/INI flags.
125
+
126
+ Args:
127
+ namespace: Unique namespace for this plugin (typically __package__).
128
+ parser: The pytest parser to register options with.
129
+ """
130
+ for opt in REGISTRY.get(namespace, []):
131
+ help_text = opt.help_text
132
+ if opt.default is not None:
133
+ help_text = f"{opt.help_text} (default: {opt.default})"
134
+
135
+ # CLI Registration
136
+ if opt.available in ("all", "cli_option"):
137
+ cli_name = f"--{opt.name.replace('_', '-')}"
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)
140
+
141
+ # INI Registration
142
+ if opt.available in ("all", "ini"):
143
+ # We set default=None here so INI allows fallback to Runtime default
144
+ parser.addini(opt.name, help=help_text, default=None, type=opt.ini_type)
145
+
146
+
147
+ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
148
+ """
149
+ Cast a value to the expected type if it's not already correct.
150
+ This handles cases where CLI arguments (always strings) need conversion,
151
+ or where default values might not match the strict type.
152
+ """
153
+ log.debug("casting value", raw_value=value, target_type=type_hint)
154
+
155
+ if type_hint is None:
156
+ return value
157
+
158
+ # Handle GenericAlias types (e.g. list[str]) for isinstance checks
159
+ origin = t.get_origin(type_hint)
160
+ check_type = origin if origin is not None else type_hint
161
+
162
+ try:
163
+ if isinstance(value, check_type):
164
+ log.debug("value already correct type, no conversion needed")
165
+ return value
166
+ except TypeError:
167
+ # Fallback if isinstance fails (e.g. some complex types)
168
+ pass
169
+
170
+ if value is None:
171
+ return None
172
+
173
+ # Casting logic for strings (from CLI or raw defaults)
174
+ if type_hint is bool and isinstance(value, str):
175
+ result = value.lower() in ("true", "1", "yes", "on")
176
+ log.debug("converted string to bool", converted_value=result)
177
+ return result
178
+
179
+ if origin is list and isinstance(value, str):
180
+ # list("foo") produces ['f', 'o', 'o'], so handle string-to-list specially
181
+ # by splitting on newlines (CLI args or raw strings from config)
182
+ result = [v.strip() for v in value.splitlines() if v.strip()]
183
+ log.debug("converted string to list", converted_value=result)
184
+ return result
185
+
186
+ # Generic fallback: call type_hint(value) as constructor
187
+ try:
188
+ if origin is not None:
189
+ result = t.cast(type, origin)(value)
190
+ else:
191
+ result = t.cast(type, type_hint)(value)
192
+ log.debug("converted using type constructor", converted_value=result)
193
+ return result
194
+ except (TypeError, ValueError) as e:
195
+ log.debug("failed to convert value", error=str(e))
196
+ raise TypeError(
197
+ f"Cannot cast value of type {type(value)} to {type_hint}"
198
+ ) from e
199
+
200
+
201
+ def get_pytest_option[T](
202
+ namespace: str, config: Config, key: str, *, type_hint: type[T] | None = None
203
+ ) -> T | t.Any | None:
204
+ """
205
+ Retrieve a configuration value from runtime overrides, CLI, or INI files.
206
+
207
+ Priority chain:
208
+ 1. Runtime overrides (via config.option in pytest_configure)
209
+ 2. CLI arguments (e.g., --my-key)
210
+ 3. Configuration files (pytest.ini, pyproject.toml)
211
+
212
+ Args:
213
+ namespace: Unique namespace for this plugin (typically __package__).
214
+ config: The pytest Config object.
215
+ key: The option name (use underscores).
216
+ type_hint: Optional expected type for validation and smart casting.
217
+
218
+ Returns:
219
+ The resolved value, optionally casted. Returns None if not found.
220
+ """
221
+ log.debug(
222
+ "getting pytest option", namespace=namespace, key=key, type_hint=type_hint
223
+ )
224
+
225
+ normalized_key = key.replace("-", "_")
226
+ opt = next(
227
+ (
228
+ entry
229
+ for entry in REGISTRY.get(namespace, [])
230
+ if entry.name == normalized_key
231
+ ),
232
+ None,
233
+ )
234
+
235
+ # Validation
236
+ if type_hint is not None and opt is not None and opt.type_hint is not None:
237
+ if type_hint != opt.type_hint:
238
+ warnings.warn(
239
+ f"Type mismatch for option '{key}': requested {type_hint}, configured {opt.type_hint}"
240
+ )
241
+
242
+ # CLI/runtime value from config.option (argparse Namespace)
243
+ val = getattr(config.option, normalized_key, None)
244
+ source = None
245
+
246
+ if val in (None, ""):
247
+ # INI value from pytest.ini or pyproject.toml
248
+ try:
249
+ val = config.getini(normalized_key)
250
+ if val not in (None, ""):
251
+ source = "ini"
252
+ except (ValueError, KeyError):
253
+ val = None
254
+
255
+ else:
256
+ source = "cli"
257
+
258
+ if val in (None, ""):
259
+ # Default value from the registry
260
+ if opt is not None:
261
+ val = opt.default
262
+ source = "default"
263
+
264
+ log.debug("resolved raw value", key=key, raw_value=val, source=source)
265
+
266
+ # Determine effective type hint
267
+ effective_type_hint = type_hint
268
+ if effective_type_hint is None and opt is not None:
269
+ effective_type_hint = opt.type_hint
270
+
271
+ # Smart cast
272
+ if val is not None and effective_type_hint is not None:
273
+ try:
274
+ result = _smart_cast(val, effective_type_hint)
275
+ log.debug("returning converted value", key=key, converted_value=result)
276
+ return result
277
+ except TypeError as e:
278
+ # warning? or just return val?
279
+ # Let's log a warning and return val to be safe
280
+ warnings.warn(f"Failed to cast option '{key}': {e}")
281
+ log.debug(
282
+ "returning raw value after conversion failure", key=key, value=val
283
+ )
284
+ return val
285
+
286
+ log.debug("returning raw value", key=key, value=val)
287
+ return val