Skip to content

Question/Feature request: Iterable usage in require and snapshot decorators #249

@claudio-ebel

Description

@claudio-ebel

Question

How do I use Iterable in a @snapshot or in a @require decorator? I didn't find any hints in the documentation so I assume I either used them wrong or didn't understand a core concept. Otherwise, I would like to propose a feature request 😀

Examples for @ensure

If I try to take a snapshot of an Iterable, it is not possible since snapshot consumes it. Using the file iter_in_ensure.py

# ––– file iter_in_ensure.py
from collections.abc import Iterable
from icontract import ensure, snapshot
from more_itertools import always_iterable


@snapshot(lambda i: list(i))
@ensure(lambda result, OLD: set(OLD.i) == set(result))
def ensure_iter(i: Iterable[int]) -> list[int]:
    return list(i)


assert 1 == len(ensure_iter(always_iterable(1)))

in a python environment where icontract and more_itertools is included, the error message is:

$ python iter_in_ensure.py
Traceback (most recent call last):
  File "iter_in_ensure.py", line 13, in <module>
    assert 1 == len(ensure_iter(always_iterable(1)))
  File "/…/icontract/_checkers.py", line 649, in wrapper
    raise violation_error
icontract.errors.ViolationError: File iter_in_ensure.py, line 8 in <module>:
set(OLD.i) == set(result):
OLD was a bunch of OLD values
OLD.i was [1]
i was <tuple_iterator object at 0x7fe0fe9afb20>
result was []
set(OLD.i) was {1}
set(result) was set()

showing that the @snapshot decorator already consumed the iterator, leaving an empty result back. A possible workaround is to use itertools.tee inside the function:

# ––– file fixup_ensure.py
from collections.abc import Iterable
from icontract import ensure
from itertools import tee
from more_itertools import always_iterable


@ensure(lambda result: set(result[0]) == set(result[1]))
def ensure_iter(i: Iterable[int]) -> tuple[list[int], Iterable[int]]:
    tee0, tee1 = tee(i, 2)
    return list(tee0), tee1


assert 1 == len(ensure_iter(always_iterable(1))[0])

but that requires to change the functions's signature for usage in @ensure only which – at least in my opinion – contradicts icontract's philosophy.

Examples for @require

With the @require decorator, I even didn't find a workaround:

# ––– file iter_in_require.py
from collections.abc import Iterable
from icontract import require
from more_itertools import always_iterable


@require(lambda i: 1 == len(list(i)))
def require_iter(i: Iterable[int]) -> list[int]:
    return list(i)


length = len(require_iter(always_iterable(1)))
assert 1 == length, f"result length was {length}"

results in

$ python iter_in_require.py
Traceback (most recent call last):
  File "iter_in_require.py", line 13, in <module>
    assert 1 == length, f"result length was {length}"
AssertionError: result length was 0

showing that @require already consumed the iterator and the function require_iter has no chance to access it again.

Versions

  • Python: Python 3.10.4
  • more-itertools: 8.13.0
  • icontract: 2.6.1

Feature pitch

If I didn't miss anything, there are features missing for the @snapshot and the @require function decorators. I suggest to introduce additional arguments to disable iterator consumption.

A possible example usage for @snapshot:

from collections.abc import Iterable
from icontract import ensure, snapshot
from more_itertools import always_iterable


@snapshot(lambda i: list(i), iter='i')  # <- new argument 'iter'
@ensure(lambda result, OLD: set(OLD.i) == set(result))
def ensure_iter(i: Iterable[int]) -> list[int]:
    return list(i)


assert 1 == len(ensure_iter(always_iterable(1)))

A possible example usage for @require:

from collections.abc import Iterable
from icontract import require
from more_itertools import always_iterable


@require(lambda i: 1 == len(list(i)), iter='i')  # <- new argument 'iter'
def require_iter(i: Iterable[int]) -> list[int]:
    return list(i)


length = len(require_iter(always_iterable(1)))
assert 1 == length, f"result length was {length}"

The new argument iter indicates to use an independent iterator. In @require's case, it forwards it to the decorated function.

I'm aware that the proposed solution is not applicable to all iterables, but I'm still convinced it would pose an improvement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions