mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-09-11 10:26:38 +00:00
fix: unify mypy environments for local checks and CI (#1291)
The isolated pre-commit mypy hook previously omitted runtime type information that make mypy used, hiding errors involving dependencies such as Pydantic and Pendulum. Makefile, pre-commit and CI now run the same full-project typing policy in the development environment defined by uv.lock. - Use uv run --locked --exact --extra dev and the same mypy arguments for Makefile and the local hook. Check all of src and tests, including on configuration-only changes. - Pin Python 3.13 for local development and the pre-commit CI job, and install the locked pre-commit version in CI. - Disable incremental analysis because existing Pendulum cache state changes mypy 2.3.1 diagnostics. Document the policy, the performance tradeoff and the existing typing debt. - Add a regression test that exercises Makefile, the hook and the CI command in a temporary project, accepting valid dependency types and detecting deliberate Pydantic/Pendulum assignment errors. Resolve the newly detected mypy diagnostics. - Enable the numpydantic and Pydantic mypy plugins, retaining strict Pydantic constructor typing with init_typed = true. Validate raw/coercible payloads through model_validate. - Propagate concrete record, provider and time-window types through generic collections, factories and lookup methods. Preserve runtime field inspection and generated time-window documentation. - Align Pendulum annotations with actual factory/arithmetic results while retaining Pydantic validation adapters at runtime. Correct optional values, array boundaries, REST handlers and plotting interfaces. - Add pinned scipy-stubs and types-psutil, update uv.lock, and supply the plugins' dependencies. - Add runtime regression coverage for validated path defaults, normalized time-series metadata, generic field inspection, invalid timestamps and unsupported provider imports. Runtime and compatibility details: - Validate path defaults as Path objects while retaining raw string defaults needed by migration serialization with exclude_defaults. - Normalize feed-in tariff lists and default charge rates to NumPy arrays; reject missing timestamps/uninitialized values explicitly. Importing into a provider without import support returns HTTP 400. - Public JSON schemas and OpenAPI structure match main (excluding the generated version). Signed-off-by: dr-dimitry Signed-off-by: dr-dimitry Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com> Co-authored-by: dr-dimitri <87113560+dr-dimitri@users.noreply.github.com> Co-authored-by: Normann <github@koldrack.com>
This commit is contained in:
co-authored by
dr-dimitri
Normann
parent
5b584cbb57
commit
1abdd345c4
@@ -8,7 +8,7 @@ import re
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Type, Union, get_args
|
||||
from typing import Any, Optional, Type, TypeVar, Union, get_args
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.fields import ComputedFieldInfo, FieldInfo
|
||||
@@ -24,8 +24,8 @@ from akkudoktoreos.core.coreabc import get_config, singletons_init
|
||||
from akkudoktoreos.core.pydantic import PydanticBaseModel
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
documented_types: set[PydanticBaseModel] = set()
|
||||
undocumented_types: dict[PydanticBaseModel, tuple[str, list[str]]] = dict()
|
||||
documented_types: set[type[PydanticBaseModel]] = set()
|
||||
undocumented_types: dict[type[PydanticBaseModel], tuple[str, list[str]]] = dict()
|
||||
|
||||
global_config_dict: dict[str, Any] = dict()
|
||||
|
||||
@@ -61,6 +61,9 @@ def get_body(config: type[PydanticBaseModel]) -> str:
|
||||
def resolve_nested_types(field_type: Any, parent_types: list[str]) -> list[tuple[Any, list[str]]]:
|
||||
resolved_types: list[tuple[type, list[str]]] = []
|
||||
|
||||
if isinstance(field_type, TypeVar):
|
||||
field_type = field_type.__bound__ or Any
|
||||
|
||||
origin = getattr(field_type, "__origin__", field_type)
|
||||
if origin is Union:
|
||||
for arg in getattr(field_type, "__args__", []):
|
||||
@@ -167,7 +170,7 @@ def build_nested_structure(keys: list[str], value: Any) -> Any:
|
||||
|
||||
def get_default_value(field_info: Union[FieldInfo, ComputedFieldInfo], regular_field: bool) -> Any:
|
||||
default_value = ""
|
||||
if regular_field:
|
||||
if regular_field and isinstance(field_info, FieldInfo):
|
||||
if (val := field_info.default) is not PydanticUndefined:
|
||||
default_value = val
|
||||
else:
|
||||
@@ -177,8 +180,13 @@ def get_default_value(field_info: Union[FieldInfo, ComputedFieldInfo], regular_f
|
||||
return default_value
|
||||
|
||||
|
||||
def get_type_name(field_type: type) -> str:
|
||||
def get_type_name(field_type: Any) -> str:
|
||||
type_name = str(field_type).replace("typing.", "").replace("pathlib._local", "pathlib")
|
||||
# Unparameterized Pydantic generics validate against their TypeVar bound.
|
||||
for arg in get_args(field_type):
|
||||
if isinstance(arg, TypeVar) and isinstance(arg.__bound__, type):
|
||||
bound = arg.__bound__
|
||||
type_name = type_name.replace(str(arg), f"{bound.__module__}.{bound.__qualname__}")
|
||||
if type_name.startswith("<class"):
|
||||
type_name = field_type.__name__
|
||||
return type_name
|
||||
@@ -229,17 +237,14 @@ def generate_config_table_md(
|
||||
table += f"| ---- {env_header_underline}| ---- | --------- | ------- | ----------- |\n"
|
||||
|
||||
|
||||
fields = {}
|
||||
for field_name, field_info in config.model_fields.items():
|
||||
fields[field_name] = field_info
|
||||
for field_name, field_info in config.model_computed_fields.items():
|
||||
fields[field_name] = field_info
|
||||
fields: dict[str, FieldInfo | ComputedFieldInfo] = dict(config.model_fields)
|
||||
fields.update(config.model_computed_fields)
|
||||
for field_name in sorted(fields.keys()):
|
||||
field_info = fields[field_name]
|
||||
regular_field = isinstance(field_info, FieldInfo)
|
||||
|
||||
config_name = field_name if extra_config else field_name.upper()
|
||||
field_type = field_info.annotation if regular_field else field_info.return_type
|
||||
field_type = (field_info.annotation if isinstance(field_info, FieldInfo) else field_info.return_type)
|
||||
default_value = get_default_value(field_info, regular_field)
|
||||
description = config.field_description(field_name)
|
||||
deprecated = config.field_deprecated(field_name)
|
||||
@@ -462,6 +467,8 @@ def write_to_file(file_path: Optional[Union[str, Path]], config_md: str):
|
||||
|
||||
# Assure timezone name does not leak to documentation
|
||||
tz_name = to_datetime().timezone_name
|
||||
if tz_name is None:
|
||||
raise RuntimeError("Documentation generation requires a timezone name")
|
||||
config_md = re.sub(re.escape(tz_name), "Europe/Berlin", config_md, flags=re.IGNORECASE)
|
||||
# Also replace UTC, as GitHub CI always is on UTC
|
||||
config_md = re.sub(re.escape("UTC"), "Europe/Berlin", config_md, flags=re.IGNORECASE)
|
||||
|
||||
@@ -5,10 +5,13 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import git
|
||||
|
||||
if __package__ is None or __package__ == "":
|
||||
if TYPE_CHECKING:
|
||||
from . import generate_openapi
|
||||
elif __package__ is None or __package__ == "":
|
||||
# uses current directory visibility
|
||||
import generate_openapi
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user