Skip to content

Commit

Permalink
Merge pull request #169 from pytest-dev/ab-slim-stacktrace
Browse files Browse the repository at this point in the history
Slim stack traces
  • Loading branch information
youtux authored Jun 11, 2022
2 parents 626fac1 + 8c52cb6 commit da70476
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Unreleased
----------
- Using a generic class container like ``dict``, ``list``, ``set``, etc. will raise a warning suggesting you to wrap your model using ``named_model(...)``. Doing this will make sure that the fixture name is correctly chosen, otherwise SubFactory and RelatedFactory aren't able to determine the name of the model. See `Generic Container Classes as models <https://pytest-factoryboy.readthedocs.io/en/latest/#generic-container-classes-as-models>`_ `#167 <https://github.com/pytest-dev/pytest-factoryboy/pull/167>`_
- Fix ``Factory._after_postgeneration`` being invoked twice. `#164 <https://github.com/pytest-dev/pytest-factoryboy/pull/164>`_ `#156 <https://github.com/pytest-dev/pytest-factoryboy/issues/156>`_
- Stack traces caused by pytest-factoryboy are now slimmer. `#169 <https://github.com/pytest-dev/pytest-factoryboy/pull/169>`_
- Check for naming conflicts between factory and model fixture name, and raise a clear error immediately. `#86 <https://github.com/pytest-dev/pytest-factoryboy/pull/86>`_

2.4.0
----------
Expand Down
28 changes: 21 additions & 7 deletions pytest_factoryboy/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
FactoryType: TypeAlias = Type[factory.Factory]
F = TypeVar("F", bound=FactoryType)
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
P = ParamSpec("P")

SEPARATOR = "__"
Expand All @@ -58,6 +59,19 @@ def __call__(self, request: SubRequest) -> Any:
return self.function(request)


class Box(Generic[T_co]):
"""Simple box class, used to hold a value.
The main purpose of this is to hold objects that we don't want to appear in stack traces.
For example, the "caller_locals" dict holding a lot of items.
"""

value: T_co

def __init__(self, value: T_co):
self.value = value


def named_model(model_cls: type[T], name: str) -> type[T]:
"""Return a model class with a given name."""
return type(name, (model_cls,), {})
Expand All @@ -83,7 +97,7 @@ def register(
factory_class: F | None = None,
_name: str | None = None,
*,
_caller_locals: dict[str, Any] | None = None,
_caller_locals: Box[dict[str, Any]] | None = None,
**kwargs: Any,
) -> F | Callable[[F], F]:
r"""Register fixtures for the factory class.
Expand All @@ -94,7 +108,7 @@ def register(
:param \**kwargs: Optional keyword arguments that override factory attributes.
"""
if _caller_locals is None:
_caller_locals = get_caller_locals()
_caller_locals = Box(get_caller_locals())

if factory_class is None:

Expand Down Expand Up @@ -136,7 +150,7 @@ def generate_fixtures(
model_name: str,
factory_name: str,
overrides: Mapping[str, Any],
caller_locals: Mapping[str, Any],
caller_locals: Box[Mapping[str, Any]],
) -> Iterable[tuple[str, Callable[..., Any]]]:
"""Generate all the FixtureDefs for the given factory class."""

Expand All @@ -154,7 +168,7 @@ def generate_fixtures(
),
)

if factory_name not in caller_locals:
if factory_name not in caller_locals.value:
yield (
factory_name,
create_fixture_with_related(
Expand Down Expand Up @@ -240,7 +254,7 @@ def make_declaration_fixturedef(
)


def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[str, Any]) -> None:
def inject_into_caller(name: str, function: Callable[..., Any], locals_: Box[dict[str, Any]]) -> None:
"""Inject a function into the caller's locals, making sure that the function will work also within classes."""
# We need to check if the caller frame is a class, since in that case the first argument is the class itself.
# In that case, we can apply the staticmethod() decorator to the injected function, so that the first param
Expand All @@ -252,11 +266,11 @@ def inject_into_caller(name: str, function: Callable[..., Any], locals_: dict[st
# This could change in the future, but it shouldn't be too much of a problem since registering a factory
# in a function namespace would not make it usable anyway.
# Therefore, we can just check for __qualname__ to figure out if we are in a class, and apply the @staticmethod.
is_class_or_function = "__qualname__" in locals_
is_class_or_function = "__qualname__" in locals_.value
if is_class_or_function:
function = staticmethod(function)

locals_[name] = function
locals_.value[name] = function


def get_model_name(factory_class: FactoryType) -> str:
Expand Down

0 comments on commit da70476

Please sign in to comment.