chore: enable ruff PLC0415 and clear the import backlog - #607
Open
AlexanderZ-Band wants to merge 10 commits into
Open
chore: enable ruff PLC0415 and clear the import backlog#607AlexanderZ-Band wants to merge 10 commits into
AlexanderZ-Band wants to merge 10 commits into
Conversation
…klog Adds `extend-select = ["PLC0415"]` to `[tool.ruff.lint]` and resolves every resulting hit across src/, tests/, examples/, docker/, and band-bridge/ with one of three treatments: - Extras-gated: a local import for a module behind an optional pip extra not installed in every CI lane (adapters, `httpx` in opencode-only tests, etc.) gets `# noqa: PLC0415`. - Genuine violation: no real justification for locality — moved to the top-level import block (the large majority of fixes, concentrated in tests/). - Other legitimate reason, kept local with a documented `# noqa: PLC0415`: a verified real circular import (`band/agent.py`'s `PlatformSettings`/ `load_agent_config`, empirically proven by moving them and re-running `python -c "import band.agent"` — moving them reorders `band.agent`'s own `band.core.*` imports behind `band.config`, which re-enters `band.logging_config` mid-init), an always-loaded pytest plugin/conftest module where a top-level import would tax every test session regardless of whether the specific fixture is used (`tests/conftest.py`, `tests/markdown_docs/fixtures.py`, `tests/e2e/baseline/fixtures/platform.py`), or a test whose entire subject is the import statement itself (`test_readme_snippets.py`, `test_lazy_exports.py`, `test_band_import.py`). `uv run ruff check .` passes clean repo-wide, `uv run ruff format .` is a no-op, `uv run pyrefly check` reports 0 errors, and the full unit suite (5272 passed, 123 skipped) is green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
Moves five pytest-plugin fixture imports (tests/conftest.py, markdown_docs/ fixtures.py, e2e/baseline/fixtures/platform.py) and the three docker runners' adapter imports to top-level: their "deferred for import cost" justification was false since root conftest.py already eagerly loads the whole band.* module graph and the docker images are single-framework. Reclassifies most remaining bucket-1 "extras-gated" sites: dev bundles every framework's third-party deps except crewai/parlant, so only those two, plus files ci.yml confirms run under crewai/parlant's isolated venvs, plus import-surface pin tests, plus files with no top-level SDK import already, keep a genuine deferral reason. Everything else was either 100% redundant (a local import shadowing a name already loaded at the file's own top level, ~110 sites across dozens of files) or had no real justification and moves to top-level. Adds a real one-line reason to every remaining noqa'd site that isn't already self-documented by a try/except guard. Caught and reverted one regression along the way: band/adapters/ langgraph.py's local imports look like the same "already loaded at top" redundancy, but tests patch band.integrations.langgraph.langchain_tools and langchain.agents.create_agent at their source module, which only works if the adapter looks them up at call time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
sqlite3 (examples/langgraph/standalone_sql_agent.py) and threading (examples/a2a_gateway/02_with_demo_agent.py) were deferred with no real reason: both are stdlib, always available, not circular, not expensive to import. Also clarifies why test_crewai_flow_adapter.py's lazy-import test keeps its import local (it's the band.adapters lazy loader itself under test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…fixes Cleanup pass over the noqa's this PR added, verified empirically against the actual import graph rather than trusting each comment's stated reason: - band.integrations.acp/__init__.py was already made fully lazy in an earlier change, so the "avoid circular import" comments on converters/acp_client.py and acp_server.py describe a cycle that no longer exists. Hoisted both to real top-level imports. - band.integrations.a2a, .a2a.gateway, and .slack still eagerly import their adapter module from __init__.py, which is what made their converters' cycles genuinely real. Made all three lazy via the same lazy_exports helper band.adapters/band.converters already use (matching the pattern band.integrations.acp already proved out), then hoisted the now-safe converter imports. - src/band/adapters/langgraph.py kept three imports local not because of an optional extra, but because tests patched the original module (langchain.agents.create_agent, .langchain_tools.agent_tools_to_langchain) and a local import was the only way the patch took effect. Imported the modules themselves at top level and switched call sites to attribute access instead, so the existing mock.patch targets keep working with a real top-level import. - tests/skills/bughunting/test_runner.py deferred one toolkit import to "avoid pulling the e2e baseline toolkit into every skills-test collection", but two sibling submodules of the same toolkit were already imported unconditionally in the same file. - tests/framework_configs/adapters.py deferred CrewAIAdapter/ CrewAIFlowAdapter behind "isolates the crewai extra", but both classes import cleanly with crewai absent (verified against this repo's crewai-less dev venv) — same treatment ClaudeSDKAdapter already got in this file. - tests/runtime/test_human_tools.py imported ChatMessageRequest/ ParticipantRequest from band_rest directly instead of the band.client.rest re-export the same file already imports from. - examples/slack/01_basic_bot.py hoisted `import uvicorn` to module scope, but SLACK_TRANSPORT defaults to socket mode, which never touches uvicorn; moved back into the http-transport branch. Full unit suite (5272 passed), ruff, and pyrefly all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
The three lazy __init__.py files copied a "Type-only imports for static analysis" comment onto a TYPE_CHECKING block, which the block already says. Removed it; also shortened the crewai-safety comment down to the one non-obvious fact plus the pointer to the test that shows the cost of faking the package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
Systematic categorization of every remaining noqa: PLC0415 in the repo by
the reason it didn't move, then per-category empirical verification of
whether hoisting is actually safe (import the module cold, check for
unconditional third-party imports at its real top level, check whether any
test patches the original location in a way that depends on late binding).
Confirmed genuinely necessary and left alone: extras truly absent from a
venv with a friendly ImportError, real circular imports, per-branch
extra-selection in multi-framework example scripts, and tests whose literal
subject is the import statement itself (~200 remaining instances).
Hoisted the ones that weren't:
- tests/framework_configs/{adapters,converters,output_adapters}.py: nearly
the entire "isolates the X extra" registry pattern turned out to be
unfounded. Every converter module (band.converters.*) only ever
transforms dicts/dataclasses and has zero third-party imports at its own
top level, so all 24 "isolates" deferrals in converters.py were
pointless — hoisted the lot into one top-level block. In adapters.py,
ParlantAdapter/GoogleADKAdapter both construct with their SDK absent
(verified empirically for parlant, by source inspection for google_adk:
every import is TYPE_CHECKING-only or method-local), and Codex/Opencode
have no pip package at all (they shell out to a CLI) — hoisted all four.
- src/band/integrations/codex/websocket_client.py: `websockets` is not
actually optional — band-sdk's own hard dependency
phoenix-channels-python-client requires it unconditionally, so every
install has it. Hoisted, and switched to a module-level `import
websockets.asyncio.client` with attribute-access calls (not `from ...
import connect`) so the existing `monkeypatch.setattr("websockets.asyncio
.client.connect", ...)` test patches keep working — bare `from X import
Y` would have bound a stale local copy, same pitfall as the langgraph.py
fix in the previous commit.
- src/band/integrations/acp/cli.py: `from band import Agent` was grouped
with three genuinely acp-extra-gated imports, but `band.Agent` doesn't
need the acp extra — this file's own top-level `band.config.logs` import
already runs all of band/__init__.py, which unconditionally binds Agent.
- tests/e2e/baseline/toolkit/provisioning.py: `build_adapter`'s own module
has no unconditional third-party imports either, and no cycle back to
provisioning.py exists.
- tests/adapters/test_crewai_flow_phase3.py: one test re-imported
nest_asyncio locally with its own try/except+skip instead of using the
module's existing `requires_nest_asyncio` marker and top-level
`_HAS_NEST_ASYNCIO` guard, which every other nest_asyncio-dependent test
in the file already does.
Full unit suite (5272 passed), ruff, and pyrefly all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
acp_client.py and a2a_gateway.py hoisted their ACPClientSessionState / GatewaySessionState imports to top level while clearing PLC0415 hits, but each name resolves through a chain that imports an optional extra (agent-client-protocol, a2a-sdk) at module load — turning what used to be a deferred failure into a hard ModuleNotFoundError for any caller who imports the converter without that extra installed. Verified live: both modules import cleanly on main without the extras, and fail on this branch; restoring the TYPE_CHECKING + deferred-import pattern (now noqa'd with the real reason) fixes it on both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…s justification Hoisting both to unconditional top-level for PLC0415 broke their extras-gated contract: langchain.agents is only needed by LangGraphAdapter's simple llm= pattern, not graph_factory= callers, and websockets ships only under the codex extra. Both regress to a hard ModuleNotFoundError at import time instead of the intended deferred/guarded failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
…og-INT-1305 Reconciles the PLC0415 import-locality rule with main's Capability.FILES rollout (#600): adopts main's rewritten crewai tool builder (ToolSpec catalog + generic wrapper, replacing the old per-tool BaseTool classes) and BandTool as the shared agent-tool-name vocabulary, then sweeps the resulting PLC0415 violations the same way the original rollout did -- promote a redundant or non-extras-gated local import to the top-level block, or add a documented noqa for a genuine extras-gated or circular import (test_files_image_passthrough_matrix.py's per-framework probes, test_adapter_conformance.py's circular reference to test_capability_matrix.py). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
An earlier PLC0415 hoist moved the agno/gemini/langchain/pydantic_ai/ strands converter imports in tests/framework_configs/converters.py to module level, defeating the per-builder try/except in _build_converter_configs() that's supposed to isolate a missing optional framework. Any pytest run that collects tests/framework_conformance/conftest.py under the dev-crewai or dev-parlant venvs (neither installs agno, gemini, langchain, pydantic_ai, or strands) now fails at import time instead of just skipping those configs. Move each gated import back into its own factory function, matching the deferred-import convention already used for adapters in tests/framework_configs/adapters.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enables ruff's
PLC0415(import-outside-top-level) rule viaextend-select = ["PLC0415"]in[tool.ruff.lint]— additive, so no otherlint behavior changes — and resolves every one of the ~700 resulting hits
across
src/,tests/,examples/,docker/, andband-bridge/.Triage
Each hit got one of three treatments:
# noqa: PLC0415+ a comment naming the extra) — theimport is behind an optional pip extra not installed in every CI
lane/venv (framework adapters,
httpxinopencode-only tests,agent-client-protocol, etc.). Single-framework Docker runner images(
docker/claude_sdk,docker/codex,docker/letta) don't qualify forthis bucket — their extra is always installed — and were moved to
top-level instead.
keeping it local. This is the large majority of the fixes, concentrated
in
tests/. About 110 of these were outright redundant local importsshadowing a name already imported at the top of the same file, and were
deleted rather than moved.
# noqa: PLC0415+ a comment explaining why):src/band/agent.py's two imports are a verified real circularimport — empirically proven by moving each to the top and
re-running
python -c "import band.agent": doing so crashes withImportError: cannot import name 'trace_context_scope'. Not a stalecomment — an actual cycle.
src/band/adapters/langgraph.py's four imports must stay local:moving them to top-level broke 3 tests that patch
band.integrations.langgraph.langchain_tools.agent_tools_to_langchain(and similar) at their source module — a top-level import binds the
name before the patch takes effect.
test_readme_snippets.py,test_lazy_exports.py,test_band_import.py,test_crewai_flow_adapter.py's lazy-loader test) test the importstatement itself, so the import IS the test subject.
Every
noqa: PLC0415site now carries either an explicit one-line reasoncomment or is self-documented by an adjacent
try:/except ImportError:block — a bare, unexplained noqa was treated as a defect and fixed during
review (see below).
Final state: 256
PLC0415sites remain (down from ~700), all noqa'dwith a stated reason; everything else was moved to top-level or deleted as
redundant.
Review iteration
This PR went through 3 rounds of independent review before being opened for
human review, each verified empirically against the actual code/tests
rather than by re-reading claims:
justifications that were actually false (the module graph was already
loaded elsewhere, so deferring bought nothing) — moved to top-level; ~90%
of noqa'd sites had no reason comment at all — closed to 0 remaining bare
noqas; 3 docker runner images mis-bucketed as extras-gated — moved to
top-level. Fixing round 1 also caught a real regression before it shipped:
langgraph.py's imports looked redundant like many others, but movingthem broke test mocking — reverted with an explanation.
redundant deletions, the langgraph.py revert, ruff/pyrefly/full test
suite) and found 2 remaining stdlib imports (
sqlite3,threadingin twoexample scripts) with no real justification — moved to top-level.
pre-commit checklist one final time, and confirmed the PR diff and this
description are internally consistent.
Pre-commit checklist (final, commit
348740e8)uv run ruff check .— clean, repo-wideuv run ruff format --check .— clean (886 files)uv run pyrefly check— 0 errorsuv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/ -v— 5272 passed, 123 skipped, 0 failedruff check --select PLC0415 --ignore-noqa .— 256 (matches the stated final count)Test plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch