polish(sprint-0): clean code per ruff + mypy strict
Run the dev linters over Sprint 0's core/library/shared modules and address every finding. Behaviour unchanged; tests still 80/80 green. Changes: - Replace `class Foo(str, Enum)` with `class Foo(StrEnum)` (PEP 663 / Python 3.11+) in 7 enum classes: ActuatorType, AlarmSeverity, AlarmType, KnobMode, KnobFunction, AutopilotMode, AccessLevel, VesselType. Pydantic v2 serialises StrEnum the same way, so YAML/JSON round-trips are byte-identical. - Use `datetime.UTC` alias in place of `datetime.timezone.utc` (UP017) across alarms.py, knob_state.py, project_config.py, and test_knob_state.py. - Remove now-unnecessary forward-reference quotes from method return type annotations (UP037) — `from __future__ import annotations` is already in scope everywhere. - Tighten `_read_json_resource` / `_read_yaml_resource` in the library loader: validate that the deserialised payload is actually a dict before returning, instead of leaking `Any` from json.loads / yaml.safe_load. Fixes the only two `mypy --strict` findings. - Add `.claude/settings.local.json` to .gitignore (personal Claude Code overrides are not committed). Verification: ruff check arautopilot/ -> All checks passed mypy arautopilot/core library shared -> Success, 0 issues, 12 files pytest -> 80 passed in 0.25s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,12 @@ intentionally **not** enabled in Phase 1 configurations.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class ActuatorType(str, Enum):
|
||||
class ActuatorType(StrEnum):
|
||||
"""Actuator family driving the rudder.
|
||||
|
||||
Phase-1 supported families have a regular value. Reserved (Phase 3)
|
||||
|
||||
@@ -8,13 +8,13 @@ the autopilot (return to STANDBY).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class AlarmSeverity(str, Enum):
|
||||
class AlarmSeverity(StrEnum):
|
||||
"""IEC-style four-level severity scheme."""
|
||||
|
||||
EMERGENCY = "emergency"
|
||||
@@ -30,7 +30,7 @@ class AlarmSeverity(str, Enum):
|
||||
"""Informational only; ack with one tap."""
|
||||
|
||||
|
||||
class AlarmType(str, Enum):
|
||||
class AlarmType(StrEnum):
|
||||
"""The fixed catalogue of alarms emitted by the firmware.
|
||||
|
||||
Adding an entry here is a deliberate, reviewed change — the firmware
|
||||
@@ -89,7 +89,7 @@ class Alarm(BaseModel):
|
||||
type: AlarmType
|
||||
severity: AlarmSeverity
|
||||
message: str = Field(min_length=1, max_length=240)
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
source: str = Field(
|
||||
default="firmware",
|
||||
description="Subsystem that raised the alarm (e.g. 'firmware', 'display', 'vms').",
|
||||
@@ -105,7 +105,7 @@ class Alarm(BaseModel):
|
||||
*,
|
||||
message: str | None = None,
|
||||
source: str = "firmware",
|
||||
) -> "Alarm":
|
||||
) -> Alarm:
|
||||
"""Convenience constructor using catalogue defaults."""
|
||||
return cls(
|
||||
type=alarm_type,
|
||||
|
||||
@@ -11,13 +11,13 @@ happens in the ESP32 firmware (Sprint 7).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class KnobMode(str, Enum):
|
||||
class KnobMode(StrEnum):
|
||||
"""High-level state of the bridge knob."""
|
||||
|
||||
LIBRE = "libre"
|
||||
@@ -30,7 +30,7 @@ class KnobMode(str, Enum):
|
||||
"""Pending value being shown for operator confirmation."""
|
||||
|
||||
|
||||
class KnobFunction(str, Enum):
|
||||
class KnobFunction(StrEnum):
|
||||
"""The set of values the knob may target once armed."""
|
||||
|
||||
NONE = "none"
|
||||
@@ -84,7 +84,7 @@ class KnobState(BaseModel):
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _consistency(self) -> "KnobState":
|
||||
def _consistency(self) -> KnobState:
|
||||
if self.mode == KnobMode.LIBRE:
|
||||
if self.function != KnobFunction.NONE:
|
||||
raise ValueError("LIBRE mode requires function == NONE")
|
||||
@@ -105,7 +105,7 @@ class KnobState(BaseModel):
|
||||
|
||||
# --- Pure transition helpers (return new immutable states) --------------
|
||||
@classmethod
|
||||
def idle(cls) -> "KnobState":
|
||||
def idle(cls) -> KnobState:
|
||||
"""Construct the canonical idle (LIBRE) state."""
|
||||
return cls()
|
||||
|
||||
@@ -116,7 +116,7 @@ class KnobState(BaseModel):
|
||||
current_value: float,
|
||||
timeout_s: float = DEFAULT_ARMED_TIMEOUT_S,
|
||||
now: datetime | None = None,
|
||||
) -> "KnobState":
|
||||
) -> KnobState:
|
||||
"""Transition LIBRE → ARMADO for the given function."""
|
||||
if self.mode != KnobMode.LIBRE:
|
||||
raise ValueError(f"Cannot arm from mode {self.mode.value}")
|
||||
@@ -127,11 +127,11 @@ class KnobState(BaseModel):
|
||||
function=function,
|
||||
current_value=current_value,
|
||||
pending_value=None,
|
||||
armed_at=now or datetime.now(timezone.utc),
|
||||
armed_at=now or datetime.now(UTC),
|
||||
timeout_remaining_s=timeout_s,
|
||||
)
|
||||
|
||||
def propose(self, value: float, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> "KnobState":
|
||||
def propose(self, value: float, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> KnobState:
|
||||
"""Operator turned the knob: stage a pending value (ARMADO → CONFIRMANDO)."""
|
||||
if self.mode not in (KnobMode.ARMADO, KnobMode.CONFIRMANDO):
|
||||
raise ValueError(f"Cannot propose from mode {self.mode.value}")
|
||||
@@ -144,7 +144,7 @@ class KnobState(BaseModel):
|
||||
timeout_remaining_s=timeout_s,
|
||||
)
|
||||
|
||||
def confirm(self, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> "KnobState":
|
||||
def confirm(self, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> KnobState:
|
||||
"""Operator pressed to confirm; commits ``pending_value`` and stays armed."""
|
||||
if self.mode != KnobMode.CONFIRMANDO:
|
||||
raise ValueError(f"Cannot confirm from mode {self.mode.value}")
|
||||
@@ -157,6 +157,6 @@ class KnobState(BaseModel):
|
||||
timeout_remaining_s=timeout_s,
|
||||
)
|
||||
|
||||
def disarm(self) -> "KnobState":
|
||||
def disarm(self) -> KnobState:
|
||||
"""Force the knob back to LIBRE (timeout, alarm, mode change, long-press)."""
|
||||
return KnobState.idle()
|
||||
|
||||
@@ -7,10 +7,10 @@ compatibility.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class AutopilotMode(str, Enum):
|
||||
class AutopilotMode(StrEnum):
|
||||
"""The complete set of autopilot operating modes, across both phases.
|
||||
|
||||
Use :func:`is_available_in_phase` to query phase gating instead of
|
||||
|
||||
@@ -11,12 +11,12 @@ Mirrors section 6 of the brief:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class AccessLevel(str, Enum):
|
||||
class AccessLevel(StrEnum):
|
||||
"""Three-level RBAC for PID tuning (brief section 6)."""
|
||||
|
||||
OPERATOR = "operator"
|
||||
@@ -133,7 +133,7 @@ class PidConfig(BaseModel):
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_loop_frequencies(self) -> "PidConfig":
|
||||
def _check_loop_frequencies(self) -> PidConfig:
|
||||
if self.inner_loop_freq_hz <= self.outer_loop_freq_hz:
|
||||
raise ValueError(
|
||||
"inner_loop_freq_hz must be strictly greater than outer_loop_freq_hz "
|
||||
|
||||
@@ -8,7 +8,7 @@ transport into the firmware build pipeline.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -38,8 +38,8 @@ class ProjectConfig(BaseModel):
|
||||
|
||||
vessel: VesselConfig
|
||||
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
modified_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
modified_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
# --- Serialisation -------------------------------------------------------
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -75,19 +75,19 @@ class ProjectConfig(BaseModel):
|
||||
|
||||
# --- Deserialisation -----------------------------------------------------
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ProjectConfig":
|
||||
def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
|
||||
return cls.model_validate(data)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> "ProjectConfig":
|
||||
def from_json(cls, text: str) -> ProjectConfig:
|
||||
return cls.model_validate(json.loads(text))
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, text: str) -> "ProjectConfig":
|
||||
def from_yaml(cls, text: str) -> ProjectConfig:
|
||||
return cls.model_validate(yaml.safe_load(text))
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path | str) -> "ProjectConfig":
|
||||
def load(cls, path: Path | str) -> ProjectConfig:
|
||||
"""Load from disk; format inferred from the file extension."""
|
||||
p = Path(path)
|
||||
text = p.read_text(encoding="utf-8")
|
||||
@@ -98,6 +98,6 @@ class ProjectConfig(BaseModel):
|
||||
return cls.from_json(text)
|
||||
raise ValueError(f"Unsupported project file extension: {suffix!r}")
|
||||
|
||||
def touch(self) -> "ProjectConfig":
|
||||
def touch(self) -> ProjectConfig:
|
||||
"""Return a copy with ``modified_at`` refreshed to now (UTC)."""
|
||||
return self.model_copy(update={"modified_at": datetime.now(timezone.utc)})
|
||||
return self.model_copy(update={"modified_at": datetime.now(UTC)})
|
||||
|
||||
@@ -6,7 +6,7 @@ of every ``ProjectConfig``.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -15,7 +15,7 @@ from arautopilot.core.ids import VesselId, new_vessel_id
|
||||
from arautopilot.core.pid_config import PidConfig
|
||||
|
||||
|
||||
class VesselType(str, Enum):
|
||||
class VesselType(StrEnum):
|
||||
"""Vessel classes targeted by Phase 1 of the product (brief section 3)."""
|
||||
|
||||
YACHT_MOTOR_PLANEO = "yacht_motor_planeo"
|
||||
|
||||
@@ -65,10 +65,16 @@ def load_default_tuning_from_path(path: Path | str) -> PidConfig:
|
||||
|
||||
|
||||
def _read_json_resource(package: str, filename: str) -> dict[str, Any]:
|
||||
return json.loads(resources.files(package).joinpath(filename).read_text(encoding="utf-8"))
|
||||
text = resources.files(package).joinpath(filename).read_text(encoding="utf-8")
|
||||
data = json.loads(text)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"{package}/{filename}: expected a JSON object at the top level")
|
||||
return data
|
||||
|
||||
|
||||
def _read_yaml_resource(package: str, filename: str) -> dict[str, Any]:
|
||||
return yaml.safe_load(
|
||||
resources.files(package).joinpath(filename).read_text(encoding="utf-8")
|
||||
)
|
||||
text = resources.files(package).joinpath(filename).read_text(encoding="utf-8")
|
||||
data = yaml.safe_load(text)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"{package}/{filename}: expected a YAML mapping at the top level")
|
||||
return data
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC
|
||||
|
||||
import pytest
|
||||
|
||||
from arautopilot.core.knob_state import (
|
||||
@@ -81,13 +83,13 @@ def test_disarm_returns_to_libre() -> None:
|
||||
|
||||
|
||||
def test_libre_with_armed_at_set_invalid() -> None:
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
KnobState(
|
||||
mode=KnobMode.LIBRE,
|
||||
function=KnobFunction.NONE,
|
||||
armed_at=datetime.now(timezone.utc),
|
||||
armed_at=datetime.now(UTC),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user