Skip to content

Fix plugin api not found#99

Merged
touale merged 6 commits intomasterfrom
fix-plugin-api-not-found
Mar 26, 2026
Merged

Fix plugin api not found#99
touale merged 6 commits intomasterfrom
fix-plugin-api-not-found

Conversation

@touale
Copy link
Copy Markdown
Owner

@touale touale commented Mar 25, 2026

Fix #98

Summary by CodeRabbit

  • Improvements

    • Ray worker startup now reliably initializes monitoring and adapter state so nested service calls work correctly.
    • Plugin loading respects test-mode silence to reduce noisy errors during test runs.
  • New Features

    • Context-managed API resolver and scoped remote-API mappings for safer plugin API resolution.
    • Plugins now propagate resolver/remote-API context into request handlers while preserving async/stream behavior.
  • Tests

    • Added integration and unit tests covering resolver behavior, context constraints, and Ray nested service calls.
  • Chores

    • Test coverage initialization and session aggregation enabled; coverage scope narrowed to plugin package.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduce an ApiResolver abstraction and ContextVar-based resolver/remote-API context; bind per-plugin api_registry/resolver for request handlers; change call_plugin_api to use resolver/context; reinitialize adapter and Sentry inside Ray worker processes; add coverage startup support for subprocesses and merge coverage shards in tests; add resolver- and Ray-focused tests.

Changes

Cohort / File(s) Summary
Coverage & Test Env
\.coveragerc, pytest.ini, tools/coverage_support/sitecustomize.py
Limit coverage source to src/framex; set COVERAGE_PROCESS_START=.coveragerc for test subprocesses; add sitecustomize to call coverage.process_startup() when env var present.
Core Plugin Resolver
src/framex/plugin/resolver.py, src/framex/plugin/manage.py
Add ApiResolver abstraction, ContextVar-backed current/default resolver and current remote-apis APIs; introduce module-level _manager seeded from settings for plugin discovery behavior.
Plugin API Surface & Binding
src/framex/plugin/__init__.py, src/framex/plugin/base.py
Switch to manager import from manage, set default API resolver; change call_plugin_api to resolve via _resolve_plugin_api and resolver/context; BasePlugin now accepts api_registry, creates api_resolver, and wraps request handlers to set/reset resolver and remote-apis for sync/async/async-generator handlers.
Ray Worker Init
src/framex/__init__.py
Add _setup_ray_worker() to set use_ray, reset adapter, and run Sentry setup inside Ray worker; persist effective use_ray back to settings and use new worker_process_setup_hook.
Tests — Hooks & Coverage Merge
tests/conftest.py
Add pytest_sessionfinish hook to combine .coverage.* shards into a single .coverage using Coverage API when available.
Tests — Resolver & Plugin Behavior
tests/test_plugin.py, tests/test_exception.py
Update tests to use context-bound set_current_remote_apis/resolver instead of interval_apis; add tests for resolver fallback, explicit resolver, plugin-context restrictions, async/stream semantics, and manager lookup behavior.
Tests — Ray Integration & Plugins
tests/test_use_ray_integration.py, tests/ray_plugins/test_service_caller.py
Add Ray integration test that spawns a Ray-backed server subprocess verifying nested plugin calls use remote_apis; add RayServiceCallerPlugin test plugin exercising cross-plugin call.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BasePlugin
    participant ApiResolver
    participant Adapter
    participant RemoteAPI

    Client->>BasePlugin: HTTP request -> handler
    BasePlugin->>ApiResolver: set_current_api_resolver(self.api_resolver)
    BasePlugin->>ApiResolver: set_current_remote_apis(self.api_registry)
    BasePlugin->>ApiResolver: call_plugin_api(api_name, **kwargs)
    ApiResolver->>Adapter: resolve -> PluginApi
    Adapter->>RemoteAPI: invoke (function or HTTP)
    RemoteAPI-->>Adapter: response
    Adapter-->>BasePlugin: return result
    BasePlugin->>ApiResolver: reset_current_remote_apis()
    BasePlugin->>ApiResolver: reset_current_api_resolver()
    BasePlugin-->>Client: HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop in threads where resolvers bind,

Context set, remote maps aligned,
In Ray's small workers adapters wake,
Coverage seeds from subprocesses take,
A rabbit cheers for plugins that find.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix plugin api not found' accurately describes the main objective of resolving plugin API discovery issues during local cross-plugin invocation.
Linked Issues check ✅ Passed The PR addresses issue #98 by implementing API resolver context binding, plugin initialization updates, and resolver infrastructure to enable local plugin-to-plugin API discovery.
Out of Scope Changes check ✅ Passed All changes directly support plugin API resolution: new resolver module, context management, plugin initialization updates, Ray worker setup, coverage configuration, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-plugin-api-not-found

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 25, 2026

Codecov Report

❌ Patch coverage is 88.65248% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/framex/plugin/base.py 77.77% 9 Missing and 1 partial ⚠️
src/framex/plugin/resolver.py 85.71% 3 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@touale touale force-pushed the fix-plugin-api-not-found branch from 8e5bbea to 940761f Compare March 25, 2026 11:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/test_plugin.py (1)

17-17: Move import to the top of the file.

The import inspect statement is placed after the first test function, which deviates from Python convention (PEP 8) of grouping all imports at the top of the module.

📦 Suggested fix

Move import inspect to line 1 with the other imports:

+import inspect
+
 import framex
 from framex.consts import VERSION

And remove line 17.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_plugin.py` at line 17, Move the "import inspect" statement from
its current location after the first test function up to the top of the module
with the other imports and remove the duplicate import where it currently
appears; locate the stray "import inspect" and relocate it to the module import
block so all imports are grouped at the top following PEP8.
src/framex/plugin/__init__.py (3)

216-227: Consider exporting ApiResolver and context helpers in __all__.

The new public utilities (ApiResolver, set_current_api_resolver, reset_current_api_resolver, get_current_remote_apis, set_current_remote_apis, reset_current_remote_apis) are imported by tests but not listed in __all__. If these are intended to be part of the public API (e.g., for advanced plugin developers), they should be exported.

📦 Suggested addition
 __all__ = [
+    "ApiResolver",
     "ApiType",
     "BasePlugin",
     "PluginMetadata",
+    "get_current_api_resolver",
+    "get_current_remote_apis",
     "load_builtin_plugins",
     "load_plugins",
     "on_proxy",
     "on_register",
     "on_request",
     "register_proxy_func",
     "remote",
+    "reset_current_api_resolver",
+    "reset_current_remote_apis",
+    "set_current_api_resolver",
+    "set_current_remote_apis",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/__init__.py` around lines 216 - 227, The __all__ export
list in framex.plugin.__init__ is missing the new public utilities used by
tests; add "ApiResolver", "set_current_api_resolver",
"reset_current_api_resolver", "get_current_remote_apis",
"set_current_remote_apis", and "reset_current_remote_apis" to the __all__ list
so these symbols are exported from the package (they are the identifiers
imported elsewhere and implemented alongside ApiType/BasePlugin/PluginMetadata).

34-40: Consider logging or raising for unexpected types in _coerce_plugin_api.

The method silently returns None for types that are neither PluginApi, dict, nor None. This could mask bugs if an unexpected type is passed.

🔍 Suggested enhancement
     `@staticmethod`
     def _coerce_plugin_api(api: PluginApi | dict[str, Any] | None) -> PluginApi | None:
         if api is None or isinstance(api, PluginApi):
             return api
         if isinstance(api, dict):
             return PluginApi.model_validate(api)
+        logger.warning(f"Unexpected type for plugin API coercion: {type(api)}")
         return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/__init__.py` around lines 34 - 40, The helper
_coerce_plugin_api currently returns None for unexpected types which can hide
bugs; update _coerce_plugin_api to either raise a TypeError (including the
actual type received) or log an error before returning None so callers get
immediate feedback — locate the _coerce_plugin_api staticmethod and replace the
final `return None` path with a TypeError that mentions the received value/type
(or call your module logger with a clear message) while keeping the existing
branches that accept PluginApi, dict, and None and continuing to use
PluginApi.model_validate for dict inputs.

104-104: ApiType used before import statement.

ApiType.FUNC and ApiType.HTTP are referenced here, but ApiType is imported at line 213 (bottom of file). While this works at runtime because function bodies execute after module loading completes, it causes:

  • IDE/type-checker confusion
  • Potential issues if this function is called during module initialization
  • Reduced code readability
📦 Suggested fix

Add ApiType to the existing import from framex.plugin.model at line 13:

-from framex.plugin.model import Plugin, PluginApi
+from framex.plugin.model import ApiType, Plugin, PluginApi

Then remove ApiType from the bottom import at line 213:

-from .model import ApiType, PluginMetadata
+from .model import PluginMetadata
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/__init__.py` at line 104, The code references ApiType (used
in the all_apis assignment with _manager) before ApiType is imported later in
the file; update the top import from framex.plugin.model to include ApiType (so
ApiType is available when all_apis is defined), and remove the duplicate/bottom
import of ApiType at the end of the file to avoid redundancy and
IDE/type-checker confusion; ensure you modify the import grouping that currently
brings in models (the import that already brings in other symbols from
framex.plugin.model) and then delete the later ApiType import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest.ini`:
- Around line 21-22: COVERAGE_PROCESS_START in pytest.ini will not trigger
coverage.process_startup() in spawned subprocesses unless sitecustomize.py is
importable; update pytest.ini so the env block also prepends
tools/coverage_support to subprocess PYTHONPATH (e.g. set
PYTHONPATH=tools/coverage_support:$PYTHONPATH alongside COVERAGE_PROCESS_START)
so sitecustomize.py can be imported by child interpreters, or alternatively
remove COVERAGE_PROCESS_START from pytest.ini and handle coverage startup
per-test as done in tests/test_use_ray_integration.py; reference:
COVERAGE_PROCESS_START, sitecustomize.py, tools/coverage_support,
tests/test_use_ray_integration.py.

In `@src/framex/__init__.py`:
- Around line 69-75: The driver process never flips the Ray mode flag, so calls
like framex.adapter.get_adapter() and BasePlugin.__init__ still see local mode;
mirror the worker-side change by setting settings.server.use_ray = True in the
driver startup path when run(use_ray=True) is invoked (the same place that
currently prepares workers), and also reset adapter state
(adapter_module._adapter = None) there so the driver and workers have consistent
mode; apply the same update to the other startup site noted (the other
invocation that prepares Ray) so both entry paths set settings.server.use_ray
and clear adapter state.

In `@src/framex/plugin/base.py`:
- Around line 85-88: The helper _call_remote_api currently always uses the
global resolver via call_plugin_api and loses any instance-scoped resolver,
causing calls from on_start or other non-request paths to miss plugin-declared
APIs; modify _call_remote_api to detect and pass the plugin instance's
resolver/fallback (e.g., self.remote_apis or self._remote_api_resolver) into
call_plugin_api when present, falling back to the global resolver only if the
instance-scoped resolver is None, and then continue to return
self._post_call_remote_api_hook(res).

In `@tests/conftest.py`:
- Around line 75-78: The try/except around "from coverage import Coverage" is
currently catching all exceptions; change the except block to only catch
ImportError (e.g., except ImportError:) so that only a missing coverage package
is ignored and any other errors during import or subsequent setup are allowed to
surface; locate the import statement referencing the Coverage symbol and replace
the broad except Exception with an except ImportError handler (optionally
capturing the exception as e if you want to log it) so the merge/coverage logic
isn't silently skipped.

In `@tests/test_use_ray_integration.py`:
- Around line 99-107: The test uses subprocess.Popen(...,
start_new_session=True) to launch Ray but cleanup currently calls
proc.terminate()/proc.kill() which only targets the launcher process; change the
cleanup logic to kill the entire process group using
os.killpg(os.getpgid(proc.pid), signal.SIGTERM) (and fall back to SIGKILL if
needed) instead of os.kill(proc.pid, ...), ensuring you import os and signal and
handle OSError for already-exited processes; locate the teardown code that
references proc.terminate()/proc.kill() in tests/test_use_ray_integration.py and
replace it with os.killpg-based group termination for the proc launched by
subprocess.Popen.

In `@tools/coverage_support/sitecustomize.py`:
- Around line 4-8: The current suppress(Exception) around importing and calling
coverage.process_startup() hides non-ImportError issues (like
coverage.ConfigError); update the exception filter to only suppress ImportError
so missing coverage installs remain quiet but real coverage misconfigurations
surface. Locate the block that checks os.getenv("COVERAGE_PROCESS_START") and
replace suppress(Exception) with suppress(ImportError) around the import
coverage and coverage.process_startup() call.

---

Nitpick comments:
In `@src/framex/plugin/__init__.py`:
- Around line 216-227: The __all__ export list in framex.plugin.__init__ is
missing the new public utilities used by tests; add "ApiResolver",
"set_current_api_resolver", "reset_current_api_resolver",
"get_current_remote_apis", "set_current_remote_apis", and
"reset_current_remote_apis" to the __all__ list so these symbols are exported
from the package (they are the identifiers imported elsewhere and implemented
alongside ApiType/BasePlugin/PluginMetadata).
- Around line 34-40: The helper _coerce_plugin_api currently returns None for
unexpected types which can hide bugs; update _coerce_plugin_api to either raise
a TypeError (including the actual type received) or log an error before
returning None so callers get immediate feedback — locate the _coerce_plugin_api
staticmethod and replace the final `return None` path with a TypeError that
mentions the received value/type (or call your module logger with a clear
message) while keeping the existing branches that accept PluginApi, dict, and
None and continuing to use PluginApi.model_validate for dict inputs.
- Line 104: The code references ApiType (used in the all_apis assignment with
_manager) before ApiType is imported later in the file; update the top import
from framex.plugin.model to include ApiType (so ApiType is available when
all_apis is defined), and remove the duplicate/bottom import of ApiType at the
end of the file to avoid redundancy and IDE/type-checker confusion; ensure you
modify the import grouping that currently brings in models (the import that
already brings in other symbols from framex.plugin.model) and then delete the
later ApiType import.

In `@tests/test_plugin.py`:
- Line 17: Move the "import inspect" statement from its current location after
the first test function up to the top of the module with the other imports and
remove the duplicate import where it currently appears; locate the stray "import
inspect" and relocate it to the module import block so all imports are grouped
at the top following PEP8.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ebd580e-2656-4743-b55c-f3ce8869185b

📥 Commits

Reviewing files that changed from the base of the PR and between b97e8eb and 8e5bbea.

📒 Files selected for processing (13)
  • .coveragerc
  • pytest.ini
  • src/framex/__init__.py
  • src/framex/plugin/__init__.py
  • src/framex/plugin/base.py
  • tests/conftest.py
  • tests/ray_plugins/__init__.py
  • tests/ray_plugins/test_service_caller.py
  • tests/test_exception.py
  • tests/test_plugin.py
  • tests/test_use_ray_integration.py
  • tools/coverage_support/__init__.py
  • tools/coverage_support/sitecustomize.py

Comment thread pytest.ini
Comment thread src/framex/__init__.py
Comment on lines +69 to +75
def _setup_ray_worker() -> None: # pragma: no cover
settings.server.use_ray = True

import framex.adapter as adapter_module

adapter_module._adapter = None
_setup_sentry()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mirror the Ray-mode switch in the driver process.

_setup_ray_worker() fixes worker-side state, but run(use_ray=True) still leaves settings.server.use_ray unchanged in the parent process. Anything that reads the setting before workers start—like framex.adapter.get_adapter() and BasePlugin.__init__()—still sees local mode unless callers mutate settings themselves, which is why tests/test_use_ray_integration.py Lines 45 and 96 have to do that manually.

Suggested fix
     num_cpus = num_cpus if num_cpus is not None else settings.server.num_cpus
     use_ray = use_ray if use_ray is not None else settings.server.use_ray
+    settings.server.use_ray = use_ray
     enable_proxy = enable_proxy if enable_proxy is not None else settings.server.enable_proxy

Also applies to: 158-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/__init__.py` around lines 69 - 75, The driver process never flips
the Ray mode flag, so calls like framex.adapter.get_adapter() and
BasePlugin.__init__ still see local mode; mirror the worker-side change by
setting settings.server.use_ray = True in the driver startup path when
run(use_ray=True) is invoked (the same place that currently prepares workers),
and also reset adapter state (adapter_module._adapter = None) there so the
driver and workers have consistent mode; apply the same update to the other
startup site noted (the other invocation that prepares Ray) so both entry paths
set settings.server.use_ray and clear adapter state.

Comment thread src/framex/plugin/base.py
Comment thread tests/conftest.py
Comment on lines +99 to +107
proc = subprocess.Popen( # noqa: S603
[sys.executable, "-c", launch_code],
cwd=repo_root,
env=env,
stdout=log_file,
stderr=subprocess.STDOUT,
text=True,
start_new_session=True,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n tests/test_use_ray_integration.py | head -130 | tail -40

Repository: touale/FrameX-kit

Length of output: 1843


🏁 Script executed:

cat -n tests/test_use_ray_integration.py

Repository: touale/FrameX-kit

Length of output: 4973


🏁 Script executed:

python3 << 'EOF'
import subprocess
import sys

# Test 1: Check what Popen.terminate() actually does
# On POSIX, it calls os.kill(self.pid, signal.SIGTERM)
# NOT os.killpg() which would kill the process group

# Let's check the subprocess module source documentation
help(subprocess.Popen.terminate)
EOF

Repository: touale/FrameX-kit

Length of output: 166


🏁 Script executed:

rg -i "killpg|process.group|session" tests/ src/ --type py -A 3 -B 3

Repository: touale/FrameX-kit

Length of output: 1924


🏁 Script executed:

# Check if there are any other tests or examples using start_new_session=True
rg "start_new_session" --type py -A 5 -B 5

Repository: touale/FrameX-kit

Length of output: 755


🏁 Script executed:

# Let's verify the actual behavior described in the review
# Check if the subprocess module's terminate() sends signal only to main process or whole group
python3 << 'EOF'
import os
import signal

# On POSIX, Popen.terminate() calls os.kill(self.pid, signal.SIGTERM)
# This sends signal to a single process, NOT the process group

# If we want to kill a process group created with start_new_session=True,
# we need os.killpg(os.getpgid(pid), signal.SIGTERM)
# OR os.killpg(pid, signal.SIGTERM) if pid is the group leader

print("Key facts about process groups with start_new_session=True:")
print("1. start_new_session=True creates a new process group on POSIX")
print("2. The subprocess becomes a session and process group leader")
print("3. proc.terminate() calls os.kill(pid, SIGTERM) - single process only")
print("4. proc.kill() calls os.kill(pid, SIGKILL) - single process only")
print("5. To kill the group: os.killpg(proc.pid, SIGTERM) works because proc.pid is the group leader")
print("6. Any child processes started by proc become part of the same group")
print("7. Only signaling the group leader won't kill the children unless it exits")
EOF

Repository: touale/FrameX-kit

Length of output: 617


🏁 Script executed:

# Check launch_code more carefully - does it create child processes?
rg -i "ray\.|serve\." tests/test_use_ray_integration.py -A 2

Repository: touale/FrameX-kit

Length of output: 271


Kill the entire Ray session group on cleanup, not just the launcher process.

start_new_session=True creates a new process group where the launcher becomes the group leader. When cleanup calls proc.terminate() or proc.kill(), it only signals that single process via os.kill(). If the launcher times out before its serve.shutdown()/ray.shutdown() completes, Ray's worker and dashboard child processes—which are in the same process group—survive cleanup and leak resources (ports, processes) that poison subsequent tests.

Use os.killpg() to terminate the entire process group:

Suggested fix
 import json
 import os
 import socket
+import signal
 import subprocess
 import sys
 import tempfile
@@
             if proc.poll() is None:
-                proc.terminate()
+                if os.name == "posix":
+                    os.killpg(proc.pid, signal.SIGTERM)
+                else:
+                    proc.terminate()
                 with suppress(subprocess.TimeoutExpired):
                     proc.wait(timeout=10)
                 if proc.poll() is None:
-                    proc.kill()
+                    if os.name == "posix":
+                        os.killpg(proc.pid, signal.SIGKILL)
+                    else:
+                        proc.kill()
                     proc.wait(timeout=10)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_use_ray_integration.py` around lines 99 - 107, The test uses
subprocess.Popen(..., start_new_session=True) to launch Ray but cleanup
currently calls proc.terminate()/proc.kill() which only targets the launcher
process; change the cleanup logic to kill the entire process group using
os.killpg(os.getpgid(proc.pid), signal.SIGTERM) (and fall back to SIGKILL if
needed) instead of os.kill(proc.pid, ...), ensuring you import os and signal and
handle OSError for already-exited processes; locate the teardown code that
references proc.terminate()/proc.kill() in tests/test_use_ray_integration.py and
replace it with os.killpg-based group termination for the proc launched by
subprocess.Popen.

Comment on lines +4 to +8
if os.getenv("COVERAGE_PROCESS_START"):
with suppress(Exception):
import coverage

coverage.process_startup()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n tools/coverage_support/sitecustomize.py

Repository: touale/FrameX-kit

Length of output: 289


🏁 Script executed:

wc -l tools/coverage_support/sitecustomize.py

Repository: touale/FrameX-kit

Length of output: 102


🏁 Script executed:

# Search for other uses of suppress(Exception) or similar patterns in the codebase
rg "suppress\(Exception\)" -A 3 -B 1

Repository: touale/FrameX-kit

Length of output: 390


🏁 Script executed:

# Check if there are any comments or documentation about why broad exception suppression was chosen
rg "COVERAGE_PROCESS_START" -B 5 -A 10

Repository: touale/FrameX-kit

Length of output: 1616


🏁 Script executed:

# Look at git history to understand context if available
git log --oneline tools/coverage_support/sitecustomize.py 2>/dev/null | head -20

Repository: touale/FrameX-kit

Length of output: 121


🏁 Script executed:

# Look for documentation or comments about coverage.process_startup behavior
rg "process_startup" -B 3 -A 3

Repository: touale/FrameX-kit

Length of output: 310


🏁 Script executed:

# Check if there are any other sitecustomize files in related projects or docs
find . -name "sitecustomize.py" -o -name "*.md" | xargs grep -l "sitecustomize" 2>/dev/null | head -5

Repository: touale/FrameX-kit

Length of output: 43


🏁 Script executed:

# Look at the commit that introduced this to understand the context
git show 940761f --stat 2>/dev/null | head -30

Repository: touale/FrameX-kit

Length of output: 1709


🏁 Script executed:

# Search for any error handling patterns or discussion about coverage in issues/tests
rg "coverage" -i tools/coverage_support/ -A 2 -B 2

Repository: touale/FrameX-kit

Length of output: 503


🌐 Web query:

coverage.py process_startup() exceptions documentation

💡 Result:

The coverage.py process_startup function is documented in the official Coverage.py API module documentation. It is used to start coverage measurement automatically at Python startup for subprocesses. Signature (Coverage.py 7.13.4+): coverage.process_startup(*, force=False, slug='default') It checks environment variables COVERAGE_PROCESS_START or COVERAGE_PROCESS_CONFIG. If set, it loads the specified config and starts a Coverage instance. Returns the Coverage instance or None if not started. No specific exceptions are documented as being raised directly by process_startup. Coverage.py raises subclasses of CoverageException in general, including: - ConfigError: problems with config files or values. - CoverageWarning: warnings (not exceptions). - DataError: errors with data files. - NoCode: no code found. - NoDataError: no data. - NoSource: source not found. - NotPython: not parsable Python. - PluginError: plugin issues. Coverage class methods (used internally) can raise these exceptions. Misuse, like calling in the main process run by 'coverage run', can lead to AssertionError in the collector (as seen in user reports), but this is not official behavior—process_startup is for sitecustomize.py or .pth files in subprocess contexts. Full details: https://coverage.readthedocs.io/en/latest/api_module.html and https://coverage.readthedocs.io/en/latest/subprocess.html

Citations:


Catch only ImportError to surface coverage misconfigurations.

The broad suppress(Exception) silently swallows ConfigError and other coverage-related exceptions, masking misconfiguration and causing undercounting without CI failure. Only ImportError (when coverage is not installed) should be suppressed.

Suggested fix
 import os
-from contextlib import suppress
 
 if os.getenv("COVERAGE_PROCESS_START"):
-    with suppress(Exception):
-        import coverage
-
-        coverage.process_startup()
+    try:
+        import coverage
+    except ImportError:
+        pass
+    else:
+        coverage.process_startup()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/coverage_support/sitecustomize.py` around lines 4 - 8, The current
suppress(Exception) around importing and calling coverage.process_startup()
hides non-ImportError issues (like coverage.ConfigError); update the exception
filter to only suppress ImportError so missing coverage installs remain quiet
but real coverage misconfigurations surface. Locate the block that checks
os.getenv("COVERAGE_PROCESS_START") and replace suppress(Exception) with
suppress(ImportError) around the import coverage and coverage.process_startup()
call.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
src/framex/__init__.py (1)

69-75: ⚠️ Potential issue | 🟠 Major

Mirror Ray mode + adapter reset in the driver path as well.

This worker-only setup is good, but run(use_ray=True) still doesn’t update driver state before plugin/deployment initialization. Please set settings.server.use_ray and clear framex.adapter._adapter in run() right after resolving use_ray, so driver and workers stay consistent.

Suggested fix
@@
     num_cpus = num_cpus if num_cpus is not None else settings.server.num_cpus
     use_ray = use_ray if use_ray is not None else settings.server.use_ray
+    settings.server.use_ray = use_ray
+    if use_ray:
+        import framex.adapter as adapter_module
+        adapter_module._adapter = None
     enable_proxy = enable_proxy if enable_proxy is not None else settings.server.enable_proxy
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/__init__.py` around lines 69 - 75, The driver path currently
doesn't mirror the worker-only Ray setup: after resolving the use_ray flag in
run(), set settings.server.use_ray = True/False accordingly and clear
framex.adapter._adapter (set to None) just like _setup_ray_worker() does, so
driver and workers share the same Ray mode before plugin/deployment
initialization; update the run() function to perform these two actions
immediately after resolving use_ray.
tests/conftest.py (1)

75-78: ⚠️ Potential issue | 🟠 Major

Narrow the import exception to ImportError only.

Line 77 catches Exception, which can hide real coverage/config failures and silently skip merge. Restrict this to ImportError so non-import failures surface.

Suggested fix
     try:
         from coverage import Coverage
-    except Exception:
+    except ImportError:
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/conftest.py` around lines 75 - 78, The try/except around importing
Coverage in tests/conftest.py currently catches all Exceptions which can hide
real errors; change the broad except to except ImportError so only
missing-imports are suppressed—locate the try: from coverage import Coverage
block and replace "except Exception:" with "except ImportError:" ensuring other
exceptions propagate.
src/framex/plugin/base.py (1)

86-88: ⚠️ Potential issue | 🟠 Major

Keep _call_remote_api() instance-scoped outside request handlers.

This still only works when the call happens under one of the wrapped @on_request methods. on_start() at Line 31 is not wrapped, so worker-side calls can still fall back to the global resolver and raise API ... is not found even when the plugin declared that dependency.

Suggested fix
 `@final`
 async def _call_remote_api(self, api_name: str, **kwargs: Any) -> Any:
-    res = await call_plugin_api(api_name, **kwargs)
+    resolver_token = set_current_api_resolver(self.api_resolver)
+    remote_token = set_current_remote_apis(self.remote_apis)
+    try:
+        res = await call_plugin_api(api_name, resolver=self.api_resolver, **kwargs)
+    finally:
+        reset_current_remote_apis(remote_token)
+        reset_current_api_resolver(resolver_token)
     return self._post_call_remote_api_hook(res)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/base.py` around lines 86 - 88, The current _call_remote_api
calls the global call_plugin_api which relies on a global resolver and fails for
worker-side calls (e.g., on_start); change _call_remote_api to use an
instance-scoped API caller stored on the plugin instance (instead of the global
call_plugin_api). Concretely, introduce or use an instance attribute (e.g.,
self._call_plugin_api or self.call_plugin_api) that is initialized during plugin
construction/initialization and replace call_plugin_api(...) with
self._call_plugin_api(...), keeping the existing post-call handling via
_post_call_remote_api_hook(res); ensure the instance-scoped caller is available
for on_start and other non-@on_request handlers.
🧹 Nitpick comments (1)
tests/test_plugin.py (1)

195-213: Assert the serialized API payload is rehydrated before dispatch.

This test still passes if call_plugin_api() skips PluginApi.model_validate() and forwards the raw dict into call_func(). Add an assertion on the first adapter argument being a PluginApi (or matching api) so the Ray serialization path is actually covered.

Suggested test strengthening
             mock_get_api.assert_not_called()
             assert result == "interval_dict_result"
+            dispatched_api = mock_adapter.return_value.call_func.call_args[0][0]
+            assert isinstance(dispatched_api, PluginApi)
+            assert dispatched_api.api == api.api
+            assert dispatched_api.deployment_name == api.deployment_name
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_plugin.py` around lines 195 - 213, The test
test_call_plugin_api_with_interval_apis_dict_payload can pass even if
call_plugin_api forwards the raw dict; update the test to assert the adapter's
call_func was invoked with a rehydrated PluginApi instance by checking the first
positional argument passed to mock_adapter.return_value.call_func is an instance
equal to (or has the same attributes as) the original api — i.e., after
obtaining result, inspect mock_adapter.return_value.call_func.call_args or
call_args_list and assert isinstance(first_arg, PluginApi) or
first_arg.model_dump() == api.model_dump(); this ensures call_plugin_api invokes
PluginApi.model_validate and doesn't forward the raw dict from
set_current_remote_apis.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/framex/__init__.py`:
- Around line 69-75: The driver path currently doesn't mirror the worker-only
Ray setup: after resolving the use_ray flag in run(), set
settings.server.use_ray = True/False accordingly and clear
framex.adapter._adapter (set to None) just like _setup_ray_worker() does, so
driver and workers share the same Ray mode before plugin/deployment
initialization; update the run() function to perform these two actions
immediately after resolving use_ray.

In `@src/framex/plugin/base.py`:
- Around line 86-88: The current _call_remote_api calls the global
call_plugin_api which relies on a global resolver and fails for worker-side
calls (e.g., on_start); change _call_remote_api to use an instance-scoped API
caller stored on the plugin instance (instead of the global call_plugin_api).
Concretely, introduce or use an instance attribute (e.g., self._call_plugin_api
or self.call_plugin_api) that is initialized during plugin
construction/initialization and replace call_plugin_api(...) with
self._call_plugin_api(...), keeping the existing post-call handling via
_post_call_remote_api_hook(res); ensure the instance-scoped caller is available
for on_start and other non-@on_request handlers.

In `@tests/conftest.py`:
- Around line 75-78: The try/except around importing Coverage in
tests/conftest.py currently catches all Exceptions which can hide real errors;
change the broad except to except ImportError so only missing-imports are
suppressed—locate the try: from coverage import Coverage block and replace
"except Exception:" with "except ImportError:" ensuring other exceptions
propagate.

---

Nitpick comments:
In `@tests/test_plugin.py`:
- Around line 195-213: The test
test_call_plugin_api_with_interval_apis_dict_payload can pass even if
call_plugin_api forwards the raw dict; update the test to assert the adapter's
call_func was invoked with a rehydrated PluginApi instance by checking the first
positional argument passed to mock_adapter.return_value.call_func is an instance
equal to (or has the same attributes as) the original api — i.e., after
obtaining result, inspect mock_adapter.return_value.call_func.call_args or
call_args_list and assert isinstance(first_arg, PluginApi) or
first_arg.model_dump() == api.model_dump(); this ensures call_plugin_api invokes
PluginApi.model_validate and doesn't forward the raw dict from
set_current_remote_apis.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d1fd49d-8ca4-4c07-b296-f4ea2e978e07

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5bbea and 940761f.

📒 Files selected for processing (13)
  • .coveragerc
  • pytest.ini
  • src/framex/__init__.py
  • src/framex/plugin/__init__.py
  • src/framex/plugin/base.py
  • tests/conftest.py
  • tests/ray_plugins/__init__.py
  • tests/ray_plugins/test_service_caller.py
  • tests/test_exception.py
  • tests/test_plugin.py
  • tests/test_use_ray_integration.py
  • tools/coverage_support/__init__.py
  • tools/coverage_support/sitecustomize.py
✅ Files skipped from review due to trivial changes (4)
  • pytest.ini
  • tools/coverage_support/sitecustomize.py
  • .coveragerc
  • tests/ray_plugins/test_service_caller.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_exception.py
  • tests/test_use_ray_integration.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/framex/plugin/base.py (1)

85-87: ⚠️ Potential issue | 🟠 Major

Reapply the instance resolver in _call_remote_api.

Line 87 now relies entirely on ambient context, but the wrapper at Lines 36-83 only runs for _on_request handlers. Calls from on_start() or any other non-request path can still miss self.remote_apis / self.api_resolver and regress to API ... is not found in worker-local execution.

Suggested fix
 `@final`
 async def _call_remote_api(self, api_name: str, **kwargs: Any) -> Any:
-    res = await call_plugin_api(api_name, **kwargs)
+    resolver_token = set_current_api_resolver(self.api_resolver)
+    remote_token = set_current_remote_apis(self.remote_apis)
+    try:
+        res = await call_plugin_api(api_name, resolver=self.api_resolver, **kwargs)
+    finally:
+        reset_current_remote_apis(remote_token)
+        reset_current_api_resolver(resolver_token)
     return self._post_call_remote_api_hook(res)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/base.py` around lines 85 - 87, The _call_remote_api
currently calls call_plugin_api without restoring the instance resolver context,
so calls from non-request paths can miss self.remote_apis/self.api_resolver and
fail; update _call_remote_api to reapply the same resolver wiring used by the
_on_request wrapper: if self.api_resolver or self.remote_apis exists, pass that
resolver/context into call_plugin_api (e.g. via api_resolver/remote_apis kwargs
or by invoking the same resolver restoration helper used by _on_request) so
remote API lookups use the instance-specific resolver.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framex/plugin/__init__.py`:
- Around line 90-93: The current early return when api_name is a PluginApi
bypasses the remote-apis guard; after calling get_current_remote_apis() you must
validate pre-resolved PluginApi inputs against the same
required_remote_apis/current_remote_apis rules as string names. Modify the
branch that handles isinstance(api_name, PluginApi) (and uses api_name.call_type
/ ApiType.PROXY) to first consult get_current_remote_apis() and
required_remote_apis and only allow the PluginApi if the remote_apis guard
permits it (otherwise raise or reject); ensure the same validation logic used
for string api_name paths is reused for PluginApi instances.
- Around line 94-98: active_resolver may be None because
get_current_api_resolver() and get_default_api_resolver() return optionals;
ensure active_resolver is non-None before using it by providing a final fallback
resolver instance (e.g., construct or call a known default ApiResolver) when
resolver or the two getters return None; modify the assignment of
active_resolver and subsequent uses in this block (references: active_resolver,
resolver, get_current_api_resolver, get_default_api_resolver) so that
coerce_plugin_api and resolve are only called on a guaranteed resolver object,
and keep existing branches using current_remote_apis and api_name unchanged.

---

Duplicate comments:
In `@src/framex/plugin/base.py`:
- Around line 85-87: The _call_remote_api currently calls call_plugin_api
without restoring the instance resolver context, so calls from non-request paths
can miss self.remote_apis/self.api_resolver and fail; update _call_remote_api to
reapply the same resolver wiring used by the _on_request wrapper: if
self.api_resolver or self.remote_apis exists, pass that resolver/context into
call_plugin_api (e.g. via api_resolver/remote_apis kwargs or by invoking the
same resolver restoration helper used by _on_request) so remote API lookups use
the instance-specific resolver.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 844b26f1-115d-4345-b53c-6a1ae281490b

📥 Commits

Reviewing files that changed from the base of the PR and between 6c184cb and d7e3262.

📒 Files selected for processing (5)
  • src/framex/plugin/__init__.py
  • src/framex/plugin/base.py
  • src/framex/plugin/manage.py
  • src/framex/plugin/resolver.py
  • tests/test_plugin.py

Comment on lines +90 to +93
current_remote_apis = get_current_remote_apis()
if isinstance(api_name, PluginApi):
api: PluginApi | None = api_name
elif isinstance(api_name, str):
api = interval_apis.get(api_name) if interval_apis else _manager.get_api(api_name)
use_proxy = False
if not api:
if isinstance(api_name, str) and api_name.startswith("/") and settings.server.enable_proxy:
return api_name, api_name.call_type == ApiType.PROXY

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the remote_apis guard to pre-resolved PluginApi inputs too.

Lines 91-93 return immediately for PluginApi arguments, so once current_remote_apis is set a caller can bypass the new required_remote_apis check just by passing a PluginApi instance instead of a string. That reopens undeclared local cross-plugin calls in the exact path this PR is trying to harden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/__init__.py` around lines 90 - 93, The current early return
when api_name is a PluginApi bypasses the remote-apis guard; after calling
get_current_remote_apis() you must validate pre-resolved PluginApi inputs
against the same required_remote_apis/current_remote_apis rules as string names.
Modify the branch that handles isinstance(api_name, PluginApi) (and uses
api_name.call_type / ApiType.PROXY) to first consult get_current_remote_apis()
and required_remote_apis and only allow the PluginApi if the remote_apis guard
permits it (otherwise raise or reject); ensure the same validation logic used
for string api_name paths is reused for PluginApi instances.

Comment on lines +94 to +98
active_resolver = resolver or get_current_api_resolver() or get_default_api_resolver()
if current_remote_apis is not None:
api = active_resolver.coerce_plugin_api(current_remote_apis.get(api_name))
else:
api = active_resolver.resolve(api_name, None)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "__init__.py" -path "*/framex/plugin/*" | head -20

Repository: touale/FrameX-kit

Length of output: 92


🏁 Script executed:

cat -n src/framex/plugin/__init__.py | sed -n '85,110p'

Repository: touale/FrameX-kit

Length of output: 1260


🏁 Script executed:

rg -A 3 "def get_current_api_resolver|def get_default_api_resolver" --type py

Repository: touale/FrameX-kit

Length of output: 484


🏁 Script executed:

rg -A 5 "def _resolve_plugin_api" src/framex/plugin/__init__.py

Repository: touale/FrameX-kit

Length of output: 278


🏁 Script executed:

rg -B 2 -A 10 "class ApiResolver" --type py | head -50

Repository: touale/FrameX-kit

Length of output: 832


🏁 Script executed:

rg "_manager" src/framex/plugin/__init__.py

Repository: touale/FrameX-kit

Length of output: 496


Ensure active_resolver is always non-None before dereferencing it.

Line 94 produces ApiResolver | None since both get_current_api_resolver() and get_default_api_resolver() return optional types. Lines 96 and 98 will raise AttributeError if both resolver functions return None. Add a fallback to instantiate a default resolver:

Suggested fix
 def _resolve_plugin_api(
     api_name: str | PluginApi,
     resolver: ApiResolver | None = None,
 ) -> tuple[PluginApi, bool]:
     current_remote_apis = get_current_remote_apis()
+    active_resolver = (
+        resolver
+        or get_current_api_resolver()
+        or get_default_api_resolver()
+        or ApiResolver(manager=_manager)
+    )
     if isinstance(api_name, PluginApi):
         return api_name, api_name.call_type == ApiType.PROXY
 
-    active_resolver = resolver or get_current_api_resolver() or get_default_api_resolver()
     if current_remote_apis is not None:
-        api = active_resolver.coerce_plugin_api(current_remote_apis.get(api_name))
+        api = ApiResolver.coerce_plugin_api(current_remote_apis.get(api_name))
     else:
         api = active_resolver.resolve(api_name, None)
🧰 Tools
🪛 GitHub Actions: Test Project

[error] 96-96: mypy error: Item "None" of "ApiResolver | None" has no attribute "coerce_plugin_api" [union-attr]


[error] 98-98: mypy error: Item "None" of "ApiResolver | None" has no attribute "resolve" [union-attr]

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/plugin/__init__.py` around lines 94 - 98, active_resolver may be
None because get_current_api_resolver() and get_default_api_resolver() return
optionals; ensure active_resolver is non-None before using it by providing a
final fallback resolver instance (e.g., construct or call a known default
ApiResolver) when resolver or the two getters return None; modify the assignment
of active_resolver and subsequent uses in this block (references:
active_resolver, resolver, get_current_api_resolver, get_default_api_resolver)
so that coerce_plugin_api and resolve are only called on a guaranteed resolver
object, and keep existing branches using current_remote_apis and api_name
unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framex/plugin/resolver.py`:
- Around line 58-60: The setter _set_default_api_resolver currently accepts None
and can clear the global _default_api_resolver causing
get_default_api_resolver() to fail later; change _set_default_api_resolver to
disallow None by updating its signature/typing to accept only ApiResolver (not
ApiResolver | None) and/or add a guard that raises a clear error if resolver is
None, and ensure the global _default_api_resolver assignment always receives a
non-null ApiResolver; update any callers and type hints accordingly to maintain
non-nullability for _default_api_resolver and get_default_api_resolver().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66d42a51-d98a-496d-aeb7-f051607148c8

📥 Commits

Reviewing files that changed from the base of the PR and between d7e3262 and 37212a3.

📒 Files selected for processing (1)
  • src/framex/plugin/resolver.py

Comment thread src/framex/plugin/resolver.py Outdated
@touale touale merged commit b840d19 into master Mar 26, 2026
8 checks passed
@touale touale deleted the fix-plugin-api-not-found branch March 26, 2026 03:49
@coderabbitai coderabbitai bot mentioned this pull request Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin API Not Found During Local Cross-Plugin Invocation (Works via Proxy)

1 participant