-
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
base: main
Are you sure you want to change the base?
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 |
|
||
ctx.obj["config"] = loaded_config | ||
# Override default yaml_file path in the model_config if `config` is provided | ||
ApplicationConfig.model_config["yaml_file"] = config |
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.
Does this handle the case of tuple[Path, ...]
?
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.
not sure, not sure if we have a test for it at the moment.
might need to add some logic to handle that
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.
pydantic_settings
defines PathType as: PathType = Union[Path, str, Sequence[Union[Path, str]]]
model_config = SettingsConfigDict( | ||
env_nested_delimiter="__", yaml_file=DEFAULT_PATH, yaml_file_encoding="utf-8" | ||
) |
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: rather than setting this class variable then calling the constructor a constructor that takes a yaml file, or a class method?
e.g.
class ApplicationConfig(...):
def __init__(self, yaml_file: Path | tuple[Path, ...] | None = None):
self.model_config = SettingsConfigDict(env_nested_delimiter="__", yaml_file=DEFAULT_PATH or yaml_file, yaml_file_encoding="utf-8")
super().__init__()
# or
@classmethod
def from_yaml(cls, yaml_file: Path | tuple[Path, ...]):
cls.model_config = yaml_field
return cls()
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.
I'd prefer if instantiating the Config a second time isn't modified by instantiating it the first time.
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 is some Pydantic magic behind the scenes. I agree that this setup is odd but that's what the docs use
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#changing-priority
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.
I'm hesitant to change that as now we're using the settings_customize_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.
in pydantic_settings model_config is specified as a ClassVariable
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
extra='forbid',
arbitrary_types_allowed=True,
validate_default=True,
case_sensitive=False,
env_prefix='',
nested_model_default_partial_update=False,
env_file=None,
env_file_encoding=None,
env_ignore_empty=False,
env_nested_delimiter=None,
env_parse_none_str=None,
env_parse_enums=None,
cli_prog_name=None,
cli_parse_args=None,
cli_parse_none_str=None,
cli_hide_none_type=False,
cli_avoid_json=False,
cli_enforce_required=False,
cli_use_class_docs_for_groups=False,
cli_exit_on_error=True,
cli_prefix='',
cli_flag_prefix_char='-',
cli_implicit_flags=False,
cli_ignore_unknown_args=False,
json_file=None,
json_file_encoding=None,
yaml_file=None,
yaml_file_encoding=None,
toml_file=None,
secrets_dir=None,
protected_namespaces=('model_', 'settings_'),
)
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 |
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.