Skip to content

Commit

Permalink
Add support for recursive data structures
Browse files Browse the repository at this point in the history
We need to update the local namespace when we resolve type annotations
of recursive data structures when registering them over `DBC` meta-class
since they are still not available in the scope at that time.

This patch simply puts the class to be registered in the local
namespace.
  • Loading branch information
mristin committed Jul 6, 2021
1 parent 8a56bf1 commit 54c8a10
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 2 deletions.
10 changes: 8 additions & 2 deletions icontract_hypothesis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,12 @@ def _strategy_for_type(
init = getattr(a_type, "__init__")

if inspect.isfunction(init):
strategy = infer_strategy(init)
# Add a local namespace in case there are forward references.
#
# This is needed if we register a class through the ``icontract.DBCMeta``
# meta-class where it references itself. For example, a node in a linked list.
strategy = infer_strategy(init, localns={a_type.__name__: a_type})

elif isinstance(init, icontract._checkers._SLOT_WRAPPER_TYPE):
# We have to distinguish this special case which is used by named tuples and
# possibly other optimized data structures.
Expand Down Expand Up @@ -1477,7 +1482,8 @@ def _register_with_hypothesis(cls: Type[T]) -> None:
return

if cls not in hypothesis.strategies._internal.types._global_type_lookup:
hypothesis.strategies.register_type_strategy(cls, _strategy_for_type(cls))
strategy = _strategy_for_type(cls)
hypothesis.strategies.register_type_strategy(custom_type=cls, strategy=strategy)


def _hook_into_icontract_and_hypothesis() -> None:
Expand Down
36 changes: 36 additions & 0 deletions tests/strategy_inference/test_strategy_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,24 @@ def some_func(x: int, y: int) -> None:
icontract_hypothesis.test_with_inferred_strategy(some_func)


class SomeCyclicalGlobalClass(icontract.DBC):
"""
Represent a class which has a cyclical dependency on itself.
For example, a node of a linked list.
"""

value: int
next_node: Optional["SomeCyclicalGlobalClass"]

@icontract.require(lambda value: value > 0)
def __init__(
self, value: int, next_node: Optional["SomeCyclicalGlobalClass"]
) -> None:
self.value = value
self.next_node = next_node


# noinspection PyUnusedLocal
class TestWithInferredStrategiesOnClasses(unittest.TestCase):
def test_no_preconditions_and_no_argument_init(self) -> None:
Expand Down Expand Up @@ -661,6 +679,24 @@ def some_func(a_or_b: Union[A, B]) -> None:

icontract_hypothesis.test_with_inferred_strategy(some_func)

def test_cyclical_data_structure(self) -> None:
def some_func(cyclical: SomeCyclicalGlobalClass) -> None:
pass

strategy = icontract_hypothesis.infer_strategy(some_func)

self.assertEqual(
"fixed_dictionaries({"
"'cyclical': fixed_dictionaries({"
"'next_node': one_of(none(), builds(SomeCyclicalGlobalClass)),\n"
" 'value': integers(min_value=1)}).map(lambda d: SomeCyclicalGlobalClass(**d))})",
str(strategy),
)

# We can not execute this strategy, as ``builds`` is not handling the recursivity well.
# Please see this Hypothesis issue:
# https://github.com/HypothesisWorks/hypothesis/issues/3026


# noinspection PyUnusedLocal
class TestRepresentationOfCondition(unittest.TestCase):
Expand Down

0 comments on commit 54c8a10

Please sign in to comment.