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:
Bobby Noelte
2026-09-10 23:20:35 +02:00
committed by GitHub
co-authored by dr-dimitri Normann
parent 5b584cbb57
commit 1abdd345c4
114 changed files with 2217 additions and 1322 deletions
+17 -17
View File
@@ -7,7 +7,7 @@ including edge cases, error handling, and timezone behavior.
import datetime
import json
import re
from typing import Any
from typing import Any, cast
from unittest.mock import MagicMock, patch
import babel
@@ -621,7 +621,7 @@ class TestToTime:
def test_to_time_invalid_input_type(self):
"""Test to_time with invalid input type."""
with pytest.raises(ValueError, match="Unsupported type"):
to_time({"invalid": "input"})
to_time(cast(Any, {"invalid": "input"}))
def test_to_time_invalid_hour_integer(self):
"""Test to_time with invalid hour as integer."""
@@ -657,7 +657,7 @@ class TestToTime:
def test_to_time_invalid_timezone_type(self):
"""Test to_time with invalid timezone type."""
with pytest.raises(ValueError, match="Invalid timezone"):
to_time("14:30", in_timezone=123)
to_time("14:30", in_timezone=cast(Any, 123))
def test_to_time_microseconds_precision(self):
"""Test to_time preserves microsecond precision."""
@@ -727,7 +727,7 @@ class TestTimeUtilityIntegration:
test_time: Time
# Test with string input
model = TestModel(test_time="14:30:45")
model = TestModel.model_validate(dict(test_time="14:30:45"))
assert isinstance(model.test_time, Time)
assert model.test_time.hour == 14
@@ -748,8 +748,8 @@ class TestTimeUtilityIntegration:
for case in test_cases:
# Both should produce the same result
direct_result = to_time(case)
model_result = TestModel(test_time=case).test_time
direct_result = to_time(cast(Any, case))
model_result = TestModel.model_validate(dict(test_time=case)).test_time
assert direct_result.hour == model_result.hour
assert direct_result.minute == model_result.minute
@@ -770,12 +770,12 @@ class ScheduleModel(PydanticBaseModel):
class TestPendulumTypes:
def test_valid_schedule_model(self):
model = ScheduleModel(
model = ScheduleModel.model_validate(dict(
start_time="14:30:00",
run_duration=to_duration("PT2H"),
scheduled_at=to_datetime("2025-07-04T09:00:00+02:00"),
run_on=to_datetime("2025-07-04")
)
))
assert isinstance(model.start_time, pendulum.Time)
assert isinstance(model.run_duration, pendulum.Duration)
@@ -788,12 +788,12 @@ class TestPendulumTypes:
assert model.run_on.to_date_string() == "2025-07-04"
def test_json_serialization(self):
model = ScheduleModel(
model = ScheduleModel.model_validate(dict(
start_time=pendulum.time(6, 15),
run_duration=pendulum.duration(minutes=45),
scheduled_at=pendulum.datetime(2025, 7, 4, 6, 15, tz="Europe/Berlin"),
run_on=pendulum.date(2025, 7, 4)
)
))
json_data = model.model_dump(mode="json")
assert "06:15:00" in json_data["start_time"]
@@ -809,30 +809,30 @@ class TestPendulumTypes:
def test_invalid_start_time(self):
with pytest.raises(ValidationError):
ScheduleModel(
ScheduleModel.model_validate(dict(
start_time="invalid",
run_duration="PT1H",
scheduled_at="2025-07-04T09:00:00+02:00",
run_on="2025-07-04"
)
))
def test_invalid_duration(self):
with pytest.raises(ValidationError):
ScheduleModel(
ScheduleModel.model_validate(dict(
start_time="10:00:00",
run_duration="2 hours", # invalid ISO 8601 duration
scheduled_at="2025-07-04T09:00:00+02:00",
run_on="2025-07-04"
)
))
def test_type_coercion(self):
dt = pendulum.datetime(2025, 7, 4, 12, 0)
model = ScheduleModel(
model = ScheduleModel.model_validate(dict(
start_time=pendulum.time(12, 0),
run_duration=pendulum.duration(hours=3),
scheduled_at=dt,
run_on=dt.date()
)
))
assert model.scheduled_at.hour == 12
assert model.run_duration.total_minutes() == 180
@@ -1424,7 +1424,7 @@ def test_hours_in_day(set_other_timezone, local_timezone, date, in_timezone, exp
"""Test the `test_hours_in_day` function."""
set_other_timezone(local_timezone)
date_input = to_datetime(date, in_timezone=in_timezone)
assert date_input.timezone.name == in_timezone
assert date_input.timezone_name == in_timezone
assert hours_in_day(date_input) == expected_hours