Skip to content

chore: enable ruff PLC0415 and clear the import backlog - #607

Open
AlexanderZ-Band wants to merge 10 commits into
mainfrom
chore/enable-ruff-plc0415-clear-import-backlog-INT-1305
Open

chore: enable ruff PLC0415 and clear the import backlog#607
AlexanderZ-Band wants to merge 10 commits into
mainfrom
chore/enable-ruff-plc0415-clear-import-backlog-INT-1305

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Enables ruff's PLC0415 (import-outside-top-level) rule via
extend-select = ["PLC0415"] in [tool.ruff.lint] — additive, so no other
lint behavior changes — and resolves every one of the ~700 resulting hits
across src/, tests/, examples/, docker/, and band-bridge/.

Triage

Each hit got one of three treatments:

  1. Extras-gated (# noqa: PLC0415 + a comment naming the extra) — the
    import is behind an optional pip extra not installed in every CI
    lane/venv (framework adapters, httpx in opencode-only tests,
    agent-client-protocol, etc.). Single-framework Docker runner images
    (docker/claude_sdk, docker/codex, docker/letta) don't qualify for
    this bucket — their extra is always installed — and were moved to
    top-level instead.
  2. Genuine violation (moved to top-level) — no real justification for
    keeping it local. This is the large majority of the fixes, concentrated
    in tests/. About 110 of these were outright redundant local imports
    shadowing a name already imported at the top of the same file, and were
    deleted rather than moved.
  3. Other legitimate reason (# noqa: PLC0415 + a comment explaining why):
    • src/band/agent.py's two imports are a verified real circular
      import
      — empirically proven by moving each to the top and
      re-running python -c "import band.agent": doing so crashes with
      ImportError: cannot import name 'trace_context_scope'. Not a stale
      comment — 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.
    • A handful of test files (test_readme_snippets.py,
      test_lazy_exports.py, test_band_import.py,
      test_crewai_flow_adapter.py's lazy-loader test) test the import
      statement itself, so the import IS the test subject.

Every noqa: PLC0415 site now carries either an explicit one-line reason
comment 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 PLC0415 sites remain (down from ~700), all noqa'd
with 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:

  • Round 1 caught: 5 fixture-file "always-loaded pytest plugin" bucket-3
    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 moving
    them broke test mocking — reverted with an explanation.
  • Round 2 independently re-verified round 1's fixes (top-level moves,
    redundant deletions, the langgraph.py revert, ruff/pyrefly/full test
    suite) and found 2 remaining stdlib imports (sqlite3, threading in two
    example scripts) with no real justification — moved to top-level.
  • Round 3 (this pass) re-verified round 2's fixes, reran the full
    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-wide
  • uv run ruff format --check . — clean (886 files)
  • uv run pyrefly check — 0 errors
  • uv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/ -v5272 passed, 123 skipped, 0 failed
  • ruff check --select PLC0415 --ignore-noqa . — 256 (matches the stated final count)

Test plan

  • All of the above ran clean on this branch
  • CI green

🤖 Generated with Claude Code

https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch

…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
@linear-code

linear-code Bot commented Sep 3, 2026

Copy link
Copy Markdown

INT-1305

AlexanderZ-Band and others added 4 commits September 4, 2026 01:17
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
@AlexanderZ-Band
AlexanderZ-Band requested a review from a team September 4, 2026 04:16
AlexanderZ-Band and others added 5 commits September 4, 2026 07:33
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
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.

1 participant