From 7dc970cfebb78fa85a983202bd4cc22baa4dc692 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 20:57:45 +0000 Subject: [PATCH] Apply PYTHON_PORTING_GUIDE to ndi.epoch.* and ndi.ontology.* Add @pydantic.validate_call to all public functions and methods in ndi.epoch and ndi.ontology modules per the porting guide's mandatory Pydantic input validation rule (Section 4). Changes: - epoch.py: Add @validate_call to is_epoch_or_empty() - epochprobemap.py: Add @validate_call to parse_devicestring(), build_devicestring(); export them from __init__.py - epochprobemap_daqsystem.py: Add @validate_call to savetofile() - epochset.py: Add @validate_call with Annotated[int, Field(ge=1)] constraints on epochclock(), t0_t1(), epochid(), epochnumber(), epochtableentry(); move ClockType to runtime import - functions.py: Add @validate_call to epochrange(), findepochnode() with ConfigDict(arbitrary_types_allowed=True) - ontology/__init__.py: Add @validate_call to lookup(); remove unused imports (lru_cache, Path, Dict, List, Optional, Tuple) - ontology/providers.py: Add @validate_call to lookup_term() on OntologyProvider base class and all 11 provider subclasses All 112 tests pass. Ruff and Black clean. https://claude.ai/code/session_01RoNGaAUDsDLzPLhMLVutbZ --- src/ndi/epoch/__init__.py | 4 +++- src/ndi/epoch/epoch.py | 4 ++++ src/ndi/epoch/epochprobemap.py | 4 ++++ src/ndi/epoch/epochprobemap_daqsystem.py | 3 +++ src/ndi/epoch/epochset.py | 28 ++++++++++++++---------- src/ndi/epoch/functions.py | 5 +++++ src/ndi/ontology/__init__.py | 7 +++--- src/ndi/ontology/providers.py | 12 ++++++++++ 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/ndi/epoch/__init__.py b/src/ndi/epoch/__init__.py index f75e0aa..f738214 100644 --- a/src/ndi/epoch/__init__.py +++ b/src/ndi/epoch/__init__.py @@ -11,7 +11,7 @@ """ from .epoch import Epoch -from .epochprobemap import EpochProbeMap +from .epochprobemap import EpochProbeMap, build_devicestring, parse_devicestring from .epochprobemap_daqsystem import EpochProbeMapDAQSystem from .epochset import EpochSet from .functions import epochrange, findepochnode @@ -21,6 +21,8 @@ "EpochSet", "EpochProbeMap", "EpochProbeMapDAQSystem", + "build_devicestring", "epochrange", "findepochnode", + "parse_devicestring", ] diff --git a/src/ndi/epoch/epoch.py b/src/ndi/epoch/epoch.py index 4d2a0b2..6e4a4c7 100644 --- a/src/ndi/epoch/epoch.py +++ b/src/ndi/epoch/epoch.py @@ -10,6 +10,9 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +import pydantic +from pydantic import ConfigDict + if TYPE_CHECKING: from ..time import ClockType from .epochprobemap import EpochProbeMap @@ -210,6 +213,7 @@ def matches_probe( return False +@pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def is_epoch_or_empty(value: Any) -> bool: """ Validate that a value is an Epoch or empty. diff --git a/src/ndi/epoch/epochprobemap.py b/src/ndi/epoch/epochprobemap.py index 2417e96..47db431 100644 --- a/src/ndi/epoch/epochprobemap.py +++ b/src/ndi/epoch/epochprobemap.py @@ -11,6 +11,8 @@ from dataclasses import dataclass from typing import Any +import pydantic + @dataclass class EpochProbeMap: @@ -141,6 +143,7 @@ def __hash__(self) -> int: return hash((self.name, self.reference, self.type)) +@pydantic.validate_call def parse_devicestring(devicestring: str) -> dict[str, str]: """ Parse a device string into components. @@ -161,6 +164,7 @@ def parse_devicestring(devicestring: str) -> dict[str, str]: } +@pydantic.validate_call def build_devicestring( name: str, deviceclass: str = "", diff --git a/src/ndi/epoch/epochprobemap_daqsystem.py b/src/ndi/epoch/epochprobemap_daqsystem.py index 25a4fc3..8d99310 100644 --- a/src/ndi/epoch/epochprobemap_daqsystem.py +++ b/src/ndi/epoch/epochprobemap_daqsystem.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import Any +import pydantic + from ..daq.daqsystemstring import DAQSystemString from .epochprobemap import EpochProbeMap @@ -84,6 +86,7 @@ def serialize(self) -> str: ] ) + @pydantic.validate_call def savetofile(self, filename: str) -> None: """ Write this epoch probe map to a file. diff --git a/src/ndi/epoch/epochset.py b/src/ndi/epoch/epochset.py index 4dc9b62..bc1b397 100644 --- a/src/ndi/epoch/epochset.py +++ b/src/ndi/epoch/epochset.py @@ -9,12 +9,13 @@ import hashlib from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any +from typing import Annotated, Any import numpy as np +import pydantic +from pydantic import Field -if TYPE_CHECKING: - from ..time import ClockType +from ..time import ClockType class EpochSet(ABC): @@ -149,7 +150,8 @@ def numepochs(self) -> int: et, _ = self.epochtable() return len(et) - def epochclock(self, epoch_number: int) -> list[ClockType]: + @pydantic.validate_call + def epochclock(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[ClockType]: """ Get clock types for an epoch. @@ -163,13 +165,14 @@ def epochclock(self, epoch_number: int) -> list[ClockType]: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() - if epoch_number < 1 or epoch_number > len(et): + if epoch_number > len(et): raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") entry = et[epoch_number - 1] return entry.get("epoch_clock", []) - def t0_t1(self, epoch_number: int) -> list[tuple[float, float]]: + @pydantic.validate_call + def t0_t1(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[tuple[float, float]]: """ Get time range for an epoch. @@ -183,13 +186,14 @@ def t0_t1(self, epoch_number: int) -> list[tuple[float, float]]: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() - if epoch_number < 1 or epoch_number > len(et): + if epoch_number > len(et): raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") entry = et[epoch_number - 1] return entry.get("t0_t1", [(np.nan, np.nan)]) - def epochid(self, epoch_number: int) -> str: + @pydantic.validate_call + def epochid(self, epoch_number: Annotated[int, Field(ge=1)]) -> str: """ Get epoch ID for an epoch number. @@ -203,11 +207,12 @@ def epochid(self, epoch_number: int) -> str: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() - if epoch_number < 1 or epoch_number > len(et): + if epoch_number > len(et): raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") return et[epoch_number - 1].get("epoch_id", "") + @pydantic.validate_call def epochnumber(self, epoch_id: str) -> int: """ Get epoch number for an epoch ID. @@ -255,7 +260,8 @@ def matchedepochtable( return matches - def epochtableentry(self, epoch_number: int) -> dict[str, Any]: + @pydantic.validate_call + def epochtableentry(self, epoch_number: Annotated[int, Field(ge=1)]) -> dict[str, Any]: """ Get a single epoch table entry. @@ -269,7 +275,7 @@ def epochtableentry(self, epoch_number: int) -> dict[str, Any]: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() - if epoch_number < 1 or epoch_number > len(et): + if epoch_number > len(et): raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") return et[epoch_number - 1] diff --git a/src/ndi/epoch/functions.py b/src/ndi/epoch/functions.py index 0df33d4..5cc93c3 100644 --- a/src/ndi/epoch/functions.py +++ b/src/ndi/epoch/functions.py @@ -10,9 +10,13 @@ from typing import Any +import pydantic +from pydantic import ConfigDict + from ..time import ClockType +@pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def epochrange( epochset_obj: Any, clocktype: ClockType, @@ -118,6 +122,7 @@ def _resolve_epoch_index( raise ValueError(f"Epoch ID '{epoch}' not found") +@pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def findepochnode( epoch_node: dict[str, Any], epoch_node_array: list[dict[str, Any]], diff --git a/src/ndi/ontology/__init__.py b/src/ndi/ontology/__init__.py index f1d147f..6041b63 100644 --- a/src/ndi/ontology/__init__.py +++ b/src/ndi/ontology/__init__.py @@ -15,9 +15,9 @@ from __future__ import annotations import json -from functools import lru_cache -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any + +import pydantic from .providers import PROVIDER_REGISTRY @@ -115,6 +115,7 @@ def _load_prefix_map() -> dict[str, str]: _CACHE_MAX = 100 +@pydantic.validate_call def lookup(lookup_string: str) -> OntologyResult: """Look up a term in the appropriate ontology. diff --git a/src/ndi/ontology/providers.py b/src/ndi/ontology/providers.py index 7aa5b6f..32772ac 100644 --- a/src/ndi/ontology/providers.py +++ b/src/ndi/ontology/providers.py @@ -13,6 +13,8 @@ from typing import Any from urllib.parse import quote +import pydantic + # Registry populated at module load PROVIDER_REGISTRY: dict[str, type[OntologyProvider]] = {} @@ -22,6 +24,7 @@ class OntologyProvider: name: str = "" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: """Look up a term by ID or name. Override in subclasses.""" from . import OntologyResult @@ -49,6 +52,7 @@ class OLSProvider(OntologyProvider): ols_ontology: str = "" ols_prefix: str = "" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: prefix = prefix or self.ols_prefix @@ -139,6 +143,7 @@ class OMProvider(OLSProvider): ols_ontology = "om" ols_prefix = "OM" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: # OM doesn't support numeric ID lookups @@ -208,6 +213,7 @@ def _load_data(self) -> list[dict[str, str]]: NDICProvider._data = [] return [] + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -230,6 +236,7 @@ class NCImProvider(OntologyProvider): name = "NCIm" _CUI_PATTERN = re.compile(r"^C\d{7}$") + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -277,6 +284,7 @@ class NCBITaxonProvider(OntologyProvider): name = "NCBITaxon" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -344,6 +352,7 @@ class WBStrainProvider(OntologyProvider): name = "WBStrain" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -392,6 +401,7 @@ class RRIDProvider(OntologyProvider): name = "RRID" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -423,6 +433,7 @@ class PubChemProvider(OntologyProvider): name = "PubChem" + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from . import OntologyResult @@ -541,6 +552,7 @@ def _load_ontology(self) -> dict[str, Any]: } return EMPTYProvider._cache + @pydantic.validate_call def lookup_term(self, term: str, prefix: str = "") -> Any: from ndi.fun.name_utils import name_to_variable_name