We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Interfaces with Protocols and ABCs step 15 of 26
Check Number Ranges at Runtime with Annotated
A server's bind(port, backlog) takes two integers. mypy checks that both
are int, but not that the port is between 1 and 65535. Annotated lets
you attach that range to the type, and a decorator can check it on every
call:
Port = Annotated[int, Bounds(1, 65535)]
Backlog = Annotated[int, Bounds(0, 128)]
@check_bounds
def bind(port: Port, backlog: Backlog) -> str: ...
bind(70000, 10) # mypy accepts this, and check_bounds raises ValueError
Annotated[int, Bounds(1, 65535)] is the type int with a metadata object
attached. mypy treats it as a plain int. The Bounds object is there for
your own code to read at runtime. bind only builds a string such as
"0.0.0.0:80/10". It opens no socket.
Read the metadata
get_type_hints(fn) returns the type of each parameter with the metadata
removed. get_type_hints(fn, include_extras=True) keeps it. An
Annotated hint then has a __metadata__ tuple that holds the metadata
objects in the order you wrote them.
Keep Port and Backlog as plain assignments. A type Port = ...
statement would hide the metadata from get_type_hints.
What to write
Write check_bounds with the ParamSpec signature from the previous
exercise:
def check_bounds[**P, R](fn: Callable[P, R]) -> Callable[P, R]: ...
When it decorates a function, read the hints with include_extras=True and
get inspect.signature(fn). Apply functools.wraps(fn) to the wrapper. On
every call, the wrapper:
- Binds the arguments with
signature.bind(*args, **kwargs), then callsapply_defaults(). If binding fails, itsTypeErrorpropagates. - Goes through the bound arguments in the order of the signature, and
through the metadata of each one in order. It skips anything that is not
a
Bounds. - Raises
ValueErrorat the firstintvalue outside the range, both ends included, without callingfn. The message is<name>=<value> outside [<lo>, <hi>], for exampleport=70000 outside [1, 65535]. - Otherwise calls
fnonce and returns its result.
So when both values are bad, the error names the port, even if the caller
passes backlog first as a keyword.
Then write metadata_visible(param). It returns two booleans. They say
whether the hint for param on bind has __metadata__ in
get_type_hints(bind), and in get_type_hints(bind, include_extras=True).
Both "port" and "backlog" give (False, True). An unknown name raises
KeyError.
Write the driver
solve(ports, backlogs) pairs the lists with zip(..., strict=True) and
calls bind on each pair, in order. It appends the result, or
f"rejected:{exc}" when bind raises ValueError, and moves on. Lists of
different lengths must still raise the ValueError from zip. Return the
results and the two flags from
metadata_visible("port"):
assert solve([80, 0, 443], [10, 5, 129]) == {
"results": ["0.0.0.0:80/10", "rejected:port=0 outside [1, 65535]",
"rejected:backlog=129 outside [0, 128]"],
"plain_has_metadata": False,
"extras_has_metadata": True,
}
See Annotated in the Python documentation.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.