Skip to content

Commit d2dddab

Browse files
committed
Migrate existing code from other projects to package repository
0 parents  commit d2dddab

23 files changed

+1408
-0
lines changed

.gitignore

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.pyd
5+
*.pyo
6+
*.pyz
7+
*.pyc
8+
*.so
9+
*.egg
10+
*.egg-info/
11+
dist/
12+
build/
13+
develop-eggs/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
MANIFEST
27+
28+
# PyCharm
29+
.idea/
30+
*.iml
31+
32+
# VS Code
33+
.vscode/
34+
35+
# Jupyter Notebook
36+
.ipynb_checkpoints
37+
38+
# Environment
39+
.env
40+
.venv/
41+
env/
42+
venv/
43+
ENV/
44+
45+
# Install
46+
*.log
47+
*.pot
48+
*.pyc
49+
*.pyo
50+
*.pyd
51+
*.so
52+
*.egg-info/
53+
dist/
54+
build/
55+
develop-eggs/
56+
downloads/
57+
eggs/
58+
.eggs/
59+
lib/
60+
lib64/
61+
parts/
62+
sdist/
63+
var/
64+
wheels/
65+
pip-wheel-metadata/
66+
share/python-wheels/
67+
*.egg-info/
68+
.installed.cfg
69+
*.egg
70+
MANIFEST
71+
72+
# Test
73+
.tox/
74+
coverage.xml
75+
.htmlcov/
76+
.pytest_cache/
77+
78+
# OS
79+
.DS_Store
80+
Thumbs.db

LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
BSD 2-Clause License
2+
3+
Copyright (c) 2024, Fabian Haenel
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Whitespace-only changes.

poetry.lock

Lines changed: 568 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
[tool.poetry]
2+
name = "pytest-parametrization-annotations"
3+
version = "0.1.0"
4+
description = "A pytest library for parametrizing tests with type hints."
5+
authors = ["Fabian Haenel <contact@fabian-haenel.io>"]
6+
readme = "README.md"
7+
packages = [{include = "pytest_parametrization_annotation", from = "src"}]
8+
classifiers = [
9+
# Status
10+
"Development Status :: 4 - Beta",
11+
12+
# License
13+
"License :: OSI Approved :: BSD License",
14+
15+
# Officially supported python versions
16+
"Programming Language :: Python :: 3 :: Only",
17+
"Programming Language :: Python :: 3.10",
18+
"Programming Language :: Python :: 3.11",
19+
"Programming Language :: Python :: 3.12",
20+
21+
# Topics
22+
"Topic :: Software Development :: Testing",
23+
"Topic :: Software Development :: Testing :: Unit",
24+
"Topic :: Utilities",
25+
26+
# Misc
27+
"Typing :: Typed",
28+
"Framework :: Pytest",
29+
"Intended Audience :: Developers",
30+
]
31+
32+
[tool.poetry.plugins."pytest11"]
33+
pytest-parametrization-annotation = "pytest_parametrization_annotation.plugin"
34+
35+
36+
[tool.poetry.dependencies]
37+
python = ">=3.10"
38+
pytest = ">=7"
39+
40+
41+
[tool.poetry.group.linting.dependencies]
42+
ruff = "v0.3.4"
43+
black = "v24.3"
44+
mypy = "^1.9.0"
45+
46+
[tool.poetry.group.testing.dependencies]
47+
pytest-cov = "*"
48+
jinja2 = ">=3"
49+
tox = "^4.14.2"
50+
51+
[build-system]
52+
requires = ["poetry-core"]
53+
build-backend = "poetry.core.masonry.api"
54+
55+
[tool.pytest.ini_options]
56+
python_files = ["test_*.py"]
57+
pythonpath = ["src"]
58+
testpaths = ["tests/"]
59+
markers = [
60+
"case: Define a test case based on type annotations",
61+
]
62+
63+
[tool.ruff]
64+
src = ["src"]
65+
exclude = [".py.j2"]
66+
67+
[tool.black]
68+
target-version = ["py310", "py311", "py312"]
69+
70+
[tool.mypy]
71+
python_version = "3.10"
72+
warn_return_any = true
73+
warn_unused_configs = true
74+
files = ["src", "tests"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .annotation import Parametrized
2+
from .exceptions import ParameterValueUndefined
3+
4+
__all__ = [
5+
"Parametrized",
6+
"ParameterValueUndefined",
7+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from dataclasses import dataclass
2+
from typing import Any, Callable
3+
4+
_Default = object()
5+
_DefaultCallable = lambda: None # noqa: E731
6+
7+
8+
@dataclass(slots=True, kw_only=True, frozen=True)
9+
class Parametrized:
10+
indirect: bool = False
11+
default: Any = _Default
12+
default_factory: Callable[[], Any] = _DefaultCallable
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class ParameterValueUndefined(Exception):
2+
def __init__(
3+
self,
4+
test: str,
5+
case: int | str,
6+
parameter: str,
7+
):
8+
self.test = test
9+
self.case = case
10+
self.parameter = parameter
11+
12+
def __str__(self):
13+
return f"{self.test} | Case '{self.case}': Failed to populate because the parameter '{self.parameter}' is not provided and default is not configured." # noqa: E501
14+
15+
def __repr__(self):
16+
return f"Test -> {self.test} | Case -> {self.case} | Field -> {self.parameter}"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from typing import Annotated, Any, Callable, Mapping, get_origin, get_type_hints
2+
3+
from _pytest.python import Metafunc
4+
5+
from pytest_parametrization_annotation.annotation import (
6+
Parametrized,
7+
_Default,
8+
_DefaultCallable,
9+
)
10+
from pytest_parametrization_annotation.exceptions import ParameterValueUndefined
11+
12+
13+
def get_parameters_from_type_hints(func: Callable) -> dict[str, Parametrized]:
14+
parameters = {}
15+
for name, type_hint in get_type_hints(func, include_extras=True).items():
16+
if get_origin(type_hint) is not Annotated:
17+
continue
18+
19+
# Check if is annotated as a parameter
20+
try:
21+
parameter = next(
22+
annotation
23+
for annotation in type_hint.__metadata__
24+
if annotation is Parametrized or isinstance(annotation, Parametrized)
25+
)
26+
except StopIteration:
27+
continue
28+
29+
parameters[name] = (
30+
parameter if isinstance(parameter, Parametrized) else Parametrized()
31+
)
32+
return parameters
33+
34+
35+
def get_parametrized_value(
36+
kwargs: Mapping[str, Any], parameter: str, meta: Parametrized
37+
) -> Any:
38+
if parameter in kwargs:
39+
return kwargs[parameter]
40+
if meta.default_factory is not _DefaultCallable:
41+
return meta.default_factory()
42+
if meta.default is not _Default:
43+
return meta.default
44+
raise KeyError(parameter)
45+
46+
47+
def register_parametrized_cases(metafunc: Metafunc):
48+
function_definition = metafunc.definition
49+
markers = [mark for mark in function_definition.iter_markers(name="case")]
50+
if markers:
51+
func = getattr(function_definition.module, function_definition.name)
52+
parameters = get_parameters_from_type_hints(func)
53+
54+
# Exit early if no parameters are annotated to be parametrized
55+
if not parameters:
56+
return
57+
58+
cases = []
59+
ids = []
60+
for i, marker in enumerate(markers):
61+
case_id = marker.args[0] if len(marker.args) > 0 else None
62+
ids.append(case_id)
63+
64+
try:
65+
cases.append(
66+
tuple(
67+
get_parametrized_value(marker.kwargs, parameter, meta)
68+
for parameter, meta in parameters.items()
69+
)
70+
)
71+
except KeyError as e:
72+
raise ParameterValueUndefined(
73+
function_definition.nodeid,
74+
case_id or i,
75+
e.args[0],
76+
) from e
77+
78+
metafunc.parametrize(
79+
tuple(parameters),
80+
cases,
81+
indirect=[
82+
name for name, parameter in parameters.items() if parameter.indirect
83+
],
84+
ids=ids,
85+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
from pytest_parametrization_annotation.functionality import register_parametrized_cases
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from _pytest.python import Metafunc
8+
from _pytest.config import Config
9+
10+
11+
def pytest_configure(config: Config) -> None:
12+
config.addinivalue_line("markers", "case: Parametrize the test with the provided values.")
13+
14+
15+
def pytest_generate_tests(metafunc: Metafunc) -> None:
16+
register_parametrized_cases(metafunc)

0 commit comments

Comments
 (0)