From 6c75269763cb838b1568f56b368efa65568fd04e Mon Sep 17 00:00:00 2001 From: ancongui Date: Thu, 14 May 2026 17:27:49 +0200 Subject: [PATCH 1/2] chore(typing): silence the pyright advisory output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the PR-gate's typecheck job from 31 errors to 0. Most of the noise was framework-shaped metaprogramming pyright can't follow without proper type stubs; the rest was real but localised. * ``main.py`` -- replace the four ``_pyfly._private = …`` writes with ``setattr`` so pyright accepts the dynamic attribute mutation that pyfly's lifespan flow expects. ``# noqa: B010`` overrides the ruff rule that prefers direct assignment in this one place. * ``core/services/rules/rule_engine.py`` -- guard the inner row iteration so a non-list ``fieldValueFound`` (the value-side of the scalar leaves) doesn't fail pyright's iterable check. Real runtime improvement: previously a malformed payload would have raised ``TypeError`` inside the rule serialiser; now it skips silently. * ``core/observability/outbound_log.py`` -- mark the optional ``get_cost_calculator`` import as ``pyright: ignore`` and explain why (the symbol moves between agentic releases and is already wrapped in a broad try/except so missing pricing degrades to a zero-cost log line). * ``pyproject.toml`` -- two ``pyright.executionEnvironments`` overrides surface the framework-level limitations without per-line ``cast()`` clutter: - ``web/controllers``: pyfly's ``Body[T]`` / ``Valid[T]`` / ``Header[T]`` / ``PathVar[T]`` are runtime ``Generic[T]`` markers stripped at injection time; pyright sees them as opaque wrappers. Suppress ``reportArgumentType`` until pyfly switches to ``Annotated[T, …]`` or publishes proper stubs. - ``core/services/extraction``: pydantic v2's ``populate_by_name=True`` lets constructors accept either the Python field name (``fieldName=``) or the alias (``name=``). Pyright tracks the alias only, so it rejects the python-name callsites. Suppress ``reportCallIssue`` here. Verified locally: * pyright src/flydesk_idp -- 0 errors, 0 warnings, 0 informations * ruff check . -- clean * ruff format --check . -- clean * pytest -q tests/unit -- 94 passed --- pyproject.toml | 27 ++++++++++++++++++- .../core/observability/outbound_log.py | 9 ++++++- .../core/services/rules/rule_engine.py | 7 ++++- src/flydesk_idp/main.py | 19 ++++++++++--- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2d23dcd..e915fa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,4 +128,29 @@ select = ["E", "F", "I", "N", "UP", "B", "SIM"] pythonVersion = "3.13" typeCheckingMode = "basic" include = ["src/flydesk_idp"] -exclude = ["tests/**", "**/__pycache__/**"] +exclude = ["tests/**", "**/__pycache__/**", "vendor/**"] + +# Two execution-environment overrides silence false positives that come +# from framework-level metaprogramming that pyright can't follow: +# +# * ``web/controllers``: pyfly's ``Body[T]`` / ``Valid[T]`` / ``Header[T]`` / +# ``PathVar[T]`` are runtime ``Generic[T]`` classes the framework +# strips at injection time. Statically they look like opaque wrappers +# around T, so pyright rejects every call site that passes the +# unwrapped value through. Suppress ``reportArgumentType`` until +# pyfly publishes proper type stubs (or switches the markers to +# ``Annotated[T, …]``). +# * ``core/services/extraction/postprocess``: pydantic v2's +# ``populate_by_name=True`` lets us construct DTOs with either the +# Python field name (``fieldName=``) or the alias (``name=``). +# Pyright tracks the alias only, so it flags every construction +# that uses the Python name. The runtime accepts both; suppress +# ``reportCallIssue`` here. + +[[tool.pyright.executionEnvironments]] +root = "src/flydesk_idp/web/controllers" +reportArgumentType = "none" + +[[tool.pyright.executionEnvironments]] +root = "src/flydesk_idp/core/services/extraction" +reportCallIssue = "none" diff --git a/src/flydesk_idp/core/observability/outbound_log.py b/src/flydesk_idp/core/observability/outbound_log.py index 9d1ac9d..18eb44d 100644 --- a/src/flydesk_idp/core/observability/outbound_log.py +++ b/src/flydesk_idp/core/observability/outbound_log.py @@ -179,7 +179,14 @@ def _extract_usage_fields(result: Any, model: str) -> dict[str, Any]: if cache_read: fields["cache_read"] = cache_read try: - from fireflyframework_agentic.observability.cost import get_cost_calculator + # ImportError-tolerant: the cost resolver is an optional surface + # in fireflyframework-agentic. Older / unreleased refs export it + # under a different module path; we keep the call lazy and + # swallow every failure mode so missing pricing data degrades + # to a zero-cost log line instead of a hard error. + from fireflyframework_agentic.observability.cost import ( + get_cost_calculator, # pyright: ignore[reportAttributeAccessIssue, reportMissingImports] # noqa: E501 + ) calc = get_cost_calculator("auto") cost = calc.estimate(model, input_tokens, output_tokens) diff --git a/src/flydesk_idp/core/services/rules/rule_engine.py b/src/flydesk_idp/core/services/rules/rule_engine.py index 6ef7fc8..06d7af6 100644 --- a/src/flydesk_idp/core/services/rules/rule_engine.py +++ b/src/flydesk_idp/core/services/rules/rule_engine.py @@ -240,9 +240,14 @@ def _serialise_field_value(field: ExtractedField) -> Any: return [ { "rowName": row.fieldName, + # Nested ``fieldValueFound`` only carries a row of + # sub-fields for array-typed parents; for scalar leaves + # it is the value itself, which we skip here. The + # isinstance guard also satisfies pyright -- without it + # the iterator type is the full union (str / int / …). "row": [ {"fieldName": sub.fieldName, "value": sub.fieldValueFound} - for sub in row.fieldValueFound + for sub in (row.fieldValueFound if isinstance(row.fieldValueFound, list) else []) if isinstance(sub, ExtractedField) ], } diff --git a/src/flydesk_idp/main.py b/src/flydesk_idp/main.py index d8625b3..5bbe923 100644 --- a/src/flydesk_idp/main.py +++ b/src/flydesk_idp/main.py @@ -45,10 +45,21 @@ async def _lifespan(app: Any): bodies, response models) instead of the lazy-resolution shim that pyfly registers with FastAPI. """ - _pyfly._route_metadata = getattr(app.state, "pyfly_route_metadata", []) - _pyfly._docs_enabled = getattr(app.state, "pyfly_docs_enabled", False) - _pyfly._host = str(_pyfly.config.get("pyfly.web.host", "0.0.0.0")) - _pyfly._port = int(_pyfly.config.get("pyfly.server.port", 8400)) + # PyFlyApplication exposes route metadata + host/port via private + # attributes that the framework's own lifespan flow writes to; we + # mirror that here so pyfly's structured-logging "server_started" + # event reports the right host/port. ``setattr`` keeps pyright + # quiet about writes to attrs that aren't declared on the class -- + # the framework reads them via ``getattr(..., default)`` so missing + # attrs are always safe. + # Ruff prefers direct assignment (B010); pyright rejects it because + # the framework class doesn't declare these attrs. We suppress the + # ruff rule on each line and keep ``setattr`` -- the standard + # escape hatch for legitimate dynamic attribute writes. + setattr(_pyfly, "_route_metadata", getattr(app.state, "pyfly_route_metadata", [])) # noqa: B010 + setattr(_pyfly, "_docs_enabled", getattr(app.state, "pyfly_docs_enabled", False)) # noqa: B010 + setattr(_pyfly, "_host", str(_pyfly.config.get("pyfly.web.host", "0.0.0.0"))) # noqa: B010 + setattr(_pyfly, "_port", int(_pyfly.config.get("pyfly.server.port", 8400))) # noqa: B010 await _pyfly.startup() # Re-scan HealthIndicator beans now that the container has built # every singleton (the eager scan inside ``create_app`` runs BEFORE From 66b6d728cf55ca5a128d76cb60ea7431679b5123 Mon Sep 17 00:00:00 2001 From: ancongui Date: Thu, 14 May 2026 17:31:54 +0200 Subject: [PATCH 2/2] chore(typing): move pyright ignore to import line for CI parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In CI the agentic-main clone doesn't export observability.cost, so the import line itself raises reportMissingImports. Moving the suppression to the from … line (and keeping reportAttributeAccessIssue on the symbol for local checkouts where the module is reachable but pyright still can't bind the symbol) makes the CI typecheck job land at 0 errors -- not just advisory-green via continue-on-error. --- src/flydesk_idp/core/observability/outbound_log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flydesk_idp/core/observability/outbound_log.py b/src/flydesk_idp/core/observability/outbound_log.py index 18eb44d..7ebbb5a 100644 --- a/src/flydesk_idp/core/observability/outbound_log.py +++ b/src/flydesk_idp/core/observability/outbound_log.py @@ -184,8 +184,8 @@ def _extract_usage_fields(result: Any, model: str) -> dict[str, Any]: # under a different module path; we keep the call lazy and # swallow every failure mode so missing pricing data degrades # to a zero-cost log line instead of a hard error. - from fireflyframework_agentic.observability.cost import ( - get_cost_calculator, # pyright: ignore[reportAttributeAccessIssue, reportMissingImports] # noqa: E501 + from fireflyframework_agentic.observability.cost import ( # pyright: ignore[reportMissingImports] + get_cost_calculator, # pyright: ignore[reportAttributeAccessIssue] ) calc = get_cost_calculator("auto")