Created
April 18, 2020 15:56
-
-
Save pauleveritt/6cac82ab73be9d78c3c378318cb98f23 to your computer and use it in GitHub Desktop.
Writeup of using Protocols for pluggable components.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
I'm interested Python and the Modern Web. Including, a modern | |
approach to templating: developer-focused, closer to Python, | |
where code quality tools can help. Draft: | |
https://viewdom-wired.readthedocs.io/en/latest/why.html | |
I'm working on pluggable "components", as that link shows. Normally | |
I'd throw zope.interface at it, but PEP 544 Protocols is more realistic: | |
mypy support by default, some IDE integration to help DX, etc. | |
I'm using tagged/htm.py/viewdom/viewdom_wired as my layers for a | |
React-style component experience. My components usually (but not always) | |
are dataclasses, so they can express dependency injection: | |
https://viewdom-wired.readthedocs.io/en/latest/usage.html#hello-world | |
I'd like to do PEP 544 structural subtyping, rather than inheritance | |
tricks. Here's an example from Glyph showing a workaround for using | |
this with callables such as dataclasses: | |
https://github.com/python/mypy/issues/4717#issuecomment-454609539 | |
I could then use this approach behind a `@component` decorator to use `wired` | |
as a registry for pluggable components, with Protocols as the pluggability. | |
""" | |
from dataclasses import dataclass | |
# This starts the part that is in framework space, so civilians don't | |
# have to see it, their tooling, e.g. IDE autocomplete, just starts | |
# working. | |
from typing import Callable, Type, TypeVar | |
from typing_extensions import Protocol | |
from viewdom import html | |
protocol = TypeVar("protocol") | |
# In wired, this decorator would also make a registration in the registry, | |
# as well as a constructor which sniffs the dataclass fields to do the DI. | |
def component(c: Callable[[], protocol]) -> Callable[[Type[protocol]], Type[protocol]]: | |
def decor(input_value: Type[protocol]) -> Type[protocol]: | |
return input_value | |
return decor | |
# /end framework space | |
# This is done by the creator of some sexy library of ready-to-go | |
# components. The average consumer doesn't see this. | |
class Heading(Protocol): | |
logo_src: str | |
def __call__(self): | |
... | |
@component(Heading) | |
@dataclass | |
class GoodHeading: | |
logo_src: str | |
def __call__(self): | |
return html('<nav><{Logo} src={self.logo_src} /></nav>') | |
@component(Heading) | |
@dataclass | |
class BadHeading: | |
logo_height: int | |
def __call__(self): | |
return html('<nav><{Logo} src="logo.png" height={self.logo_height}/></nav>') | |
# /end component library author consumer | |
# I will leave out what the end-user will see, as it is specific to | |
# the work I'm doing. | |
if __name__ == '__main__': | |
good = GoodHeading(logo_src='logo.png') | |
bad = BadHeading(logo_height=50) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment