Hopscotch#
Writing a decoupled application ā a āpluggable appā ā in Python is a common practice.
Looking for a modern registry that scales from simple use, up to rich dependency injection (DI)?
hopscotch
is a registry and DI package for Python 3.9+, written to support research into component-driven development for Pythonās web story.
Letās Be Real
I expect a lot of skepticism. In fact, I donāt expect a lot of adoption. Instead, Iām using this to learn and write articles.
Features#
Simple to complex. The easy stuff for a simple registry is easy, but rich, replaceable systems are in scope also.
Better DX. Improve developer experience through deep embrace of static analysis and usage of symbols instead of magic names.
Hierarchical. A cascade of parent registries helps model request lifecycles.
Tested and documented. High test coverage and quality docs with lots of (tested) examples.- Extensible.
Great with components. When used with
viewdom
, everything is wired up and you can just work in templates.
Hopscotch takes its history from wired
, which came from Pyramid
, which came from Zope
.
Requirements#
Python 3.9+.
venusian (for decorators)
Installation#
You can install Hopscotch
via pip from PyPI:
$ pip install hopscotch
Quick Examples#
Letās look at: a hello world, same but with a decorator, replacement, and multiple choice.
Hereās a registry with one ākind of thingā in it:
# One kind of thing
@dataclass
class Greeter:
"""A simple greeter."""
greeting: str = "Hello!"
registry = Registry()
registry.register(Greeter)
# Later
greeter = registry.get(Greeter)
# greeter.greeting == "Hello!"
Thatās manual registration ā letās try with a decorator:
@injectable()
@dataclass
class Greeter:
"""A simple greeter."""
greeting: str = "Hello!"
registry = Registry()
registry.scan()
# Later
greeter = registry.get(Greeter)
# greeter.greeting == "Hello!"
Youāre building a pluggable app where people can replace builtins:
# Some site might want to change a built-in.
@injectable(kind=Greeter)
@dataclass
class CustomGreeter:
"""Provide a different ``Greeter`` in this site."""
greeting: str = "Howdy!"
Sometimes you want a Greeter
but sometimes you want a FrenchGreeter
ā for example, based on the row of data a request is processing:
@injectable(kind=Greeter, context=FrenchCustomer)
@dataclass
class FrenchGreeter:
"""Provide a different ``Greeter`` in this site."""
greeting: str = "Bonjour!"
# Much later
child_registry = Registry(
parent=parent_registry,
context=french_customer
)
greeter2 = child_registry.get(Greeter)
# greeter2.greeting == "Bonjour!"
Finally, have your data constructed for you in rich ways, including custom field āoperatorsā:
@injectable()
@dataclass
class SiteConfig:
punctuation: str = "!"
@injectable()
@dataclass
class Greeter:
"""A simple greeter."""
punctuation: str = get(SiteConfig, attr="punctuation")
greeting: str = "Hello"
def greet(self) -> str:
"""Provide a greeting."""
return f"{self.greeting}{self.punctuation}"
The full code for these examples are in the docs, with more explanation (and many more examples.)
And donāt worry, dataclasses arenāt required.
Some support is available for plain-old classes, NamedTuple
, and even functions.
Contributing#
Contributions are very welcome. To learn more, see the contributorās guide.
License#
Distributed under the terms of the MIT license, Hopscotch is free and open source software.
Issues#
If you encounter any problems, please file an issue along with a detailed description.
Credits#
This project was generated from @cjolowiczās Hypermodern Python Cookiecutter template.
Why Hopscotch
?#
Iām not convinced the world of Python wants registries (though they should.) Iām really not convinced Python wants another registry package ā there are already several, some that come really close to what I wanted. Registry plus dependency injection?
Cāmon, man.
This document tries to explain what itches are being scratched in Hopscotch. Remember: I donāt actually expect this stack of software to get adopted. Itās primarily for me to learn and articulate some ideas from the world of frontend development.
Why Not?#
Iāll start here. Things like registries are an indirection. All frameworks are by definition an indirection ā some mysterious force is calling your code and passing in arguments. Have you ever written a pytest test? If so, a registry is calling your code and even doing dependency injection!
Still, registries have a bad rap in Python. Inversion of control, dependency injection ā hell, when even type hints are ātoo much ceremonyā, you know something like Hopscotch is in left field.
Though times are kind of changing, thanks to FastAPI and its cohort.
Pluggable Apps#
My background in Zope has baked into my consciousness a love of pluggable apps. Whatās that? A āmostly doneā, out-of-the-box (OOTB) system with pieces that can be extended (add), replaced (overwritten), and varied (multiple implementations from which best-fit is chosen.) If youāve ever used Pyramid and seen its predicates ā thatās what I mean.
As an example, imagine a Sphinx (pluggable app) using a theme (plugin) in a site (local customization.) Iād like to change the breadcrumbs, but only in one class of thing, or one part of the site.
Fail Faster#
I want static analysis to help drive a better developer experience. Sitting in an editor, I want red warnings when I do something wrong.
āConvention over configurationā, with its magically-named variables and files, flies in the face of this. I want to see how far I can go with pluggable systems that express the lines you can paint within, via a smart editor.
Tooling#
In a related sense, I want CI to tell me ā or even better, people extending or using my downstream system ā when the rules are broken. If youāve ever written a Sphinx extension, youāll know ā itās magical names all the way down.
Caller-Callee Decoupling#
Again in Sphinx, if I want to extend something and there wasnāt a specially-designed facility (e.g. āput your list of sidebars here as stringsā), then I have to fork/monkeypatch the caller. If Iām writing to a plug point, and I need more information than what it will pass me, I have toā¦fork the caller.
Forking the caller is bad.
So instead, pluggable systems pass around a universe object, where you can get everything (the Sphinx app, the request object.)
Small Surface Area#
This is also bad. The callee now has a contract thatāsā¦kind of big. They might get passed more arguments than they want. It certainly makes test writing a contemplative exercise.
I want my callable arguments to say exactly what I depend on, no more, no less.
And later, I might want to cache/persist the results. In that case, I really donāt want to depend on the universe. How do you hash the universe?
Opportunities#
Registry-driven injection has some opportunities for fun ideas.
Frontend development is very, very rich in innovation. Views driven by immutable-state stores as reactive observersā¦itās not overkill, itās actually fodder for some real leaps forward.
One of my biggest goals is to have a component (injectable) constructed by āthe systemā which tracks what you depended on. If anything changes, we regenerate you, immutably. If youāre building a Sphinx site, you might render breadcrumbs once for a folder, and other items therein will use it.
And then, persist that rendered component, so when you wake up next timeā¦if nothing changed, youāre already built.
There are other places for cool thinking, likeā¦htmx. If the things you depend on are replaceable, you might have a āLayoutā component which knows how to write fragments to disk. Then, getting your htmx-driven views let you fetch smaller payloads.
Yeh, Sure, Back to jinja2
#
Itās asking too much for people to rethink templating in Python. But, Iāll tinker with it anyway.
Registry#
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.
Registering Things#
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"
Maybe 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ā Greeting
:
>>> 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.ā
Kind#
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 Greeting
.)
However, tooling ā editors, mypy
ā will complain that the function isnāt really a type-of Greeting
.
ā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 TypeVar
.
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 AnotherGreeting
, not Greeting
?
Yep.
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.
Parent Registries#
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.
Warning
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.)
Context#
Hooray, hereās kind of the whole point of Hopscotch: picking the ābestā implementation.
In our mythical web system, a request comes in for /customer/mary
.
Iām just guessing, but weāll probably get the mary
row from the Customer
database, as the primary object for that request.
Maybe the 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 context
:
>>> 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:
A
Greeting
to be used in the general caseA
AnotherGreeting
to be used when the customer is aFrenchCustomer
>>> registry = Registry()
>>> registry.register(Greeting)
>>> registry.register(AnotherGreeting, context=FrenchCustomer)
A request comes in with no context (or any context that isnāt FrenchCustomer
):
>>> child_registry = Registry(parent=registry, context=None)
>>> greeting = registry.get(Greeting)
>>> greeting.salutation
'Hello'
Another request comes in ā but itās for a FrenchCustomer
:
>>> 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 context=FrenchCustomer
.
Why?
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 get
.
Letās use a FrenchCustomer
, but with the parent registry that was created with context=None
:
>>> greeting = registry.get(Greeting, context=customer)
>>> greeting.salutation
'Another Hello'
Precedence#
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.
Decorator#
Imperative registration is definitely not-sexy.
Letās show use of the @injectable
decorator.
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.
Singletons#
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.ā
Warning
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.
Props#
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.
Injection#
Hopscotch has two faces: a registry for implementations and a dependency injector for construction.
What does that even mean?
In Hopscotch, hereās the flow:
Something asks for a ākind of thingāā¦maybe a component asks for a subcomponent
The registry gets the best implementation
The registry then calls that implementation, supplying it the arguments it is asking for
Some of those arguments might be ākinds of thingsā which also need instances
Later, when Iām brave, this will all be cached and persistent
If registries are an āOMG too much magicā thing in Python, then dependency injection is a āyouāre trying to make this into Javaā kind of thing. In Hopscotch, I want to show we can lower the bar to make the simple case really simple, especially for consumers.
Letās roll.
The Simplest Case#
We have a Greeter
who says hello with a greeting.
Actually, a Greeting
ā an instance of a class that is in āthe systemā.
@injectable()
@dataclass()
class Greeter:
"""A dataclass to engage a customer."""
greeting: Greeting
We can make a pluggable which has Greeting
and Greeter
in its registry:
>>> from hopscotch import Registry
>>> registry = Registry()
>>> registry.scan()
Now that weāre all setup, letās ask for a Greeter
:
>>> greeter = registry.get(Greeter)
>>> greeter.greeting.salutation
'Hello'
What happened?
We asked the registry for
Greeter
It found the ābest-fitā implementationā¦in this case,
Greeter
itselfThe registry started constructing an instance by introspecting its fields
The
Greeter.greeting
field had a type hint ofGreeting
The registry had an implementation for
Greeting
Since
Greeting
had a default value for its one field, it could be constructedThe registry used that constructed instance to construct
Greeter
Done
āWoah, dataclass magical mumbo jumbo!ā you say. Well, hereās an example using a plain-old-class:
class Greeter:
"""A plain-old-class to engage a customer."""
greeting: Greeting
def __init__(self, greeting: Greeting):
"""Construct a greeter."""
self.greeting = greeting
Hereās a NamedTuple
:
class Greeter(NamedTuple):
"""A ``NamedTuple`` to engage a customer."""
greeting: Greeting
ā¦but with a caveat: NamedTuple
and functions (next) have a little sharp edge regarding the ākindā discussion in Registry.
Hereās a function for Greeter
that can also be dependency injected:
class Greeter(NamedTuple):
"""A ``NamedTuple`` to engage a customer."""
greeting: Greeting
Even for the āsimpleā case, this is pretty valuable. Really de-coupled systems, where you can add things without monkey-patching and the callees get to decide what they need.
Manual Factory#
āToo much magic!ā
Itās true that the injector has a good number of policy decisions in the service of āhelping.ā
Perhaps youād like to keep injection, but have manual control over construction?
For that, provide a class method named __hopscotch_factory
:
@dataclass()
class GreetingFactory:
"""Use the ``__hopscotch_factory__`` protocol to control creation."""
salutation: str
@classmethod
def __hopscotch_factory__(cls, registry: Registry) -> GreetingFactory:
"""Manually construct this instance, instead of injection."""
return cls(salutation="Hi From Factory")
Generics#
Hopscotch injection works by the type hint.
Provide a type, Hopscotch tries to go get it and make an instance for you.
But those type hints can beā¦rich.
Hereās a Greeter
who can have an optional Greeting
:
@dataclass()
class GreeterOptional:
"""A dataclass to engage a customer with optional greeting."""
greeting: Optional[Greeting] # no default
Dataclasses especially have some extra generics to cover their fields.
Default Values#
When youāre constructing or calling something ā dataclass, plain old class, NamedTuple, function ā the parameters might have default values.
A dataclass might have a field with a default value:
class Greeting:
"""A dataclass to give a greeting."""
salutation: str = "Hello"
But so might a function:
def GreeterOptional(greeting: Optional[str]) -> Optional[str]:
"""A function to engage a customer with optional greeting."""
return greeting
The default value is the lowest-precedence option.
The injector tries to go get a value from the registry based on the field/parameterās type.
In these two cases, the type hint says str
which, obviously, wonāt be in the registry.
Operators#
Now, on to the part where Hopscotch actually adds to the status quo.
In some cases, we want a little transform between what weāre asking for and what weāre getting. For example, perhaps we have a registry with a context:
>>> registry = Registry()
>>> registry.register(Greeting)
>>> registry.register(AnotherGreeting, kind=Greeting)
>>> registry.register(Greeter)
Weād like to get Greeting
out, but we know itās really going to be AnotherGreeting
.
For this we can use an āoperatorā: a simple callable class which is given some inputs and returns an output:
@injectable()
@dataclass()
class GreeterGetAnother:
"""Use an operator to change the type hint of what's retrieved."""
customer_name: AnotherGreeting = get(Greeting)
What is get
?
Itās an āoperatorā:
@dataclass(frozen=True)
class Get:
"""Lookup a kind and optionally pluck an attr."""
lookup_key: Any
attr: Optional[str] = None
def __call__(
self,
registry: Registry,
) -> object:
"""Use registry to find lookup key and optionally pluck attr."""
# Can't lookup a string, ever, so bail on this with an error.
if isinstance(self.lookup_key, str):
lk = self.lookup_key
msg = f"Cannot use a string '{lk}' as container lookup value"
raise ValueError(msg)
result_value = registry.get(self.lookup_key)
# Are we plucking an attr?
if self.attr is not None:
result_value = getattr(result_value, self.attr)
return result_value
In this case, weāre saying: āSure, go get me a Greeting
, but actually, I know it is a AnotherGreeting
.ā
Hereās a super-useful variation: get me a Greeting
and then pluck the attribute Iām really looking for:
@dataclass()
class GreeterFirstName:
"""A dataclass that gets an attribute off a dependency."""
customer_name: str = get(Customer, attr="first_name")
Letās see it in action. I have a registry which registers a Customer
singleton and GreeterFirstName
, then gets a Greeter
:
>>> registry = Registry()
>>> customer = Customer(first_name="Mary")
>>> registry.register(customer) # A singleton
>>> registry.register(GreeterFirstName, kind=Greeter)
>>> greeter = registry.get(Greeter)
>>> greeter.customer_name
'Mary'
Operators act like a DSL, giving instructions to the injector. Since you can very easily write your own, it provides a nice way to concentrate your injectables on what they really need. Minimizing the surface area with the outside system has benefits.
Annotated#
We just discussed operators.
I lied a little: get
isnāt strictly an operator.
Itās actually a dataclasses.field
which stuffs some expected values ā namely, operator
ā in the metadata part of a field
.
This is actually syntactic sugar over a more verbose form that can be used outside dataclasses: plain old classes, NamedTuple
, and even functions.
For example, hereās a function that asks the registry to get the Customer
and pluck the first_name
:
def GreeterAnnotated(
customer_name: Annotated[str, Get(Customer, attr="first_name")]
) -> str:
"""A function to engage a customer with an ``Annotated``."""
return customer_name
Previously, the dataclasses.field
metadata communicated with the injector.
This uses PEP 593 ā Flexible function and variable annotations to give injector instructions.
The NamedTuple
and plain old classes also use this.
In fact, dataclasses can use Annotated
also, though thereās no real reason to.
As long as the operator has a āfieldā version ā Get
vs. get
ā itās a lot more convenient to use the latter.
Context#
Sometimes you want to inject a field that has an attribute off the context.
You canāt just say get(Context)
as there isnāt a Context
class registered on the registry.
Instead, itās an attribute.
Instead, thereās another operator for this: the Context
operator with its context
field:
@injectable(context=FrenchCustomer)
@dataclass()
class GreeterFrenchCustomer:
"""A dataclass that depends on a different registry context."""
customer: FrenchCustomer = context()
This does the moral equivalent of grabbing registry.context
.
It also supports passing in attr=
to pluck just one attribute.
Props#
Weāve seen how Hopscotch can gather the inputs needed to construct an instance: symbols in the registry, operators that return values, default values, etc.
Hopscotch was written to power ViewDOM
and its software for component-driven development.
In frontends, components are usually passed āpropsā in a particular usage.
Hopscotch also allows āpropsā: values passed in during registry.get()
which have the highest precedence.
Hereās an example of a registry with a Greeting
:
>>> registry = Registry()
>>> registry.register(Greeting)
Iām now in a request and, for some reason, I want to supply a specific salutation
, as a āpropā:
>>> greeting = registry.get(Greeting, salutation="Hello Prop")
>>> greeting.salutation
'Hello Prop'
What would that look like in ViewDOM?
In a template somewhere, youād say: <{Greeting} salutation="Hello Prop" //>
.
Same Olā Dataclasses#
As can be inferred, dataclasses are the āfirst-class citizen.ā
As such, thereās several aspects that are accommodated.
For example, fields that should be handled in a __post_init__
are deferred to let the dataclass handle that field, rather than injecting it.
inject_callable#
ViewDOM works well with Hopscotch, but doesnāt require it. You can have some utility components as functions that arenāt in the registry, because of the whole type/kind thing. Or, you can just skip completely the āreplaceable componentsā thing and just rely in symbols as the only implementation.
For this, Hopscotch lets you use the injector independently of the registry via the hopscotch.inject_callable
function.
Itās a bit more work: you have to supply a Registration
object that is the result of introspecting the target to be constructed.
(Thereās a function for that too.)
Future Work#
Just to reiterate: I donāt expect much adoption of this. Iām treating it less as āa maybe big package on PyPIā and more like ātopics to write articles about.ā
With that said, here are some places Iām interested in taking this:
Badass Sphinx āThemeā#
Hopscotch is the lowest layer. Above that are a number of other things, resulting in the real target. Iād like to make a really neat āthemeā that works with Sphinx, Pelican, Pyramid, and whatever. When I say āthemeāā¦itās a bit of a departure from whatās in Sphinx. Letās just say it that way.
Kind#
I would really like to crack the nut on protocols and really allow implementations that donāt subclass, but still fulfill the contract. Iām skeptical, though: mypy is just pretty overwhelmed with whatās on its plate.
One easy first step to improve the developer experience (DX) is to take a page out of Will McGuganās handbook and infer the type. Letās say we had this:
@injectable()
@dataclass
class AnotherHeading(Heading):
title: str
We could skip needing @injectable(kind=Heading)
with this logic:
Get the base classes
If that class is registered, register this class as a kind
Precedence#
My current scheme for deciding on the best implementation is pretty naive and brittle. Iād like to restructure the datastructure for the registrations ā for the hundredth time ā to make it more efficient, effective, and simple to get the best match.
Performance#
In a similar sense, lookups are going to be grossly less efficient than the standard Python āgo get me this function.ā I need it to be a little less gross and possibly rely on caching.
Iāve tried to think the entire time around ideas of immutability, making decisions up-front, doing work only once, etc. I can extend it to an idea of: take all the inputs, make a hashable named-tuple, and remember what came out.
Persistence#
āA good Gatsby for Pythonā has been a target of mine. Iād like re-render time to be super-fast, but also startup time. There are ways to remember the introspection results and only update them when software changes.
Reactive#
If youāre going to compete with Hugo, and youāre in Pythonā¦you have to do some tricks. The biggest being: do the minimal amount work needed on each operation.
Iād like a component system that remembers injection and scribbles down the observer and observable. Then, when the observable changes, go find everything that injected it, and update it.
Ambitious. Then again, frontend systems are on their 4th generation of these ideas.
Configuration Step#
At the moment, you can just keep adding registrations to a registry at any time.
Systems like Pyramid have an explicit configuration step which closes at some point. Hopscotch could benefit from this ā in performance, reliability, and simplicity ā by using this to re-compute more efficient datastructures in the registry. It could also implement the alternative to ākind=ā mentioned above.
Predicates#
Ahh, the really big win. Pyramid has a concept of registrations with predicates: extra kwargs of registration information. These are then used to find really specific best-fit registrations. For example, āuse this kind of Heading in this section of the site.ā
Iāve written this before, for Decate. Itās kind of fun and certainly useful.
Reference#
Hopscotch
only has a few public symbols to be used by other packages.
Hereās the API.
Registry#
The registry is the central part of Hopscotch.
It mimics the registry in wired
, Pyramid
, and Zope
(all 3 of which use Zopeās registry.)
- class hopscotch.Registry(parent=None, context=None)#
Type-oriented registry with special features.
- Parameters
parent (Optional[hopscotch.registry.Registry]) ā
context (Optional[Any]) ā
- Return type
None
- get(kind, context=None, **kwargs)#
Find an appropriate kind class and construct an implementation.
The passed-in keyword args act as āpropsā which have highest-precedence as arguments used in construction.
- Parameters
kind (Type[hopscotch.registry.T]) ā
context (Optional[Any]) ā
kwargs (dict[str, Any]) ā
- Return type
hopscotch.registry.T
- get_best_match(kind, context_class=None, allow_singletons=True)#
Find the best-match registration, if any.
Using the registry is a two-step process: lookup an implementation, then if needed, construct and return. This is the first part.
- Parameters
kind (Type[hopscotch.registry.T]) ā
context_class (Optional[Any]) ā
allow_singletons (bool) ā
- Return type
Optional[hopscotch.registry.Registration]
- inject(registration, props=None)#
Use injection to construct and return an instance.
- Parameters
registration (hopscotch.registry.Registration) ā
props (Optional[dict[str, Any]]) ā
- Return type
hopscotch.registry.T
- register(implementation, *, kind=None, context=None)#
Use a LIFO list for all the possible implementations.
Note that the implementation must be a subclass of the kind.
- Parameters
implementation (hopscotch.registry.T) ā
kind (Optional[Type[hopscotch.registry.T]]) ā
context (Optional[Any]) ā
- Return type
None
- scan(pkg=None)#
Look for decorators that need to be registered.
- Parameters
pkg (Optional[Union[module, str]]) ā
- Return type
None
- setup(pkg=None)#
Pass the registry to a package which has a setup function.
- Parameters
pkg (Optional[Union[module, str]]) ā
- Return type
None
injectable#
This decorator provides a convenient way for the venusian
-based scanner in the registry to recursively look for registrations.
- class hopscotch.injectable(kind=None, *, context=None)#
venusian
decorator to register an injectable factory .- Parameters
kind (Optional[Type[hopscotch.registry.T]]) ā
context (Optional[Optional[Any]]) ā
inject_callable#
Sometimes you want injection without a registry.
As an example, viewdom
works both with and without a registry.
For the latter, it does a simpler form of injection, but with many of the same rules and machinery.
- class hopscotch.inject_callable(registration, props=None, registry=None)#
Construct target with or without a registry.
- Parameters
registration (hopscotch.registry.Registration) ā
props (Optional[dict[str, Any]]) ā
registry (Optional[hopscotch.registry.Registry]) ā
- Return type
hopscotch.registry.T
Registration#
When using inject_callable
directly, you need to make an object with the introspected registration information.
This is the object to use.
- class hopscotch.Registration(implementation, kind=None, context=None, field_infos=<factory>, is_singleton=False)#
Collect registration and introspection info of a target.
- Parameters
implementation (Union[Callable[[...], object], object]) ā
kind (Optional[Callable[[...], object]]) ā
context (Optional[Callable[[...], object]]) ā
field_infos (list[hopscotch.field_infos.FieldInfo]) ā
is_singleton (bool) ā
- Return type
None
hopscotch.fixtures#
Hopscotch provides some fixtures for use in tests and examples.
DummyOperator#
Example objects for tests, examples, and docs.
- class hopscotch.fixtures.DummyOperator(arg)#
Simulate an operator that looks something up.
- Parameters
arg (str) ā
- Return type
None
Dataclass Examples#
Example objects and kinds implemented as dataclasses.
- class hopscotch.fixtures.dataklasses.AnotherGreeting(salutation='Another Hello')#
A replacement alternative for the default
Greeting
.- Parameters
salutation (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.Customer(first_name)#
The person to greet, stored as the registry context.
- Parameters
first_name (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.FrenchCustomer(first_name)#
A different kind of person to greet, stored as the registry context.
- Parameters
first_name (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.Greeter(greeting)#
A dataclass to engage a customer.
- Parameters
greeting (hopscotch.fixtures.dataklasses.Greeting) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterAnnotated(greeting)#
A dataclass to engage a customer with an annotation.
- Parameters
greeting (hopscotch.fixtures.dataklasses.Greeting) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterChildren(children)#
A dataclass that is passed a tree of VDOM nodes.
- Parameters
children (tuple[str]) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterCustomer(customer)#
A dataclass that depends on the registry context.
- Parameters
customer (hopscotch.fixtures.dataklasses.Customer) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterFirstName(customer_name)#
A dataclass that gets an attribute off a dependency.
- Parameters
customer_name (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterFrenchCustomer(customer)#
A dataclass that depends on a different registry context.
- Parameters
customer (hopscotch.fixtures.dataklasses.FrenchCustomer) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterGetAnother(customer_name)#
Use an operator to change the type hint of whatās retrieved.
- Parameters
customer_name (hopscotch.fixtures.dataklasses.AnotherGreeting) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterKind(greeting)#
A dataclass
Kind
to engage a customer.- Parameters
greeting (hopscotch.fixtures.dataklasses.Greeting) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterOptional(greeting)#
A dataclass to engage a customer with optional greeting.
- Parameters
greeting (Optional[hopscotch.fixtures.dataklasses.Greeting]) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreeterRegistry(registry)#
A dataclass that depends on the registry.
- Parameters
registry (hopscotch.registry.Registry) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.Greeting(salutation='Hello')#
A dataclass to give a greeting.
- Parameters
salutation (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingFactory(salutation)#
Use the
__hopscotch_factory__
protocol to control creation.- Parameters
salutation (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingFieldDefault(salutation='Default Argument')#
A dataclass with a field using a default argument.
- Parameters
salutation (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingFieldDefaultFactory(salutation=<factory>)#
A dataclass with a field using a default factory.
- Parameters
salutation (list) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingInitFalse#
A dataclass with a field that inits to false.
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingNoDefault(salutation)#
A dataclass to give a greeting with no default value.
- Parameters
salutation (str) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingOperator(greeter)#
A dataclass to give a greeting via an operator.
- Parameters
greeter (hopscotch.fixtures.dataklasses.Greeting) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingPath(location)#
A dataclass to give a builtin Path.
- Parameters
location (pathlib.Path) ā
- Return type
None
- class hopscotch.fixtures.dataklasses.GreetingTuple(salutation)#
A dataclass to give a sequence of greetings.
- Parameters
salutation (tuple[str, ...]) ā
- Return type
None
Function Examples#
Example objects and kinds implemented as functions.
- hopscotch.fixtures.functions.Greeter(greeting)#
A function to engage a customer.
- Parameters
greeting (str) ā
- Return type
str
- hopscotch.fixtures.functions.GreeterAnnotated(customer_name)#
A function to engage a customer with an
Annotated
.- Parameters
customer_name (str) ā
- Return type
str
- hopscotch.fixtures.functions.GreeterChildren(children)#
A function that is passed a tree of VDOM nodes.
- Parameters
children (tuple[str]) ā
- Return type
tuple[str]
- hopscotch.fixtures.functions.GreeterOptional(greeting)#
A function to engage a customer with optional greeting.
- Parameters
greeting (Optional[str]) ā
- Return type
Optional[str]
- hopscotch.fixtures.functions.Greeting(salutation='Hello')#
A function to give a greeting.
- Parameters
salutation (str) ā
- Return type
str
- hopscotch.fixtures.functions.GreetingDefaultNoHint(salutation='Hello')#
A function to with a parameter having no hint.
- Return type
str
- hopscotch.fixtures.functions.GreetingNoDefault(salutation)#
A function to give a greeting without a default.
- Parameters
salutation (str) ā
- Return type
str
NamedTuple
Examples#
Example objects and kinds implemented as NamedTuple
.
Note that, since typing.NamedTuple
doesnāt really do inheritance,
we canāt implement a Kind
as a NamedTuple
.
- class hopscotch.fixtures.named_tuples.Greeter(greeting)#
A
NamedTuple
to engage a customer.- Parameters
greeting (hopscotch.fixtures.named_tuples.Greeting) ā
- greeting: hopscotch.fixtures.named_tuples.Greeting#
Alias for field number 0
- class hopscotch.fixtures.named_tuples.GreeterAnnotated(greeting)#
A
NamedTuple
to engage a customer with an annotation.- Parameters
greeting (hopscotch.fixtures.named_tuples.Greeting) ā
- greeting: hopscotch.fixtures.named_tuples.Greeting#
Alias for field number 0
- class hopscotch.fixtures.named_tuples.GreeterChildren(children)#
A
NamedTuple
that is passed a tree of VDOM nodes.- Parameters
children (tuple[str]) ā
- children: tuple[str]#
Alias for field number 0
- class hopscotch.fixtures.named_tuples.GreeterOptional(greeting)#
A
NamedTuple
to engage a customer with optional greeting.- Parameters
greeting (Optional[hopscotch.fixtures.named_tuples.Greeting]) ā
- greeting: Optional[hopscotch.fixtures.named_tuples.Greeting]#
Alias for field number 0
Plain Old Class Examples#
Example objects and kinds implemented as plain classes.
- class hopscotch.fixtures.plain_classes.Greeter(greeting)#
A plain-old-class to engage a customer.
- Parameters
greeting (hopscotch.fixtures.plain_classes.Greeting) ā
- class hopscotch.fixtures.plain_classes.GreeterAnnotated(greeting)#
A plain-old-class to engage a customer with an annotation.
- Parameters
greeting (hopscotch.fixtures.plain_classes.Greeting) ā
- class hopscotch.fixtures.plain_classes.GreeterChildren(children)#
A plain-old-class that is passed a tree of VDOM nodes.
- Parameters
children (tuple[str]) ā
- class hopscotch.fixtures.plain_classes.GreeterKind(greeting)#
A plain-old-class
Kind
to engage a customer.- Parameters
greeting (hopscotch.fixtures.plain_classes.Greeting) ā
- class hopscotch.fixtures.plain_classes.GreeterOptional(greeting)#
A plain-old-class to engage a customer with optional greeting.
- Parameters
greeting (Optional[hopscotch.fixtures.plain_classes.Greeting]) ā
- class hopscotch.fixtures.plain_classes.Greeting#
A plain-old-class to give a greeting.
- class hopscotch.fixtures.plain_classes.GreetingNoDefault(salutation)#
A plain-old-class to give a greeting without a default.
- Parameters
salutation (str) ā
Contributor Guide#
Thank you for your interest in improving this project. This project is open-source under the MIT license and welcomes contributions in the form of bug reports, feature requests, and pull requests.
Here is a list of important resources for contributors:
How to report a bug#
Report bugs on the Issue Tracker.
When filing an issue, make sure to answer these questions:
Which operating system and Python version are you using?
Which version of this project are you using?
What did you do?
What did you expect to see?
What did you see instead?
The best way to get your bug fixed is to provide a test case, and/or steps to reproduce the issue.
How to request a feature#
Request features on the Issue Tracker.
How to set up your development environment#
You need Python 3.6+ and the following tools:
Install the package with development requirements:
$ poetry install
You can now run an interactive Python session, or the command-line interface:
$ poetry run python
$ poetry run hopscotch
How to test the project#
Run the full test suite:
$ nox
List the available Nox sessions:
$ nox --list-sessions
You can also run a specific Nox session. For example, invoke the unit test suite like this:
$ nox --session=tests
Unit tests are located in the tests
directory,
and are written using the pytest testing framework.
How to submit changes#
Open a pull request to submit changes to this project.
Your pull request needs to meet the following guidelines for acceptance:
The Nox test suite must pass without errors and warnings.
Include unit tests. This project maintains 100% code coverage.
If your changes add functionality, update the documentation accordingly.
Feel free to submit early, thoughāwe can always iterate on this.
To run linting and code formatting checks before commiting your change, you can install pre-commit as a Git hook by running the following command:
$ nox --session=pre-commit -- install
It is recommended to open an issue before starting work on anything. This will allow a chance to talk it over with the owners and validate your approach.
Contributor Covenant Code of Conduct#
Our Pledge#
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
Our Standards#
Examples of behavior that contributes to a positive environment for our community include:
Demonstrating empathy and kindness toward other people
Being respectful of differing opinions, viewpoints, and experiences
Giving and gracefully accepting constructive feedback
Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
The use of sexualized language or imagery, and sexual attention or advances of any kind
Trolling, insulting or derogatory comments, and personal or political attacks
Public or private harassment
Publishing othersā private information, such as a physical or email address, without their explicit permission
Other conduct which could reasonably be considered inappropriate in a professional setting
Enforcement Responsibilities#
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
Scope#
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
Enforcement#
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at pauleveritt@me.com. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
Enforcement Guidelines#
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
1. Correction#
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
2. Warning#
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
3. Temporary Ban#
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
4. Permanent Ban#
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
Attribution#
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozillaās code of conduct enforcement ladder.
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
MIT License#
Copyright Ā© 2021 Paul Everitt
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the āSoftwareā), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The software is provided āas isā, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.