Skip to content

Commit 7c6503b

Browse files
authored
Merge pull request #61 from 56kyle/develop
Prepping master for release
2 parents 046ed57 + dbd7032 commit 7c6503b

File tree

16 files changed

+2229
-873
lines changed

16 files changed

+2229
-873
lines changed

.flake8

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[flake8]
22
select = B,B9,C,D,DAR,E,F,N,RST,S,W
3-
ignore = E203,E501,RST201,RST203,RST301,W503
3+
ignore = E203,E501,RST201,RST203,RST301,W503,B905
44
max-line-length = 80
55
max-complexity = 10
66
docstring-convention = google
7-
per-file-ignores = tests/*:S101
7+
per-file-ignores = tests/*:S101,D100,D101,D102,D103,D104
88
rst-roles = class,const,func,meth,mod,ref
99
rst-directives = deprecated

.github/workflows/tests.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ jobs:
1717
- { python: "3.10", os: "ubuntu-latest", session: "mypy" }
1818
- { python: "3.9", os: "ubuntu-latest", session: "mypy" }
1919
- { python: "3.8", os: "ubuntu-latest", session: "mypy" }
20-
- { python: "3.7", os: "ubuntu-latest", session: "mypy" }
2120
- { python: "3.10", os: "ubuntu-latest", session: "tests" }
2221
- { python: "3.9", os: "ubuntu-latest", session: "tests" }
2322
- { python: "3.8", os: "ubuntu-latest", session: "tests" }
24-
- { python: "3.7", os: "ubuntu-latest", session: "tests" }
2523
- { python: "3.10", os: "windows-latest", session: "tests" }
2624
- { python: "3.10", os: "macos-latest", session: "tests" }
2725
- { python: "3.10", os: "ubuntu-latest", session: "typeguard" }

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
package = "pytest_static"
26-
python_versions = ["3.10", "3.9", "3.8", "3.7"]
26+
python_versions = ["3.10", "3.9", "3.8"]
2727
nox.needs_version = ">= 2021.6.6"
2828
nox.options.sessions = (
2929
"pre-commit",

poetry.lock

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

pyproject.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@ repository = "https://github.com/56kyle/pytest-static"
1010
documentation = "https://pytest-static.readthedocs.io"
1111
classifiers = [
1212
"Development Status :: 1 - Planning",
13+
"Programming Language :: Python :: 3",
14+
"Programming Language :: Python :: 3.8",
15+
"Programming Language :: Python :: 3.9",
16+
"Programming Language :: Python :: 3.10",
17+
"License :: OSI Approved :: MIT License",
18+
"Operating System :: OS Independent",
19+
"Topic :: Software Development :: Testing",
20+
"Framework :: Pytest",
21+
"Framework :: Pytest :: Plugin",
1322
]
1423

1524
[tool.poetry.urls]
1625
Changelog = "https://github.com/56kyle/pytest-static/releases"
1726

1827
[tool.poetry.dependencies]
19-
python = "^3.7"
28+
python = ">=3.8,<4.0"
2029
click = ">=8.0.1"
30+
loguru = "^0.7.0"
31+
tornado = ">=6.3.2"
32+
2133

2234
[tool.poetry.dev-dependencies]
2335
Pygments = ">=2.10.0"
@@ -48,6 +60,9 @@ myst-parser = {version = ">=0.16.1"}
4860
[tool.poetry.scripts]
4961
pytest-static = "pytest_static.__main__:main"
5062

63+
[tool.poetry.plugins."pytest"]
64+
pytest-static= "pytest_static.plugin"
65+
5166
[tool.coverage.paths]
5267
source = ["src", "*/site-packages"]
5368
tests = ["tests", "*/tests"]

src/pytest_static/parametric.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
"""A Python module used for parameterizing Literal's and common types."""
2+
import inspect
3+
import itertools
4+
from dataclasses import dataclass
5+
from dataclasses import field
6+
from enum import Enum
7+
from typing import Any
8+
from typing import Callable
9+
from typing import Dict
10+
from typing import FrozenSet
11+
from typing import Generic
12+
from typing import Iterable
13+
from typing import List
14+
from typing import Literal
15+
from typing import Optional
16+
from typing import Sequence
17+
from typing import Set
18+
from typing import Tuple
19+
from typing import Type
20+
from typing import TypeVar
21+
from typing import Union
22+
from typing import get_args
23+
from typing import get_origin
24+
25+
from _pytest.mark import Mark
26+
from _pytest.python import Metafunc
27+
28+
from pytest_static.type_sets import PREDEFINED_TYPE_SETS
29+
30+
31+
# Redefines pytest's typing so that we can get 100% test coverage
32+
_ScopeName = Literal["session", "package", "module", "class", "function"]
33+
34+
T = TypeVar("T")
35+
36+
37+
DEFAULT_SUM_TYPES: Set[Any] = {Union, Optional, Enum}
38+
DEFAULT_PRODUCT_TYPES: Set[Any] = {
39+
List,
40+
list,
41+
Set,
42+
set,
43+
FrozenSet,
44+
frozenset,
45+
Dict,
46+
dict,
47+
Tuple,
48+
tuple,
49+
}
50+
51+
52+
@dataclass(frozen=True)
53+
class ExpandedType(Generic[T]):
54+
"""A dataclass used to represent a type with expanded type arguments."""
55+
56+
primary_type: Type[T]
57+
type_args: Tuple[Union[Any, "ExpandedType[Any]"], ...]
58+
59+
@staticmethod
60+
def _get_parameter_combinations(
61+
parameter_instance_sets: List[Tuple[T, ...]]
62+
) -> List[Tuple[Any, ...]]:
63+
"""Returns a list of parameter combinations."""
64+
if len(parameter_instance_sets) > 1:
65+
return list(itertools.product(*parameter_instance_sets))
66+
return list(zip(*parameter_instance_sets))
67+
68+
def get_instances(self) -> Tuple[T, ...]:
69+
"""Returns a tuple of all possible instances of the primary_type."""
70+
parameter_instance_sets: List[
71+
Tuple[T, ...]
72+
] = self._get_parameter_instance_sets()
73+
74+
parameter_combinations: List[
75+
Tuple[Any, ...]
76+
] = self._get_parameter_combinations(parameter_instance_sets)
77+
78+
instances: Tuple[T, ...] = self._instantiate_each_parameter_combination(
79+
parameter_combinations
80+
)
81+
return instances
82+
83+
def _get_parameter_instance_sets(self) -> List[Tuple[T, ...]]:
84+
"""Returns a list of parameter instance sets."""
85+
parameter_instances: List[Tuple[T, ...]] = []
86+
for arg in self.type_args:
87+
if isinstance(arg, ExpandedType):
88+
parameter_instances.append(arg.get_instances())
89+
else:
90+
parameter_instances.append(tuple(PREDEFINED_TYPE_SETS.get(arg, arg)))
91+
return parameter_instances
92+
93+
def _instantiate_each_parameter_combination(
94+
self, parameter_combinations: List[Tuple[Any, ...]]
95+
) -> Tuple[T, ...]:
96+
"""Returns a tuple of all possible instances of the primary_type."""
97+
try:
98+
return self._instantiate_from_signature(parameter_combinations)
99+
except ValueError as e:
100+
if "no signature found for builtin type" not in str(e):
101+
raise e
102+
return self._instantiate_from_trial_and_error(parameter_combinations)
103+
104+
def _instantiate_from_signature(
105+
self, parameter_combinations: List[Tuple[Any, ...]]
106+
) -> Tuple[T, ...]:
107+
"""Returns a tuple of all possible instances of the primary_type."""
108+
signature: inspect.Signature = inspect.signature(self.primary_type)
109+
if len(signature.parameters) > 1:
110+
return self._instantiate_combinations_using_expanded(
111+
parameter_combinations=parameter_combinations
112+
)
113+
return self._instantiate_combinations_using_not_expanded(
114+
parameter_combinations=parameter_combinations
115+
)
116+
117+
def _instantiate_from_trial_and_error(
118+
self, parameter_combinations: List[Tuple[Any, ...]]
119+
) -> Tuple[T, ...]:
120+
"""Returns a tuple of all possible instances of the primary_type."""
121+
try:
122+
return self._instantiate_combinations_using_expanded(
123+
parameter_combinations=parameter_combinations
124+
)
125+
except TypeError:
126+
return self._instantiate_combinations_using_not_expanded(
127+
parameter_combinations=parameter_combinations
128+
)
129+
130+
def _instantiate_combinations_using_expanded(
131+
self, parameter_combinations: List[Tuple[Any, ...]]
132+
) -> Tuple[T, ...]:
133+
"""Returns a tuple of all possible instances of the primary_type."""
134+
return tuple(self._instantiate_expanded(pc) for pc in parameter_combinations)
135+
136+
def _instantiate_combinations_using_not_expanded(
137+
self, parameter_combinations: List[Tuple[Any, ...]]
138+
) -> Tuple[T, ...]:
139+
"""Returns a tuple of all possible instances of the primary_type."""
140+
return tuple(
141+
self._instantiate_not_expanded(pc) for pc in parameter_combinations
142+
)
143+
144+
def _instantiate_expanded(self, combination: Tuple[Any, ...]) -> T:
145+
"""Returns an instance of the primary_type using the combination provided."""
146+
if self.primary_type is dict:
147+
instantiation_method: Callable[..., T] = self.primary_type
148+
return instantiation_method([combination])
149+
return self.primary_type(*combination)
150+
151+
def _instantiate_not_expanded(self, combination: Tuple[Any, ...]) -> T:
152+
"""Returns an instance of the primary_type using the combination provided."""
153+
instantiation_method: Callable[..., T] = self.primary_type
154+
return instantiation_method(combination)
155+
156+
157+
@dataclass(frozen=True)
158+
class Config:
159+
"""A dataclass used to configure the expansion of types."""
160+
161+
max_elements: int = 5
162+
max_depth: int = 5
163+
custom_handlers: Dict[
164+
Any, Callable[[Type[T], "Config"], Set[Union[Any, ExpandedType[T]]]]
165+
] = field(default_factory=dict)
166+
167+
168+
DEFAULT_CONFIG: Config = Config()
169+
170+
171+
def parametrize_types(
172+
metafunc: Metafunc,
173+
argnames: Union[str, Sequence[str]],
174+
argtypes: List[Type[T]],
175+
indirect: Union[bool, Sequence[str]] = False,
176+
ids: Optional[
177+
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
178+
] = None,
179+
scope: Optional[_ScopeName] = None,
180+
*,
181+
_param_mark: Optional[Mark] = None,
182+
) -> None:
183+
"""Parametrizes the provided argnames with the provided argtypes."""
184+
argnames = _ensure_sequence(argnames)
185+
if len(argnames) != len(argtypes):
186+
raise ValueError("The number of argnames and argtypes must be the same.")
187+
188+
instance_sets: List[List[T]] = [
189+
list(get_all_possible_type_instances(t)) for t in argtypes
190+
]
191+
instance_combinations: List[Iterable[itertools.product[Tuple[Any, ...]]]] = list(
192+
itertools.product(*instance_sets)
193+
)
194+
195+
if ids is None:
196+
ids = [", ".join(map(repr, ic)) for ic in instance_combinations]
197+
198+
metafunc.parametrize(
199+
argnames=argnames,
200+
argvalues=instance_combinations,
201+
indirect=indirect,
202+
ids=ids,
203+
scope=scope,
204+
_param_mark=_param_mark,
205+
)
206+
207+
208+
def get_all_possible_type_instances(
209+
type_arg: Type[T], config: Config = DEFAULT_CONFIG
210+
) -> Tuple[T, ...]:
211+
"""Returns a tuple of all possible instances of the provided type."""
212+
expanded_types: Set[Union[Any, ExpandedType[T]]] = expand_type(type_arg, config)
213+
instances: List[T] = []
214+
for expanded_type in expanded_types:
215+
if isinstance(expanded_type, ExpandedType):
216+
instances.extend(expanded_type.get_instances())
217+
else:
218+
instances.extend(PREDEFINED_TYPE_SETS.get(expanded_type, []))
219+
return tuple(instances)
220+
221+
222+
def _ensure_sequence(value: Union[str, Sequence[str]]) -> Sequence[str]:
223+
"""Returns the provided value as a sequence."""
224+
if isinstance(value, str):
225+
return value.split(", ")
226+
return value
227+
228+
229+
def return_self(arg: T, *_: Any) -> Set[T]:
230+
"""Returns the provided argument."""
231+
return {arg}
232+
233+
234+
def expand_type(
235+
type_arg: Union[Any, Type[T]], config: Config = DEFAULT_CONFIG
236+
) -> Set[Union[Any, ExpandedType[T]]]:
237+
"""Expands the provided type into the set of all possible subtype combinations."""
238+
origin: Any = get_origin(type_arg) or type_arg
239+
240+
if origin in PREDEFINED_TYPE_SETS:
241+
return {origin}
242+
243+
type_handlers: Dict[
244+
Any, Callable[[Type[T], Config], Set[Union[Any, ExpandedType[T]]]]
245+
] = {
246+
Literal: return_self,
247+
Ellipsis: return_self,
248+
**{sum_type: expand_sum_type for sum_type in DEFAULT_SUM_TYPES},
249+
**{product_type: expand_product_type for product_type in DEFAULT_PRODUCT_TYPES},
250+
}
251+
252+
# Add custom handlers from configuration
253+
type_handlers.update(config.custom_handlers)
254+
255+
if origin in type_handlers:
256+
return type_handlers[origin](type_arg, config)
257+
258+
return {type_arg}
259+
260+
261+
def expand_sum_type(
262+
type_arg: Type[T], config: Config
263+
) -> Set[Union[Any, ExpandedType[T]]]:
264+
"""Expands a sum type into the set of all possible subtype combinations."""
265+
return {
266+
*itertools.chain.from_iterable(
267+
expand_type(arg, config) for arg in get_args(type_arg)
268+
)
269+
}
270+
271+
272+
def expand_product_type(
273+
type_arg: Type[T], config: Config
274+
) -> Set[Union[Any, ExpandedType[T]]]:
275+
"""Expands a product type into the set of all possible subtype combinations."""
276+
origin: Any = get_origin(type_arg) or type_arg
277+
args: Tuple[Any, ...] = get_args(type_arg)
278+
sets: List[Set[Union[Any, ExpandedType[T]]]] = [
279+
expand_type(arg, config) for arg in args
280+
]
281+
product_sets: Tuple[Iterable[Union[Any, ExpandedType[T]]], ...] = tuple(
282+
itertools.product(*sets)
283+
)
284+
return {ExpandedType(origin, tuple(product_set)) for product_set in product_sets}

src/pytest_static/plugin.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""The pytest-static pytest plugin."""
2+
3+
import pytest
4+
from _pytest.python import Metafunc
5+
6+
from pytest_static.parametric import parametrize_types
7+
8+
9+
def pytest_generate_tests(metafunc: Metafunc) -> None:
10+
"""Generate parametrized tests for the given argnames and types."""
11+
for marker in metafunc.definition.iter_markers(name="parametrize_types"):
12+
parametrize_types(metafunc, *marker.args, **marker.kwargs)
13+
14+
15+
def pytest_configure(config: pytest.Config) -> None:
16+
"""Adds pytest-static plugin markers to the pytest CLI."""
17+
config.addinivalue_line(
18+
"markers",
19+
"parametrize_types(argnames, types, ids, *args, **kwargs):"
20+
" Generate parametrized tests for the given argnames and types in argvalues.",
21+
)

0 commit comments

Comments
 (0)