You are given a skeleton for a small Python "feature" framework. The goal is to use metaprogramming to:
Feature
objects via a
@features
decorator.
Resolver
objects via a
@resolver
decorator.
execute
function that, given some known input feature values and desired output features, automatically computes the outputs by chaining resolvers.
You must implement the parts marked Implement me! so that all the tests at the bottom pass.
You may find the following tools useful (but they are not required):
cls.__annotations__
cls.__name__
setattr(obj, key, value)
inspect.signature(fn)
typing.get_type_hints
Implement the @features class decorator so that:
Feature
instance with:
name
equal to
"<ClassName>.<field_name>"
(for example,
"User.name"
).
typ
equal to the Python type of that attribute (resolving forward references like the string annotation
"User"
to the actual class
User
).
features
which is a list of all
Feature
instances for that class in declaration order.
User.id
,
User.email
) evaluates to a
Feature
instance with the corresponding
name
and
typ
, so that expressions like
User.id == Feature(name="User.id", typ=int)
are true.
Skeleton and tests:
import dataclasses
@dataclasses.dataclass(unsafe_hash=True)
class Feature:
name: str # e.g. "User.name"
typ: type # e.g. str
def features(cls):
"""Class decorator. Implement me!
After decoration:
- <Class>.features is a list[Feature]
- <Class>.<field_name> is a Feature instance
"""
return cls
@features
class Card:
id: int
number: str
owner: "User" # forward reference
@features
class User:
id: int
email: str
name: str
card_id: int
is_fraud: bool
def test_scalars():
assert User.features == [
Feature(name="User.id", typ=int),
Feature(name="User.email", typ=str),
Feature(name="User.name", typ=str),
Feature(name="User.card_id", typ=int),
Feature(name="User.is_fraud", typ=bool),
], User.features
def test_forward_reference():
assert Card.features == [
Feature(name="Card.id", typ=int),
Feature(name="Card.number", typ=str),
Feature(name="Card.owner", typ=User), # Note: typ should be the class User, not the string "User"
], Card.features
def test_properties():
assert User.id == Feature(name="User.id", typ=int)
test_scalars()
test_forward_reference()
test_properties()
Resolvers are functions that compute the value of one Feature from other Features. You must implement the @resolver decorator so that it:
Resolver
dataclass instance.
Feature
objects from the function's parameter annotations in order.
Feature
from the function's return annotation.
Resolver.fn
so that it can be called later.
Assume resolver functions are annotated using Feature objects like User.id, User.name etc., for both parameters and return type.
Skeleton and tests:
import inspect
import dataclasses
from typing import Callable, Any, TypeVar, ParamSpec, Generic
P = ParamSpec("P")
T = TypeVar("T")
@dataclasses.dataclass
class Resolver(Generic[P, T]):
inputs: list[Feature]
output: Feature
fn: Callable[P, T]
def resolver(fn: Callable[P, T]) -> Resolver[P, T]:
"""Function decorator. Implement me!
Should return a Resolver instance whose:
- inputs: parameter annotations (Feature objects) in order.
- output: return annotation (a Feature object).
- fn: the original function.
"""
return fn
@resolver
def get_user_name(id: User.id) -> User.name:
if id == 1:
return "elliot"
return "joe"
@resolver
def get_user_email(id: User.id) -> User.email:
if id == 1:
return "elliot@chalk.ai"
return "fraudster@chalk.ai"
@resolver
def get_user_fraud_score(name: User.name, email: User.email) -> User.is_fraud:
return name.lower() not in email.lower()
def test_resolvers():
assert get_user_name.output == User.name, get_user_name.output
assert get_user_name.inputs == [User.id], get_user_name.inputs
assert get_user_email.output == User.email, get_user_email.output
assert get_user_email.inputs == [User.id], get_user_email.inputs
assert get_user_fraud_score.output == User.is_fraud
assert get_user_fraud_score.inputs == [User.name, User.email]
test_resolvers()
You may additionally choose to register all created Resolver instances in some global registry so that execute (in Step 3) can find them, but the exact registration mechanism is up to you as long as the behavior matches the tests.
Implement the execute function that computes desired output features from a set of known inputs by chaining together resolvers defined using @resolver.
Requirements:
inputs
is a dictionary mapping either:
Feature
objects to their concrete values (e.g.,
{User.id: 1}
), or
outputs
is a list of requested outputs, each being either a
Feature
or something you map to a
Feature
.
execute
returns a dictionary
{Feature: value}
for all requested output features.
Resolver
objects created by the
@resolver
decorator.
User.is_fraud
from
User.id
by chaining through the appropriate resolvers.
Skeleton and tests:
def execute(inputs: dict[Feature | Any, Any], outputs: list[Feature | Any]) -> dict[Feature, Any]:
"""Implement me!
Example:
execute(inputs={User.id: 1}, outputs=[User.is_fraud]) == {User.is_fraud: False}
"""
return {}
def test_execute():
assert execute(
inputs={User.id: 1},
outputs=[User.is_fraud],
) == {User.is_fraud: False}
assert execute(
inputs={User.id: 2},
outputs=[User.is_fraud],
) == {User.is_fraud: True}
test_execute()
Your task: implement features, resolver, and execute so that all the provided tests (test_scalars, test_forward_reference, test_properties, test_resolvers, and test_execute) pass without modifying the tests themselves.