Skip to content

Instantly share code, notes, and snippets.

@pauleveritt
Created August 31, 2024 13:28
Show Gist options
  • Save pauleveritt/c90c01697def48bbd3d131bf26d20e8d to your computer and use it in GitHub Desktop.
Save pauleveritt/c90c01697def48bbd3d131bf26d20e8d to your computer and use it in GitHub Desktop.
Simulating Hopscotch in svcs
from dataclasses import dataclass
from pathlib import PurePath
from typing import Protocol, Callable, Any
import pytest
from svcs import Container, Registry
@dataclass
class Registration:
# Wish I could say type[Protocol] here
svc_type: Any
factory: Callable[..., object]
# Per-request overrides
context: Callable[..., object] | None = None
location: PurePath | None = None
@dataclass
class Registrations:
# These two go in the registry
system: list[Registration]
site: list[Registration]
# ---- Different kinds of people we might talk to
class Context(Protocol):
first_name: str
@dataclass
class CustomerContext:
first_name: str
@dataclass
class EmployeeContext:
first_name: str
# --- Different kinds of greetings we might give
class Greeting(Protocol):
salutation: str
@dataclass
class DefaultGreeting:
# The system default greeting
salutation: str = "Good Day"
# The site has two other kinds of context
@dataclass
class CustomerGreeting:
salutation: str = "Hello"
@dataclass
class EmployeeGreeting:
salutation: str = "Wassup"
# The site has a special greeting in the garden aisle
@dataclass
class GardenGreeting:
salutation: str = "Hot day outside"
@dataclass
class Request:
context: Context
location: PurePath
@dataclass
class RegistryRegistrations:
"""Registrations discovered at startup time from system and site."""
site: list[Registration]
system: list[Registration]
registry_registrations = RegistryRegistrations(
system=[
Registration(svc_type=Greeting, factory=DefaultGreeting),
],
site=[
Registration(svc_type=Greeting, factory=EmployeeGreeting, context=EmployeeContext),
Registration(svc_type=Greeting, factory=GardenGreeting, location=PurePath("/garden")),
]
)
@pytest.fixture
def registry() -> Registry:
registry = Registry()
# Process the non-context, non-location registrations, starting with system,
# then doing site (thus the latter has precedence.)
registrations = registry_registrations.system + registry_registrations.site
for r in registrations:
if r.location is None and r.context is None:
# Not request-dependent, so put it in the registry.
registry.register_factory(r.svc_type, r.factory)
return registry
def setup_container(registry: Registry, request: Request) -> Container:
# The big idea:
# 1. We've already collected all the "registrations" during a configuration
# step, e.g. with venusian.
# 2. We're processing a "request" which means creating a container.
# 3. Find the "best" request-specific registration, if any, and register
# locally in the container. But only if they match the request values.
# 4. Thus we won't have multiple container-local registrations for the same
# thing and then figure out which one is best.
container = Container(registry)
registrations = registry_registrations.system + registry_registrations.site
for r in registrations:
if (r.location is not None and request.location.is_relative_to(r.location)) or (
r.context is not None and isinstance(request.context, r.context)):
# This registration was EITHER location or context, and it matches the
# request value, so register it in container.
container.register_local_factory(r.svc_type, r.factory)
# TODO There's a smarter way to "score" registrations, dumb way for now.
for r in registrations:
if (r.location and request.location.is_relative_to(r.location)) and (r.context is request.context):
# This registration was for BOTH location and context, and it matches the
# request values, so register it.
container.register_local_factory(r.svc_type, r.factory)
# Finally, put the request in the container.
container.register_local_value(Request, request)
return container
def test_defaults(registry: Registry):
context = CustomerContext(first_name="Mary")
location = PurePath("/entrance")
request = Request(context=context, location=location)
container = setup_container(registry, request)
request: Request = container.get_abstract(Request)
assert request.context is context
assert request.location == location
greeting: Greeting = container.get_abstract(Greeting)
assert greeting.salutation == "Good Day"
def test_employee_context(registry: Registry):
# This is a site-local registration which depends on a value in
# the container.
context = EmployeeContext(first_name="Fred")
location = PurePath("/entrance")
request = Request(context=context, location=location)
container = setup_container(registry, request)
greeting: Greeting = container.get_abstract(Greeting)
assert greeting.salutation == "Wassup"
def test_employee_garden(registry: Registry):
# Match on both employee and location
context = EmployeeContext(first_name="Fred")
location = PurePath("/garden/plans")
request = Request(context=context, location=location)
container = setup_container(registry, request)
greeting: Greeting = container.get_abstract(Greeting)
assert greeting.salutation == "Hot day outside"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment