You are given a skeleton for a small Python "feature" framework. The goal is to use metaprogramming to:
-
Parse type-annotated classes into
Feature
objects via a
@features
decorator.
-
Parse functions into
Resolver
objects via a
@resolver
decorator.
-
Implement an
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
Step 1: Parse features from annotated classes
Implement the @features class decorator so that:
-
For each type-annotated attribute in the decorated class, you create a
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
).
-
The decorated class has a class attribute
features
which is a list of all
Feature
instances for that class in declaration order.
-
Each annotated attribute of the class (e.g.,
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()
Step 2: Parse resolvers from functions
Resolvers are functions that compute the value of one Feature from other Features. You must implement the @resolver decorator so that it:
-
Wraps the original function in a
Resolver
dataclass instance.
-
Extracts the input
Feature
objects from the function's parameter annotations in order.
-
Extracts the output
Feature
from the function's return annotation.
-
Stores the original function in
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.
Step 3: Implement the execution engine
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
-
optionally, any alternative keys you choose to support, as long as the provided tests pass.
-
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.
-
It must:
-
Use the registered
Resolver
objects created by the
@resolver
decorator.
-
Repeatedly apply resolvers whose input features are all known until all requested outputs are computed or no further progress can be made.
-
For the cases in the tests below, successfully compute
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.