New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Env custom type casting #2330
Env custom type casting #2330
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking good 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better
Was just thinking about this... I think it would be a good idea to pass a "key" parameter to the converter, to make it possible to convert only a specific environment variable without converting others that would also be possible to convert. |
Can you provide a snippet of what that would look like as a dev? At that point, why not just convert yourself? |
Sure! That's what I thought this PR was meant to avoid, so this came to mind. In one of my codebases I currently have the following method, which is called every time the application is started. It's only use is to coerce values. ## config.py
from base64 import b64decode
from sanic import Sanic
def init(app: Sanic) -> None:
"""This is simply used to coerce the config values
into the correct type where necessary. Use environment
variables to set config values.
"""
config = app.config
if isinstance(config.JWT_SECRET, str):
config.JWT_SECRET = b64decode(config.JWT_SECRET) After thinking about it, I realised that passing the key may not be possible as there'd be no way to indicate "no, I don't want to coerce this", but the following implementation came to mind to replace the example above, where the value can be any callable: config = Config(coercers={"JWT_SECRET": b64decode})
app = Sanic(__name__, config=config)
@app.before_server_start
async def display(app, _):
assert isinstance(app.config.JWT_SECRET, bytes) |
Hmm.... 🤔 two thoughts:
Config(something={b64decode: "JWT_SECRET", foobar: ["FOO", "BAR"]}) Thinking more, maybe the answer is to just combine both concepts. class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[
Foo,
(b64decode, "JWT_SECRET"),
(foobar, "FOO", "BAR"),
])
# OR
config = Config(converters=[
Foo,
(b64decode, "JWT_SECRET"),
(foobar, ("FOO", "BAR")),
])
# OR
config = Config(converters={
Foo: None,
b64decode: "JWT_SECRET",
foobar: ("FOO", "BAR"),
}) I think the third option may be the winner. Pass a dict (maybe we also allow a simple list like in the original if NONE of them have key constraints) where:
|
I like it, either option works for me! What about if a key appears in more than one place though? That's why I suggested using they keys as key (maybe we can make it such that a |
I think it executes in the order they are listed. If there are multiple things that run on it, they would each run in turn. |
Looking at the code, it appears that converters are (more or less) global - I feel that this is very misleading, as instance methods do not normally modify class-level or global data. Rather than have a shared registry, it would probably be better to just have a |
That makes total sense. Nice call out. |
(thinking on it, the attribute would probably be better named as just Some other thoughts:
|
It really needs to be done upfront since environment variables are loaded when the application instance is instantiated. After that, there really is not so much use to them. This could be a possibility, but it is verbose. config = Config()
config.converters = [...]
app = Sanic(..., config=Config(converters=[datetime, UUID])) That was what I figured the most common use case would be.
💯
Now I need to go dust off my skills and clear the cobwebs from my brain. Its been a while. Will need to go see how they do it. Something in particular that sticks out for you? Another option is that we keep this PR much more limited akin to what it is doing now. Then move any further sophistication over to Sanic Extensions where I think we have more liberty to be cute with things. from sanic_ext import ConfigPlus
config = ConfigPlus()
# do whatever needs to be done
app = Sanic(..., config=config) |
Inspired by the pattern from @Varriount in #2321 and
app.router.register_pattern
, this PR adds the ability to register custom converters to cast values loaded from environment variables into a custom type.Example: