Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6c32b64
Initial bit of the plugin
56kyle May 8, 2023
59485f4
Finds a bit of a middle ground for the chaos surrounding dicts
56kyle May 12, 2023
8d9282e
Fixes imports
56kyle Aug 19, 2023
ad8a082
Refactors a bit and fixes more imports broken by the project move.
56kyle Aug 19, 2023
cc4d0b9
Converts imports back to not include src directory and instead adds a…
56kyle Aug 19, 2023
0cef4c7
Adds more types for mypy
56kyle Aug 19, 2023
d434489
More work trying to appease the mypy overlord
56kyle Aug 20, 2023
72c8635
Works a bit more on tests and adds some fixtures
56kyle Aug 20, 2023
04efd3d
Fixes a test
56kyle Aug 29, 2023
a2e757b
Improving test coverage by adding _ScopeName to the package
56kyle Aug 29, 2023
b44dde9
Returns test coverage to 100%
56kyle Aug 31, 2023
757bebf
Appeases the majority of mypy. Prepping to use ParamSpec in ExpandedType
56kyle Aug 31, 2023
f55b650
cleans up typing and sacrifices a goat for our mypy overlords
56kyle Aug 31, 2023
2815ee6
Appeases mypy further and adds B905 to Flake8 ignore due to backward …
56kyle Sep 1, 2023
49c04c9
Updates packages
56kyle Sep 1, 2023
d162a4b
Changes github actions to stop checking 3.7 since it reached EOL
56kyle Sep 1, 2023
d5c68da
Merge pull request #58 from 56kyle/feature/redesign_type_expansion
56kyle Sep 1, 2023
7f23fbb
Merge branch 'master' into merge_master_back_in
56kyle Sep 1, 2023
fe77633
Merge pull request #59 from 56kyle/merge_master_back_in
56kyle Sep 1, 2023
a87d3d3
Changes pyproject.toml to match our actual allowed versions
56kyle Sep 1, 2023
dbd7032
Merge pull request #60 from 56kyle/updates_metadata
56kyle Sep 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[flake8]
select = B,B9,C,D,DAR,E,F,N,RST,S,W
ignore = E203,E501,RST201,RST203,RST301,W503
ignore = E203,E501,RST201,RST203,RST301,W503,B905
max-line-length = 80
max-complexity = 10
docstring-convention = google
per-file-ignores = tests/*:S101
per-file-ignores = tests/*:S101,D100,D101,D102,D103,D104
rst-roles = class,const,func,meth,mod,ref
rst-directives = deprecated
2 changes: 0 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ jobs:
- { python: "3.10", os: "ubuntu-latest", session: "mypy" }
- { python: "3.9", os: "ubuntu-latest", session: "mypy" }
- { python: "3.8", os: "ubuntu-latest", session: "mypy" }
- { python: "3.7", os: "ubuntu-latest", session: "mypy" }
- { python: "3.10", os: "ubuntu-latest", session: "tests" }
- { python: "3.9", os: "ubuntu-latest", session: "tests" }
- { python: "3.8", os: "ubuntu-latest", session: "tests" }
- { python: "3.7", os: "ubuntu-latest", session: "tests" }
- { python: "3.10", os: "windows-latest", session: "tests" }
- { python: "3.10", os: "macos-latest", session: "tests" }
- { python: "3.10", os: "ubuntu-latest", session: "typeguard" }
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


package = "pytest_static"
python_versions = ["3.10", "3.9", "3.8", "3.7"]
python_versions = ["3.10", "3.9", "3.8"]
nox.needs_version = ">= 2021.6.6"
nox.options.sessions = (
"pre-commit",
Expand Down
1,787 changes: 920 additions & 867 deletions poetry.lock

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@ repository = "https://github.com/56kyle/pytest-static"
documentation = "https://pytest-static.readthedocs.io"
classifiers = [
"Development Status :: 1 - Planning",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Testing",
"Framework :: Pytest",
"Framework :: Pytest :: Plugin",
]

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

[tool.poetry.dependencies]
python = "^3.7"
python = ">=3.8,<4.0"
click = ">=8.0.1"
loguru = "^0.7.0"
tornado = ">=6.3.2"


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

[tool.poetry.plugins."pytest"]
pytest-static= "pytest_static.plugin"

[tool.coverage.paths]
source = ["src", "*/site-packages"]
tests = ["tests", "*/tests"]
Expand Down
284 changes: 284 additions & 0 deletions src/pytest_static/parametric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
"""A Python module used for parameterizing Literal's and common types."""
import inspect
import itertools
from dataclasses import dataclass
from dataclasses import field
from enum import Enum
from typing import Any
from typing import Callable
from typing import Dict
from typing import FrozenSet
from typing import Generic
from typing import Iterable
from typing import List
from typing import Literal
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
from typing import get_args
from typing import get_origin

from _pytest.mark import Mark
from _pytest.python import Metafunc

from pytest_static.type_sets import PREDEFINED_TYPE_SETS


# Redefines pytest's typing so that we can get 100% test coverage
_ScopeName = Literal["session", "package", "module", "class", "function"]

T = TypeVar("T")


DEFAULT_SUM_TYPES: Set[Any] = {Union, Optional, Enum}
DEFAULT_PRODUCT_TYPES: Set[Any] = {
List,
list,
Set,
set,
FrozenSet,
frozenset,
Dict,
dict,
Tuple,
tuple,
}


@dataclass(frozen=True)
class ExpandedType(Generic[T]):
"""A dataclass used to represent a type with expanded type arguments."""

primary_type: Type[T]
type_args: Tuple[Union[Any, "ExpandedType[Any]"], ...]

@staticmethod
def _get_parameter_combinations(
parameter_instance_sets: List[Tuple[T, ...]]
) -> List[Tuple[Any, ...]]:
"""Returns a list of parameter combinations."""
if len(parameter_instance_sets) > 1:
return list(itertools.product(*parameter_instance_sets))
return list(zip(*parameter_instance_sets))

def get_instances(self) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
parameter_instance_sets: List[
Tuple[T, ...]
] = self._get_parameter_instance_sets()

parameter_combinations: List[
Tuple[Any, ...]
] = self._get_parameter_combinations(parameter_instance_sets)

instances: Tuple[T, ...] = self._instantiate_each_parameter_combination(
parameter_combinations
)
return instances

def _get_parameter_instance_sets(self) -> List[Tuple[T, ...]]:
"""Returns a list of parameter instance sets."""
parameter_instances: List[Tuple[T, ...]] = []
for arg in self.type_args:
if isinstance(arg, ExpandedType):
parameter_instances.append(arg.get_instances())
else:
parameter_instances.append(tuple(PREDEFINED_TYPE_SETS.get(arg, arg)))
return parameter_instances

def _instantiate_each_parameter_combination(
self, parameter_combinations: List[Tuple[Any, ...]]
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
try:
return self._instantiate_from_signature(parameter_combinations)
except ValueError as e:
if "no signature found for builtin type" not in str(e):
raise e
return self._instantiate_from_trial_and_error(parameter_combinations)

def _instantiate_from_signature(
self, parameter_combinations: List[Tuple[Any, ...]]
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
signature: inspect.Signature = inspect.signature(self.primary_type)
if len(signature.parameters) > 1:
return self._instantiate_combinations_using_expanded(
parameter_combinations=parameter_combinations
)
return self._instantiate_combinations_using_not_expanded(
parameter_combinations=parameter_combinations
)

def _instantiate_from_trial_and_error(
self, parameter_combinations: List[Tuple[Any, ...]]
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
try:
return self._instantiate_combinations_using_expanded(
parameter_combinations=parameter_combinations
)
except TypeError:
return self._instantiate_combinations_using_not_expanded(
parameter_combinations=parameter_combinations
)

def _instantiate_combinations_using_expanded(
self, parameter_combinations: List[Tuple[Any, ...]]
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
return tuple(self._instantiate_expanded(pc) for pc in parameter_combinations)

def _instantiate_combinations_using_not_expanded(
self, parameter_combinations: List[Tuple[Any, ...]]
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the primary_type."""
return tuple(
self._instantiate_not_expanded(pc) for pc in parameter_combinations
)

def _instantiate_expanded(self, combination: Tuple[Any, ...]) -> T:
"""Returns an instance of the primary_type using the combination provided."""
if self.primary_type is dict:
instantiation_method: Callable[..., T] = self.primary_type
return instantiation_method([combination])
return self.primary_type(*combination)

def _instantiate_not_expanded(self, combination: Tuple[Any, ...]) -> T:
"""Returns an instance of the primary_type using the combination provided."""
instantiation_method: Callable[..., T] = self.primary_type
return instantiation_method(combination)


@dataclass(frozen=True)
class Config:
"""A dataclass used to configure the expansion of types."""

max_elements: int = 5
max_depth: int = 5
custom_handlers: Dict[
Any, Callable[[Type[T], "Config"], Set[Union[Any, ExpandedType[T]]]]
] = field(default_factory=dict)


DEFAULT_CONFIG: Config = Config()


def parametrize_types(
metafunc: Metafunc,
argnames: Union[str, Sequence[str]],
argtypes: List[Type[T]],
indirect: Union[bool, Sequence[str]] = False,
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
scope: Optional[_ScopeName] = None,
*,
_param_mark: Optional[Mark] = None,
) -> None:
"""Parametrizes the provided argnames with the provided argtypes."""
argnames = _ensure_sequence(argnames)
if len(argnames) != len(argtypes):
raise ValueError("The number of argnames and argtypes must be the same.")

instance_sets: List[List[T]] = [
list(get_all_possible_type_instances(t)) for t in argtypes
]
instance_combinations: List[Iterable[itertools.product[Tuple[Any, ...]]]] = list(
itertools.product(*instance_sets)
)

if ids is None:
ids = [", ".join(map(repr, ic)) for ic in instance_combinations]

metafunc.parametrize(
argnames=argnames,
argvalues=instance_combinations,
indirect=indirect,
ids=ids,
scope=scope,
_param_mark=_param_mark,
)


def get_all_possible_type_instances(
type_arg: Type[T], config: Config = DEFAULT_CONFIG
) -> Tuple[T, ...]:
"""Returns a tuple of all possible instances of the provided type."""
expanded_types: Set[Union[Any, ExpandedType[T]]] = expand_type(type_arg, config)
instances: List[T] = []
for expanded_type in expanded_types:
if isinstance(expanded_type, ExpandedType):
instances.extend(expanded_type.get_instances())
else:
instances.extend(PREDEFINED_TYPE_SETS.get(expanded_type, []))
return tuple(instances)


def _ensure_sequence(value: Union[str, Sequence[str]]) -> Sequence[str]:
"""Returns the provided value as a sequence."""
if isinstance(value, str):
return value.split(", ")
return value


def return_self(arg: T, *_: Any) -> Set[T]:
"""Returns the provided argument."""
return {arg}


def expand_type(
type_arg: Union[Any, Type[T]], config: Config = DEFAULT_CONFIG
) -> Set[Union[Any, ExpandedType[T]]]:
"""Expands the provided type into the set of all possible subtype combinations."""
origin: Any = get_origin(type_arg) or type_arg

if origin in PREDEFINED_TYPE_SETS:
return {origin}

type_handlers: Dict[
Any, Callable[[Type[T], Config], Set[Union[Any, ExpandedType[T]]]]
] = {
Literal: return_self,
Ellipsis: return_self,
**{sum_type: expand_sum_type for sum_type in DEFAULT_SUM_TYPES},
**{product_type: expand_product_type for product_type in DEFAULT_PRODUCT_TYPES},
}

# Add custom handlers from configuration
type_handlers.update(config.custom_handlers)

if origin in type_handlers:
return type_handlers[origin](type_arg, config)

return {type_arg}


def expand_sum_type(
type_arg: Type[T], config: Config
) -> Set[Union[Any, ExpandedType[T]]]:
"""Expands a sum type into the set of all possible subtype combinations."""
return {
*itertools.chain.from_iterable(
expand_type(arg, config) for arg in get_args(type_arg)
)
}


def expand_product_type(
type_arg: Type[T], config: Config
) -> Set[Union[Any, ExpandedType[T]]]:
"""Expands a product type into the set of all possible subtype combinations."""
origin: Any = get_origin(type_arg) or type_arg
args: Tuple[Any, ...] = get_args(type_arg)
sets: List[Set[Union[Any, ExpandedType[T]]]] = [
expand_type(arg, config) for arg in args
]
product_sets: Tuple[Iterable[Union[Any, ExpandedType[T]]], ...] = tuple(
itertools.product(*sets)
)
return {ExpandedType(origin, tuple(product_set)) for product_set in product_sets}
21 changes: 21 additions & 0 deletions src/pytest_static/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""The pytest-static pytest plugin."""

import pytest
from _pytest.python import Metafunc

from pytest_static.parametric import parametrize_types


def pytest_generate_tests(metafunc: Metafunc) -> None:
"""Generate parametrized tests for the given argnames and types."""
for marker in metafunc.definition.iter_markers(name="parametrize_types"):
parametrize_types(metafunc, *marker.args, **marker.kwargs)


def pytest_configure(config: pytest.Config) -> None:
"""Adds pytest-static plugin markers to the pytest CLI."""
config.addinivalue_line(
"markers",
"parametrize_types(argnames, types, ids, *args, **kwargs):"
" Generate parametrized tests for the given argnames and types in argvalues.",
)
Loading