Skip to content

Commit 4ff66a6

Browse files
tercelclaude
andcommitted
fix(types): clear all 31 pre-existing pyright errors
CLAUDE.md requires zero pyright errors before commit. The 31 errors that had accumulated on main are now resolved. Full src/apcore/ scan goes from 31 errors → 0. Real type bugs fixed: - executor.py:_translate_abort approval branch was constructing ApprovalDeniedError(message=...) without the required `result` parameter — would TypeError at runtime if the pipeline ever aborted at the approval_gate step. Now constructs synthetic ApprovalResult per status. (Pipeline v2 raises typed errors directly so this branch is rarely reached in practice, but it was still a runtime bug.) - executor.py:_validate_async error_dict assignment was unsafe (`e.to_dict() if hasattr(e, "to_dict")`); now uses callable check + isinstance(produced, dict) narrowing. - sys_modules/registration.py was passing MetricsCollector | None into HealthSummaryModule and HealthModuleModule, both of which declare MetricsCollector (non-Optional). Would crash at startup if metrics_collector was None. Now defaults to a fresh empty MetricsCollector so health modules degrade gracefully. API improvements: - ContextKey.get gains @overload signatures so key.get(ctx, default=v) narrows the return type to T (was T | None). Cleans up six append-on-None warnings in observability/{context_logger,metrics, usage}.py with no call-site changes. - decorator._has_context_param renamed to _context_param_name and collapsed to return str | None (was tuple[bool, str | None]). The has_context boolean was redundant; checking `is not None` is enough. Pure pyright satisfaction: - Optional dependency imports (aiohttp, opentelemetry.*, watchdog.*) marked with `# type: ignore[import-not-found]` since they live inside try/except ImportError blocks. - pydantic.fields.PydanticUndefined → pydantic_core.PydanticUndefined (canonical export path; pydantic.fields is the private re-export). - builtin_steps.py:redact_sensitive call sites now cast() schema_dict_fn() result to dict[str, Any] (Pydantic contract). - executor.Executor.__init__ strategy_kwargs gets explicit dict[str, Any] annotation to break the dict() inference union. - pipeline._validate_dependencies adds tuple[str, ...] annotations for the getattr-derived requires/provides locals. - pipeline_config._import_step BaseStep factory call now has a targeted call-arg ignore. - context.deserialize services=None has the same arg-type ignore as the field declaration. - pipeline_config factory call has a localized call-arg ignore. Tests: tests/test_decorator.py updated to track the _has_context_param → _context_param_name rename. Verification: black ✓ ruff ✓ pyright ✓ pytest 2252/2252 ✓. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6587510 commit 4ff66a6

13 files changed

Lines changed: 85 additions & 66 deletions

File tree

src/apcore/builtin_steps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import inspect
1616
import logging
1717
import time
18-
from typing import Any
18+
from typing import Any, cast
1919

2020
import pydantic
2121

@@ -488,7 +488,7 @@ async def execute(self, ctx: PipelineContext) -> StepResult:
488488
if schema_dict_fn is not None and callable(schema_dict_fn):
489489
from apcore.utils.redaction import redact_sensitive
490490

491-
schema = schema_dict_fn()
491+
schema = cast(dict[str, Any], schema_dict_fn())
492492
redacted = redact_sensitive(ctx.inputs, schema)
493493
if ctx.context is not None and hasattr(ctx.context, "redacted_inputs"):
494494
ctx.context.redacted_inputs = redacted
@@ -646,7 +646,7 @@ async def execute(self, ctx: PipelineContext) -> StepResult:
646646
if schema_dict_fn is not None and callable(schema_dict_fn):
647647
from apcore.utils.redaction import redact_sensitive
648648

649-
schema = schema_dict_fn()
649+
schema = cast(dict[str, Any], schema_dict_fn())
650650
redacted = redact_sensitive(ctx.output, schema)
651651
ctx.context.data[REDACTED_OUTPUT.name] = redacted
652652

src/apcore/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def deserialize(cls, data: dict[str, Any]) -> Context:
136136
identity=identity,
137137
redacted_inputs=data.get("redacted_inputs"),
138138
data=dict(data.get("data", {})),
139-
services=None,
139+
services=None, # type: ignore[arg-type]
140140
cancel_token=None,
141141
)
142142

src/apcore/context_key.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import Any, Generic, Protocol, TypeVar
6+
from typing import Any, Generic, Protocol, TypeVar, overload
77

88
T = TypeVar("T")
99

10-
_MISSING = object()
10+
_MISSING: Any = object()
1111

1212

1313
class _ContextLike(Protocol):
@@ -26,10 +26,20 @@ class ContextKey(Generic[T]):
2626

2727
name: str
2828

29-
def get(self, ctx: _ContextLike, default: T | None = None) -> T | None: # type: ignore[type-var]
30-
"""Return the value for this key, or *default* if absent."""
29+
@overload
30+
def get(self, ctx: _ContextLike) -> T | None: ...
31+
32+
@overload
33+
def get(self, ctx: _ContextLike, default: T) -> T: ...
34+
35+
def get(self, ctx: _ContextLike, default: T | None = None) -> T | None:
36+
"""Return the value for this key, or *default* if absent.
37+
38+
Overloaded so that ``key.get(ctx)`` returns ``T | None`` while
39+
``key.get(ctx, default=value)`` narrows the return type to ``T``.
40+
"""
3141
value = ctx.data.get(self.name, _MISSING)
32-
return default if value is _MISSING else value # type: ignore[return-value]
42+
return default if value is _MISSING else value
3343

3444
def set(self, ctx: _ContextLike, value: T) -> None:
3545
"""Store *value* under this key in context.data."""

src/apcore/decorator.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,25 @@ def generate_output_model(func: Any) -> type[BaseModel]:
131131
return create_model("OutputModel", result=(return_type, ...))
132132

133133

134-
def _has_context_param(func: Any) -> tuple[bool, str | None]:
135-
"""Check if any parameter has type annotation that is the Context class.
134+
def _context_param_name(func: Any) -> str | None:
135+
"""Return the name of the parameter typed as ``Context``, or None.
136136
137-
Returns (True, param_name) if found, (False, None) otherwise.
138-
Detection is type-based, not name-based.
137+
Detection is type-based, not name-based. Returns the parameter name when
138+
a single ``Context``-typed parameter is found; returns ``None`` when the
139+
function has no Context parameter or its annotations cannot be resolved.
139140
"""
140141
try:
141142
hints = typing.get_type_hints(func, include_extras=True)
142143
except NameError:
143-
return (False, None)
144+
return None
144145

145146
for param_name, hint in hints.items():
146147
if param_name == "return":
147148
continue
148149
if hint is Context:
149-
return (True, param_name)
150+
return param_name
150151

151-
return (False, None)
152+
return None
152153

153154

154155
def _normalize_result(result: Any) -> dict[str, Any]:
@@ -193,7 +194,7 @@ def __init__(
193194
self.input_schema = input_schema if input_schema is not None else generate_input_model(func, param_descs)
194195
self.output_schema = output_schema if output_schema is not None else generate_output_model(func)
195196

196-
has_context, context_param_name = _has_context_param(func)
197+
context_param_name = _context_param_name(func)
197198

198199
# Description priority chain
199200
if description is not None:
@@ -223,7 +224,7 @@ def __init__(
223224

224225
async def _async_execute(inputs: dict[str, Any], context: Context) -> dict[str, Any]:
225226
call_kwargs = dict(inputs)
226-
if has_context:
227+
if context_param_name is not None:
227228
call_kwargs[context_param_name] = context
228229
result = await func(**call_kwargs)
229230
return _normalize_result(result)
@@ -233,7 +234,7 @@ async def _async_execute(inputs: dict[str, Any], context: Context) -> dict[str,
233234

234235
def _sync_execute(inputs: dict[str, Any], context: Context) -> dict[str, Any]:
235236
call_kwargs = dict(inputs)
236-
if has_context:
237+
if context_param_name is not None:
237238
call_kwargs[context_param_name] = context
238239
result = func(**call_kwargs)
239240
return _normalize_result(result)

src/apcore/events/subscribers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from dataclasses import asdict
88

99
try:
10-
import aiohttp
10+
import aiohttp # type: ignore[import-not-found]
1111
except ImportError:
1212
aiohttp = None # type: ignore[assignment]
1313

src/apcore/executor.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,15 @@ def __init__(
143143
self._middleware_manager.add(mw)
144144

145145
# Resolve strategy (pass middleware_manager and executor for production parity)
146-
strategy_kwargs = dict(
147-
registry=registry,
148-
config=config,
149-
acl=acl,
150-
approval_handler=approval_handler,
151-
middlewares=middlewares,
152-
middleware_manager=self._middleware_manager,
153-
executor=self,
154-
)
146+
strategy_kwargs: dict[str, Any] = {
147+
"registry": registry,
148+
"config": config,
149+
"acl": acl,
150+
"approval_handler": approval_handler,
151+
"middlewares": middlewares,
152+
"middleware_manager": self._middleware_manager,
153+
"executor": self,
154+
}
155155
if strategy is None:
156156
from apcore.builtin_steps import build_standard_strategy
157157

@@ -401,7 +401,12 @@ async def _validate_async(
401401
except Exception as e:
402402
# Step raised an error (e.g., ModuleNotFoundError, ACLDeniedError)
403403
# Convert to a failed check using the error's own code/dict
404-
error_dict = e.to_dict() if hasattr(e, "to_dict") else {"code": type(e).__name__, "message": str(e)}
404+
error_dict: dict[str, Any] = {"code": type(e).__name__, "message": str(e)}
405+
to_dict_fn = getattr(e, "to_dict", None)
406+
if callable(to_dict_fn):
407+
produced = to_dict_fn()
408+
if isinstance(produced, dict):
409+
error_dict = produced
405410
code = getattr(e, "code", type(e).__name__)
406411

407412
# Determine which check failed based on error type
@@ -487,12 +492,15 @@ def _translate_abort(self, abort: PipelineAbortError) -> ModuleError:
487492
caller_id, target_id = pair[0].strip(), pair[1].strip()
488493
return ACLDeniedError(caller_id=caller_id, target_id=target_id)
489494
if step == "approval_gate":
490-
if "rejected" in explanation.lower() or "denied" in explanation.lower():
491-
return ApprovalDeniedError(message=explanation)
492-
if "timeout" in explanation.lower():
493-
return ApprovalTimeoutError(message=explanation)
494-
if "pending" in explanation.lower():
495-
return ApprovalPendingError(message=explanation)
495+
from apcore.approval import ApprovalResult
496+
497+
lowered = explanation.lower()
498+
if "rejected" in lowered or "denied" in lowered:
499+
return ApprovalDeniedError(result=ApprovalResult(status="rejected", reason=explanation))
500+
if "timeout" in lowered:
501+
return ApprovalTimeoutError(result=ApprovalResult(status="timeout", reason=explanation))
502+
if "pending" in lowered:
503+
return ApprovalPendingError(result=ApprovalResult(status="pending", reason=explanation))
496504
if step == "input_validation" and "validation failed" in explanation.lower():
497505
return SchemaValidationError(message=explanation)
498506
if step == "output_validation" and "validation failed" in explanation.lower():

src/apcore/observability/tracing.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,13 @@ def __init__(
124124
ImportError: If required opentelemetry packages are not installed.
125125
"""
126126
try:
127-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
127+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( # type: ignore[import-not-found]
128128
OTLPSpanExporter as _OTLPSpanExporter,
129129
)
130-
from opentelemetry.sdk.resources import Resource
131-
from opentelemetry.sdk.trace import TracerProvider
132-
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
133-
from opentelemetry.trace import StatusCode
130+
from opentelemetry.sdk.resources import Resource # type: ignore[import-not-found]
131+
from opentelemetry.sdk.trace import TracerProvider # type: ignore[import-not-found]
132+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor # type: ignore[import-not-found]
133+
from opentelemetry.trace import StatusCode # type: ignore[import-not-found]
134134
except ImportError:
135135
raise ImportError(
136136
"opentelemetry packages are required for OTLPExporter. "

src/apcore/pipeline.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,15 @@ def _validate_dependencies(self) -> None:
153153
"""Warn if any step's requires are not provided by a preceding step."""
154154
provided: set[str] = set()
155155
for step in self.steps:
156-
requires = getattr(step, "requires", ())
156+
requires: tuple[str, ...] = getattr(step, "requires", ())
157157
missing = set(requires) - provided
158158
if missing:
159159
_logger.warning(
160160
"Step '%s' requires %s, but no preceding step provides them. " "This may cause runtime errors.",
161161
step.name,
162162
missing,
163163
)
164-
provides = getattr(step, "provides", ())
164+
provides: tuple[str, ...] = getattr(step, "provides", ())
165165
provided.update(provides)
166166

167167
def insert_after(self, anchor: str, step: Step) -> None:

src/apcore/pipeline_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def _resolve_step(step_def: dict[str, Any]) -> BaseStep:
8383
if type_name and type_name in _step_type_registry:
8484
factory = _step_type_registry[type_name]
8585
if isinstance(factory, type) and issubclass(factory, BaseStep):
86-
step = factory(**config) if config else factory()
86+
# Factory is a BaseStep subclass; either it provides defaults for
87+
# `name` (most user steps do) or the YAML supplied `config` overrides.
88+
step = factory(**config) if config else factory() # type: ignore[call-arg]
8789
else:
8890
step = factory(config)
8991
# Override metadata from YAML

src/apcore/registry/registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -932,8 +932,8 @@ def watch(self) -> None:
932932
Raises ImportError if watchdog is not installed.
933933
"""
934934
try:
935-
from watchdog.observers import Observer
936-
from watchdog.events import FileSystemEventHandler, FileSystemEvent
935+
from watchdog.observers import Observer # type: ignore[import-not-found]
936+
from watchdog.events import FileSystemEventHandler, FileSystemEvent # type: ignore[import-not-found]
937937
except ImportError:
938938
raise ImportError("watchdog is required for hot reload. Install it with: pip install watchdog")
939939

0 commit comments

Comments
 (0)