Make Settings Pydantic and use the power of BaseSettings to simplify CLI#700
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.
Looks like settings_customise_sources in the docs?
There was a problem hiding this comment.
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.
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 |
| config_manager = ConfigManager() | ||
|
|
||
|
|
||
| def set_config(new_config: ApplicationConfig): |
There was a problem hiding this comment.
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.
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.
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.
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"] = NoneThere is nothing that makes this obvious to me as a developer.
There was a problem hiding this comment.
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.
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.
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.
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.