Skip to content

Commit

Permalink
[feature] detect cycles in profile definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanhoelzl committed Dec 9, 2021
1 parent 70167d3 commit bff1e6b
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 12 deletions.
2 changes: 1 addition & 1 deletion conftest.py
Expand Up @@ -68,6 +68,6 @@ def mccabe(config: Config) -> None:
"""profile for mccabe code complexity"""
config.option.mccabe = True
try:
config.addinivalue_line("mccabe-complexity", "3")
config.addinivalue_line("mccabe-complexity", "4")
except ValueError:
pass
39 changes: 31 additions & 8 deletions pytest_profiles/profile.py
Expand Up @@ -16,6 +16,25 @@

from _pytest.config import Config # pylint: disable=protected-access


class PytestProfilesException(Exception):
"""Base for pytest profiles exceptions."""


class UnknownProfiles(PytestProfilesException):
"""Exception for using a unknown profile."""

def __init__(self, unkonwn_profiles: Iterable[str]) -> None:
super().__init__(f"unregistered profiles used: {', '.join(unkonwn_profiles)}")


class ProfileCycleDetected(PytestProfilesException):
"""Exception when an cycle in the profile definitions is detected."""

def __init__(self, cycle: Iterable[str]) -> None:
super().__init__(f"profile cycle detected: {' -> '.join(cycle)}")


RegisteredProfiles: MutableMapping[str, "Profile"] = OrderedDict()


Expand Down Expand Up @@ -81,20 +100,24 @@ def resolve_profiles(
)

deduplicated = OrderedDict((n, None) for n in with_dependecies).keys()
_check_for_unregistered_profiles(deduplicated)
_check_for_unknown_profiles(deduplicated)
for profile_name in deduplicated:
yield RegisteredProfiles[profile_name]


def _with_dependencies(profile_name: str) -> Generator[str, None, None]:
def _with_dependencies(
profile_name: str, chain: Optional[List[str]] = None
) -> Generator[str, None, None]:
if profile_name in RegisteredProfiles:
chain = chain or []
if profile_name in chain:
raise ProfileCycleDetected([*chain, profile_name])
for dependency in RegisteredProfiles[profile_name].uses or []:
yield from _with_dependencies(dependency)
yield dependency
yield from _with_dependencies(dependency, chain=[*chain, profile_name])
yield profile_name


def _check_for_unregistered_profiles(profile_names: Iterable[str]) -> None:
unregistered = [n for n in profile_names if n not in RegisteredProfiles]
if unregistered:
raise ValueError(f"unregistered profiles used: {', '.join(unregistered)}")
def _check_for_unknown_profiles(profile_names: Iterable[str]) -> None:
unknown = [n for n in profile_names if n not in RegisteredProfiles]
if unknown:
raise UnknownProfiles(unknown)
20 changes: 17 additions & 3 deletions tests/test_profile.py
Expand Up @@ -2,7 +2,13 @@
import pytest
from _pytest.config import Config # pylint: disable=protected-access

from pytest_profiles.profile import RegisteredProfiles, profile, resolve_profiles
from pytest_profiles.profile import (
ProfileCycleDetected,
RegisteredProfiles,
UnknownProfiles,
profile,
resolve_profiles,
)


def test_create_and_apply(pytester: pytest.Pytester) -> None:
Expand Down Expand Up @@ -67,6 +73,14 @@ def test_resolve_profiles_keep_order() -> None:
assert list(resolve_profiles(profiles=["first"])) == [first, second]


def test_resolve_profiles_value_error_on_unregistered_profile() -> None:
with pytest.raises(ValueError):
def test_resolve_profiles_value_error_on_unknown_profile() -> None:
with pytest.raises(UnknownProfiles):
list(resolve_profiles(profiles=["first"]))


def test_resolve_profiles_detect_cycles() -> None:
profile(name="first", uses="second")(lambda c: None)
profile(name="second", uses="first")(lambda c: None)

with pytest.raises(ProfileCycleDetected):
list(resolve_profiles(profiles=["first"]))

0 comments on commit bff1e6b

Please sign in to comment.