-
Notifications
You must be signed in to change notification settings - Fork 6
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
Make Settings Pydantic and use the power of BaseSettings to simplify CLI #700
Conversation
File "/workspaces/blueapi/src/blueapi/service/interface.py", line 21, in <module>
INTERNALERROR> _CONFIG: ApplicationConfig = ApplicationConfig() now it's the time when the use of global constants bites us |
@callumforrester why do we get another instance of """This module provides interface between web application and underlying Bluesky
context and worker"""
_CONFIG: ApplicationConfig = ApplicationConfig()
def config() -> ApplicationConfig:
return _CONFIG
def set_config(new_config: ApplicationConfig):
global _CONFIG
_CONFIG = new_config
|
@stan-dot that's for the subprocess, you need an instance in the main process, which is passed down to the subprocess via |
I think that the why would we want it to be None? Pydantic expects it to not be None. I haven't worked with python subprocesses before and not sure how to debug successfully |
src/blueapi/config.py
Outdated
) | ||
|
||
@classmethod | ||
def customize_sources(cls, init_settings, env_settings, file_secret_settings): |
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.
Looks like settings_customise_sources
in the docs?
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.
class Settings(BaseSettings):
foobar: str
nested: Nested
model_config = SettingsConfigDict(toml_file='config.toml')
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
return (TomlConfigSettingsSource(settings_cls),)
Looking at how a toml file is used by an equivalent
And
settings_customise_sources takes four callables as arguments and returns any number of callables as a tuple. In turn these callables are called to build the inputs to the fields of the settings class.
I think you need the signature to exactly match what is shown?
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.
yeah, it needs to be an override. Not sure why did this got changes
So currently the following sequence of events happens:
We could instead make it |
9ec6d18
to
bf47630
Compare
@callumforrester do main and subprocess share a global variable? you didn't mention where does step 2 happen - whether in main process or the subprocess |
@stan-dot No they can't share a global variable. The main process passes the idea into the subprocess via |
I thought usually it's values or references passed, not |
Can you tell I haven't finished my morning coffee yet? Honestly no idea what I intended to type there, "config", maybe? |
no, I cannot tell that. I'd appreciate concise technical terms used |
51d5f52
to
ab8aaf1
Compare
ab8aaf1
to
7cd6dea
Compare
very odd errors, trying to fix now |
trying to refactor the @callumforrester it looks like unlike the patterns I can see in the codebase for worker and rest api the It makes me feel confused, where can I read the ADR for this? |
huh, I'd need to refactor |
|
||
def config() -> ApplicationConfig: | ||
return _CONFIG | ||
config_manager = ConfigManager() | ||
|
||
|
||
def set_config(new_config: ApplicationConfig): |
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.
Could: Can we remove this now?
ApplicationConfig.model_config["yaml_file"] = config | ||
app_config = ApplicationConfig() # Instantiates with customized sources |
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.
Should: I'm concerned this. mutation of global state is an antipattern. Could you instead subclass the config?
ApplicationConfig.model_config["yaml_file"] = config | |
app_config = ApplicationConfig() # Instantiates with customized sources | |
class YamlConfig(ApplicationConfig): | |
model_config = {**ApplicationConfig.model_config, **{"yaml_file": config}} | |
app_config = YamlConfig() |
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.
that's how Pydantic uses it and this is not regular state but settings
also this is not Java
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.
Mutation of global state is an antipattern in any programming language (good thread).
Now I have to remember to address this if I use ApplicationConfig
somewhere else, just like you have:
ApplicationConfig.model_config["yaml_file"] = None
There is nothing that makes this obvious to me as a developer.
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.
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.
the mutable settings option is here:
and we're not using it
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#removing-sources
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.
It looks like what we're trying to do was just not a well-envisioned use case: pydantic/pydantic-settings#346
Should maybe reconsider the use of BaseSettings
and/or raise a PR upstream?
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.
that is true, turns out that pydantic-settings isn't as well-finished product like pydantic itself
|
||
def __init__(self, config: ApplicationConfig = None): | ||
if config is None: | ||
ApplicationConfig.model_config["yaml_file"] = None |
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.
Should: Again, I think you should find an alternative to mutating global state, if you fix the instance in CLI then you shouldn't need this line.
Since instance level file configuration for BaseSettings isn't going to be in pydantic_settings until v3; and v3 will release at some time next year; and that the work on this in pydantic_settings has not yet begun, I think we need to decide either to shelve this until such time we can instantiate our settings in a way we're happy with, or consider other workarounds. We could, for example, deprecate the ability to pass See also: pydantic/pydantic-settings#259 for various approaches. |
should this be closed as not at the best time now @callumforrester ? if so I think it's best that I also unassign myself from the |
@stan-dot happy for you to do so, thanks for your work so far |
Fixes #495
Using Pydantic for logic to do with file loading. It has a bit of magic but will lower our lines of code. ApplicationConfig will extend BaseSettings, while the nested configs like ScratchConfig are still BlueapiBaseModels.