Legends ofPythos
Claim your name
Pythonic Python

Fenrir, the Bound Wolf

The last fight of this track

Boss · level 17

Fenrir

the Bound Wolf

Health4 of 4 stages standing
No chain forged of iron could hold Fenrir, so the dwarves made Gleipnir from six things that do not exist: the sound of a cat's footfall, the beard of a woman, the roots of a mountain, the sinews of a bear, the breath of a fish and the spittle of a bird. The wolf is straining at it again. Make the binding lazy, counted, tidy and tested, and it will hold.

The fight

Four stages, each building on the last. Every stage opens with your finished code from the stage before, so you pick up where you left off. This fight draws on generators, itertools, decorators, context managers, namedtuple and unittest.
Stage 1: The strands. Draw the strands out lazily, one at a time.
Stage 2: The count. Count every weaving with a decorator.
Stage 3: The binding. Bind the wolf, and unbind him, whatever happens.
Stage 4: The test. Prove the binding holds, with tests.

The fight

0 of 4 stages won

Stage 1 of 4

+60 XP
GLEIPNIR holds the six strands of the binding. Write a generator function strands() that yields them one at a time, in order, and take(n, iterable), which returns a list of the first n items of any iterable using islice, without reading any further than it needs to. Give both functions type hints for their parameters and return values (Iterator[str] from collections.abc suits a generator). For example, take(2, strands()) is ["the sound of a cat's footfall", 'the beard of a woman'].
GLEIPNIR = (
    "the sound of a cat's footfall",
    'the beard of a woman',
    'the roots of a mountain',
    'the sinews of a bear',
    'the breath of a fish',
    'the spittle of a bird',
)




from collections.abc import Iterable, Iterator
from itertools import islice




def strands():
    # Yield each strand of GLEIPNIR in turn (and add type hints)
    pass




def take(n, iterable):
    # The first n items, as a list, reading no further than needed
    pass

Run your code to check it against the tests.

Stage 2 of 4

+60 XP
Keep stage 1. Write a decorator counted that counts how many times the decorated function is called, in an attribute calls on the wrapper that starts at 0. The wrapper passes any positional and keyword arguments through, returns the function's result, and uses functools.wraps so it keeps the function's name and docstring. Then write weave(n: int) -> list[str], with a docstring, decorated with @counted, returning the first n strands by way of take and strands. For example, after two more calls to weave, weave.calls has gone up by 2.
GLEIPNIR = (
    "the sound of a cat's footfall",
    'the beard of a woman',
    'the roots of a mountain',
    'the sinews of a bear',
    'the breath of a fish',
    'the spittle of a bird',
)




from collections.abc import Iterable, Iterator
from itertools import islice




def strands() -> Iterator[str]:
    for strand in GLEIPNIR:
        yield strand




def take(n: int, iterable: Iterable) -> list:
    return list(islice(iterable, n))




from functools import wraps




def counted(func):
    # Wrap func, count its calls in wrapper.calls, keep its name and docstring
    pass




# Decorate weave with @counted and give it a docstring
def weave(n: int) -> list[str]:
    pass

Run your code to check it against the tests.

Stage 3 of 4

+60 XP
Keep everything and write a context manager binding(log) with contextlib.contextmanager. On entry it appends 'bound' to the list log; on exit it appends 'unbound', even when the body of the with block raises. The exception must still reach the caller: the binding cleans up, it does not swallow it. strain(log, bite) is already written for you, and runs a body inside your binding: strain(log) leaves log as ['bound', 'strain', 'unbound'].
GLEIPNIR = (
    "the sound of a cat's footfall",
    'the beard of a woman',
    'the roots of a mountain',
    'the sinews of a bear',
    'the breath of a fish',
    'the spittle of a bird',
)




from collections.abc import Iterable, Iterator
from itertools import islice




def strands() -> Iterator[str]:
    for strand in GLEIPNIR:
        yield strand




def take(n: int, iterable: Iterable) -> list:
    return list(islice(iterable, n))




from functools import wraps




def counted(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper




@counted

Run your code to check it against the tests.

Stage 4 of 4

+60 XP
Keep everything. Define Strand = namedtuple('Strand', 'name length') and write strand_lengths(), which returns a list with a Strand for each strand of Gleipnir, where length is the number of words in it. Then write a unittest.TestCase subclass GleipnirTests with at least three test methods: one that checks weave(3) returns three strands, one that checks weave.calls goes up when weave is called, and one that checks binding logs 'unbound' after the body raises. Finally, write run_tests(), which runs GleipnirTests with unittest.TextTestRunner(stream=io.StringIO()) and returns whether they all passed. For example, strand_lengths()[1] is Strand('the beard of a woman', 5).
GLEIPNIR = (
    "the sound of a cat's footfall",
    'the beard of a woman',
    'the roots of a mountain',
    'the sinews of a bear',
    'the breath of a fish',
    'the spittle of a bird',
)




from collections.abc import Iterable, Iterator
from itertools import islice




def strands() -> Iterator[str]:
    for strand in GLEIPNIR:
        yield strand




def take(n: int, iterable: Iterable) -> list:
    return list(islice(iterable, n))




from functools import wraps




def counted(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper




@counted

Run your code to check it against the tests.