The registry holds implementations of “kinds of things”. It then lets you retrieve an implementation, possibly doing injection along the way.
Creating a Registry#
It’s quite simple to create a registry:
>>> from hopscotch import Registry >>> registry = Registry()
You can also create with a parent and with a context. These are both discussed below.
Using a Registry#
Hopscotch comes from the Pyramid family, which doesn’t like module-level globals for the “app”. Usually your registry would become part of your “app” and passed around as needed.
For example, when using
viewdom, the registry is passed around behind the scenes – one of the benefits of DI.
Once you have a registry, you can put something into it “imperatively”, meaning, by calling a method on that object. For example, imagine you had this code:
class Greeting: """A dataclass to give a greeting.""" salutation: str = "Hello"
Greeting ships with your pluggable app.
But you want to allow a local site to replace it with a different greeting:
@dataclass() class AnotherGreeting(Greeting): """A replacement alternative for the default ``Greeting``.""" salutation: str = "Another Hello"
Easy: they just grab the registry and register their custom
AnotherGreeting, telling the registry it’s a “kind of”
>>> registry.register(AnotherGreeting, kind=Greeting)
As a note, there’s nothing magical about dataclasses at this point. You could just as easily use a “plain old class.”
What’s up with that word “kind”? At this point, it’s a hedge because I can’t make up my mind if the registry will be about types.
As we see below, you can
.get something from the registry.
What is this “something”?
Well, it’s the best implementation for the situation, out of the registered implementations.
But you should get back something that, type-wise, is a “kind of” the thing you asked for, to get the developer experience benefits of static analysis.
It would be great if “kind” didn’t have to mean “subclass”.
You could register things that really had no implementation coupling (inheritance) with the thing they were replacing.
In fact, you could use a
NamedTuple or even a function.
(Historical fact: you can actually register a function as an implementation of
However, tooling – editors,
mypy – will complain that the function isn’t really a type-of
“A-ha” you say, “that’s what PEP 544 protocols are for.”
That’s what I thought too.
But protocols can’t be used in all the places a type can – for example, a
So I’m currently stuck with Frankenkind. For now, it’s “subtype.”
Retrieving From A Registry#
Super, we now have a place to store implementations. How do we get one out?
>>> greeting = registry.get(Greeting) >>> greeting.salutation 'Another Hello'
Hmm, I got
There were two implementations.
The most recent one – the second one – out-prioritized the earlier one (which is still in the registry.)
And another “Hmm”…I got back an instance, not the class. Yep. The registry constructs your instances. These dataclasses had default values on the fields, so nothing was needed.
We all work with web-based systems. There’s a startup phase, then when a request comes in, a request-response phase. The startup information should be setup once, then the per-request information stored and discarded.
Hopscotch has a hierarchical registry. When you create a registry, you can pass a parent:
>>> child_registry = Registry(parent=registry)
If you try to get something from the child, it will find the registration in the parent:
>>> greeting = child_registry.get(Greeting) >>> greeting.salutation 'Another Hello'
The injector is aware of parentage. When it goes to get something from the registry, it will walk up until it finds the first match.
I’m In Over My Head Hierarchical registries will ultimately be awesome. While they work now, it’s a “just barely” kind of thing. Getting it really right – high performance, lower complexity, caching, and multiprocess – will be hard. (But will be worth it.)
Hooray, here’s kind of the whole point of Hopscotch: picking the “best” implementation.
In our mythical web system, a request comes in for
I’m just guessing, but we’ll probably get the
mary row from the
Customer database, as the primary object for that request.
Customer looks like this:
@dataclass() class Customer: """The person to greet, stored as the registry context.""" first_name: str
Pyramid makes this a first-class idea called the request “context.”
If you put it to use, it’s really powerful.
For example, you can register a view that is custom to that “kind of thing.”
wired also keeps a similar idea in its registry.
Hopscotch does this by letting you, like
wired, create a registry with an optional
>>> customer = Customer(first_name="mary") >>> child_registry = Registry(parent=registry, context=customer) >>> registry.context is None True >>> child_registry.context.first_name 'mary'
To really see why this is useful, let’s start over with a registry that has two registrations:
Greetingto be used in the general case
AnotherGreetingto be used when the customer is a
>>> registry = Registry() >>> registry.register(Greeting) >>> registry.register(AnotherGreeting, context=FrenchCustomer)
A request comes in with no context (or any context that isn’t
>>> child_registry = Registry(parent=registry, context=None) >>> greeting = registry.get(Greeting) >>> greeting.salutation 'Hello'
Another request comes in – but it’s for a
>>> customer = FrenchCustomer(first_name="marie") >>> child_registry = Registry(parent=registry, context=customer) >>> greeting = child_registry.get(Greeting) >>> greeting.salutation 'Another Hello'
This time when we asked for
Greeting, we got the registration for
Because the child registry was created in a way that was “bound” to that as the registry context.
As a note, you can also manually provide a context when doing a
Let’s use a
FrenchCustomer, but with the parent registry that was created with
>>> greeting = registry.get(Greeting, context=customer) >>> greeting.salutation 'Another Hello'
The registry lets you register multiple implementations of a “kind.” How does the registry decide which to use?
I’ll be honest: the current implementation is sketchy, though it has potential. Basically, it looks through the current registry for matches (before going to the parent.) It eliminates those that don’t match the context. It then uses a “policy” to decide the best fit.
This can get better/richer/faster in the future.
Imperative registration is definitely not-sexy.
Let’s show use of the
Let’s again imagine we have a
Greeting, but let’s show the line before what we showed previously:
@injectable() @dataclass() # Start Greeting class Greeting: """A dataclass to give a greeting.""" salutation: str = "Hello"
When we create a registry this time, we’ll call
.scan() to go look for decorators:
>>> registry = Registry() >>> registry.scan()
As before, we can later get this:
>>> greeting = registry.get(Greeting, context=customer) >>> greeting.salutation 'Hello'
.scan() can be passed a symbol for a package/module, or a string for a package location.
It’s based on the scanner in
venusian which is a really cool way to defer registration until after import.
Sometimes you don’t need an instance constructed. The data is “outside” the system and there’s only one implementation and you already have the instance. That’s where singletons come in.
In Hopscotch you can register a singleton:
>>> greeting = Greeting(salutation="I am a singleton") >>> registry = Registry() >>> registry.register(greeting) >>> greeting = registry.get(Greeting) >>> greeting.salutation 'I am a singleton'
You can register a singleton as a “kind” and it will replace a built-in:
>>> greeting = AnotherGreeting() >>> registry = Registry() >>> registry.register(Greeting) >>> registry.register(greeting, kind=Greeting) >>> greeting = registry.get(Greeting) >>> greeting.salutation 'Hello'
Singletons get higher “precedence” than non-singleton registrations. This helps when you want to say: “Listen, this is the answer in this registry.”
Is This Dumb? I went back and forth on whether singletons should use the same method for registering and getting. I settled on “simpler DX”. But it makes the type hinting harder.
We’ll cover this more in injection, but as a placeholder….when you do a
registry.get() you can pass in kwargs to use in the construction.
These are called “props”, to mimic component-driven development.