Skip to content

Commit

Permalink
Fix type checking problems with Pydantic dataclass by not using it.
Browse files Browse the repository at this point in the history
  • Loading branch information
aholmes committed Oct 2, 2023
1 parent 38cf2f6 commit ab66756
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 64 deletions.
5 changes: 2 additions & 3 deletions src/database/BL_Python/database/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from pydantic.dataclasses import dataclass
from pydantic import BaseModel


@dataclass(frozen=True)
class DatabaseConfig:
class DatabaseConfig(BaseModel):
connection_string: str = "sqlite:///:memory:"
sqlalchemy_echo: bool = False
55 changes: 18 additions & 37 deletions src/programming/BL_Python/programming/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,25 @@
from typing import TYPE_CHECKING, Any, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast

import toml
from BL_Python.programming.collections.dict import AnyDict, merge
from pydantic.dataclasses import dataclass


# isort: off
# fmt: off
# Fix this error
# https://bugs.python.org/issue45524
# https://github.com/Fatal1ty/mashumaro/issues/28
# Unfortunately Pydantic hides the PydanticDataclass
# behind `if TYPE_CHECKING`, which causes Python
# annotation inspection to fail with methods
# like, e.g. `get_type_hints`.
if TYPE_CHECKING:
import pydantic._internal._dataclasses # import PydanticDataclass
class PydanticDataclass:
pass
pydantic._internal._dataclasses.PydanticDataclass = PydanticDataclass
# fmt: on
# isort: on
class Config:
pass


class ConfigBuilder:
_root_config: type[Any] | None = None
_configs: list[type[Any]] | None = None

def with_root_config(self, config: "type[PydanticDataclass]"):
from pydantic import BaseModel

TConfig = TypeVar("TConfig")


class ConfigBuilder(Generic[TConfig]):
_root_config: "type[TConfig]" | None = None
_configs: "list[type[BaseModel]]" | None = None

def with_root_config(self, config: "type[TConfig]"):
self._root_config = config
return self

def with_configs(self, configs: "list[type[PydanticDataclass]]"):
def with_configs(self, configs: "list[type[BaseModel]]"):
self._configs = configs
return self

def build(self):
def build(self) -> type[TConfig]:
if self._root_config and not self._configs:
return self._root_config

Expand All @@ -64,18 +46,17 @@ def build(self):
attrs["__annotations__"] = annotations
# make one type that has the names of the config objects
# as attributes, and the class as their type
_new_type = type("GeneratedConfig", (_new_type_base,), attrs)

return dataclass(frozen=True)(_new_type)
_new_type = cast(
"type[TConfig]", type("GeneratedConfig", (_new_type_base,), attrs)
)


TConfig = TypeVar("TConfig")
return _new_type


def load_config(
toml_file_path: str,
config_overrides: AnyDict | None = None,
config_type: type[TConfig] = Config,
config_type: type[TConfig] = BaseModel,
) -> TConfig:
config_dict: dict[str, Any] = toml.load(toml_file_path)

Expand Down
13 changes: 10 additions & 3 deletions src/web/BL_Python/web/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
from logging import Logger
from os import environ, path
from typing import Any, Optional, cast
from typing import TYPE_CHECKING, Any, Optional, cast

import json_logging
from BL_Python.programming.config import Config, ConfigBuilder, load_config
Expand Down Expand Up @@ -39,11 +39,18 @@
_get_program_dir = lambda: path.dirname(get_path_executed_script())
_get_exec_dir = lambda: path.abspath(".")

# isort: off
# fmt: off
if TYPE_CHECKING:
from pydantic import BaseModel
# fmt: on
# isort: on


def create_app(
config_filename: str = "config.toml",
# FIXME should be a list of PydanticDataclass
application_configs: list[Any] | None = None,
application_configs: "list[type[BaseModel]] | None" = None,
application_modules: list[Module] | None = None
# FIXME eventually should replace with builders
# and configurators so this list of params doesn't
Expand All @@ -69,7 +76,7 @@ def create_app(
config_type = AppConfig
if application_configs is not None:
# fmt: off
config_type = ConfigBuilder()\
config_type = ConfigBuilder[AppConfig]()\
.with_root_config(AppConfig)\
.with_configs(application_configs)\
.build()
Expand Down
43 changes: 22 additions & 21 deletions src/web/BL_Python/web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
from typing import Any, Literal

from flask.config import Config as FlaskAppConfig
from pydantic.dataclasses import dataclass
#from pydantic.dataclasses import dataclass
from pydantic import BaseModel


@dataclass(frozen=True)
class LoggingConfig:
#@dataclass(frozen=True)
class LoggingConfig(BaseModel):
log_level: str = "INFO"


@dataclass(frozen=True)
class WebSecurityCorsConfig:
#@dataclass(frozen=True)
class WebSecurityCorsConfig(BaseModel):
origin: str | None = None
allow_credentials: bool = True
allow_methods: list[
Expand All @@ -30,27 +31,27 @@ class WebSecurityCorsConfig:
] = field(default_factory=lambda: ["GET", "POST", "OPTIONS"])


@dataclass(frozen=True)
class WebSecurityConfig:
#@dataclass(frozen=True)
class WebSecurityConfig(BaseModel):
cors: WebSecurityCorsConfig = WebSecurityCorsConfig()
csp: str | None = None


@dataclass(frozen=True)
class WebConfig:
#@dataclass(frozen=True)
class WebConfig(BaseModel):
security: WebSecurityConfig = WebSecurityConfig()


@dataclass(frozen=True)
class FlaskOpenApiConfig:
#@dataclass(frozen=True)
class FlaskOpenApiConfig(BaseModel):
spec_path: str | None = None
validate_responses: bool = False
use_swagger: bool = True
swagger_url: str | None = None


@dataclass(frozen=True)
class FlaskSessionCookieConfig:
#@dataclass(frozen=True)
class FlaskSessionCookieConfig(BaseModel):
# FIXME this needs to be handled much more securely.
# FIXME This is not done at the moment solely because we are not making
# FIXME active use of sessions, but this should not be forgotten!
Expand Down Expand Up @@ -82,8 +83,8 @@ class ConfigObject:
flask_app_config.from_object(ConfigObject)


@dataclass(frozen=True)
class FlaskSessionConfig:
#@dataclass(frozen=True)
class FlaskSessionConfig(BaseModel):
cookie: FlaskSessionCookieConfig
permanent: bool = True
lifetime: int | None = 86400
Expand Down Expand Up @@ -116,8 +117,8 @@ class ConfigObject:
flask_app_config.from_object(ConfigObject)


@dataclass(frozen=True)
class FlaskConfig:
#@dataclass(frozen=True)
class FlaskConfig(BaseModel):
app_name: str = "app"
env: str = "Development"
host: str | None = None
Expand All @@ -143,8 +144,8 @@ class ConfigObject:
flask_app_config.from_object(ConfigObject)


@dataclass(frozen=True)
class SAML2Config:
#@dataclass(frozen=True)
class SAML2Config(BaseModel):
metadata: str | None = None
metadata_url: str | None = None
relay_state: str | None = None
Expand All @@ -153,8 +154,8 @@ class SAML2Config:
logging: dict[str, Any] | None = None


@dataclass(frozen=True)
class Config:
#@dataclass(frozen=True)
class Config(BaseModel):
logging: LoggingConfig = LoggingConfig()
web: WebConfig = WebConfig()
flask: FlaskConfig | None = None
Expand Down

0 comments on commit ab66756

Please sign in to comment.