원클릭으로
circular-import-analysis
// Run circular import detection against ddtrace and propose architectural fixes for any cycles found. Use this when adding or refactoring modules, or when the detect_circular_imports CI job reports new cycles on a PR.
// Run circular import detection against ddtrace and propose architectural fixes for any cycles found. Use this when adding or refactoring modules, or when the detect_circular_imports CI job reports new cycles on a PR.
Run targeted linting, formatting, and code quality checks on modified files. Use this to validate code style, type safety, security, and other quality metrics before committing. Supports running all checks or targeting specific checks on specific files for efficient validation.
Validate code changes by intelligently selecting and running the appropriate test suites. Use this when editing code to verify changes work correctly, run tests, validate functionality, or check for regressions. Automatically discovers affected test suites, selects the minimal set of venvs needed for validation, and handles test execution with Docker services as needed.
Diagnose and fix slow base venv build times caused by unnecessary recompilation of native extensions (CMake, Cython, Rust) across riot generate runs. Use when CI base venv builds are slow, when ext_cache isn't saving time, or when investigating warm build regressions.
Run performance benchmarks to measure the impact of code changes. Discovers relevant benchmark scenarios based on changed files, executes them comparing a baseline version against local changes, and summarizes performance results. Use this when touching performance-sensitive code paths or when asked about performance impact.
Review CI results for the current branch, commit, or PR using the Datadog MCP. Use this when CI is failing, to understand what's blocking a PR, or to get actionable fix instructions for failed jobs and tests.
Compare CPython source code between two Python versions to identify changes in headers and structs. Use this when adding support for a new Python version to understand what changed between versions.
| name | circular-import-analysis |
| description | Run circular import detection against ddtrace and propose architectural fixes for any cycles found. Use this when adding or refactoring modules, or when the detect_circular_imports CI job reports new cycles on a PR. |
| allowed-tools | ["Bash","Read","Grep","Glob","Edit","TodoWrite"] |
This skill runs the circular import detector locally and proposes sound architectural fixes for any cycles found. The guiding principle is Separation of Concerns: fixes must restructure ownership of code, not paper over the problem with deferred imports.
detect_circular_imports CI job reports new cycles on your PR.uv run --script scripts/import-analysis/cycles.py analyze cycles.json
This writes all detected cycles to cycles.json and prints a count to stdout. Requires
uv on PATH (brew install uv or pip install uv).
To read the results:
cat cycles.json # raw JSON array of cycles
Each entry is an array of module names forming the cycle, e.g.:
["ddtrace", "ddtrace.internal.datastreams", "ddtrace"]
Clean up afterwards:
rm cycles.json
Never use deferred imports (
import xinside a function body) as a fix. They hide the structural problem, complicate testing and static analysis, and impose a runtime cost on every call.
Before proposing a fix, trace why each module needs the other:
# Who imports whom?
grep -rn "^import ddtrace\|^from ddtrace" ddtrace/internal/datastreams/ --include="*.py"
grep -rn "^import ddtrace.internal.datastreams\|^from ddtrace.internal.datastreams" ddtrace/ --include="*.py"
Identify the exact names (classes, functions, constants) that cross the boundary in each direction. Often only a small fraction of each module is actually involved.
When to use: Two modules share a type (e.g. a dataclass, a Protocol, a constant) that both need to import, but neither should own.
Before:
ddtrace.foo → ddtrace.bar → ddtrace.foo (cycle)
After:
ddtrace.foo → ddtrace._types (no cycle)
ddtrace.bar → ddtrace._types
Create a thin _types.py (or interfaces.py) module that contains only the shared
contract. Both sides import from it; neither imports from the other. In dd-trace-py,
ddtrace/_trace/types.py and ddtrace/internal/schema.py are examples of this pattern.
When to use: Module A calls into module B, but B also needs to notify A of events.
Before:
ddtrace.core → ddtrace.contrib.foo → ddtrace.core
After:
ddtrace.core → ddtrace._interfaces.IFooHook (abstract)
ddtrace.contrib.foo → ddtrace._interfaces.IFooHook (implements)
Define a Protocol or abstract base in a third module. ddtrace.core depends on the
protocol, not the implementation. The contrib module registers itself at startup
(see the existing ddtrace/internal/hooks.py registration pattern).
When to use: A lower-level module (e.g. ddtrace.internal.X) imports a higher-level
module only to register or configure itself at import time.
Before:
ddtrace → ddtrace.internal.X → ddtrace (X registers itself during import)
After: Remove the registration from ddtrace.internal.X's module scope. Instead,
have ddtrace/__init__.py (the higher-level module) call X.register() explicitly after
importing X. The lower-level module exposes a registration API but does not call it
itself.
This is the standard dd-trace-py pattern: integrations do not self-activate; ddtrace
drives the lifecycle.
When to use: A large module contains both high-level logic (which imports from elsewhere) and low-level primitives (which are imported by elsewhere). The primitives do not actually need the high-level logic.
Before:
ddtrace.trace → ddtrace._trace.tracer → ddtrace.trace
After:
ddtrace.trace._api (pure public types / constants — no upward imports)
ddtrace._trace.tracer → ddtrace.trace._api
ddtrace.trace → ddtrace.trace._api
→ ddtrace._trace.tracer
Check whether the parts of the module that are imported by the lower-level module can be
split into a _api.py, _types.py, or _base.py sub-module with no reverse
dependencies.
When to use: The cycle exists because a piece of logic ended up in the wrong module. This is the simplest and often best fix.
Ask: "Does this function/class conceptually belong in module A or module B?" If it belongs in A, move it there so that B (which uses it) imports from A — not the other way round. The cycle disappears because there is now a clear dependency direction.
ddtrace -> ddtrace.internal.datastreams -> ddtrace
ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.datastreams -> ddtrace.trace
ddtrace -> ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.datastreams -> ddtrace
All three cycles pass through ddtrace.internal.datastreams importing something from the
top-level ddtrace or ddtrace.trace packages. The correct investigation path:
ddtrace.internal.datastreams imports from ddtrace / ddtrace.trace:
grep -rn "^from ddtrace\b\|^import ddtrace\b" ddtrace/internal/datastreams/ --include="*.py"
ddtrace/internal/ (→ Pattern 5),
or whether they represent a shared contract (→ Pattern 1 or 2).if TYPE_CHECKING guards or function-level
imports as a substitute._types.py
if the type already conceptually belongs somewhere.uv run --script scripts/import-analysis/cycles.py analyze cycles.json after the change and confirming the cycle is gone.