Skip to content

fix(sdk): @basilica.distributed decorator captures module-level imports (fixes #477)#478

Merged
epappas merged 1 commit into
mainfrom
fix/477-decorator-module-imports
May 17, 2026
Merged

fix(sdk): @basilica.distributed decorator captures module-level imports (fixes #477)#478
epappas merged 1 commit into
mainfrom
fix/477-decorator-module-imports

Conversation

@epappas
Copy link
Copy Markdown
Contributor

@epappas epappas commented May 17, 2026

Summary

Closes #477. Fixes the canonical NameError: name 'os' is not defined failure mode in examples/20_distributed_diloco.py documented at one-covenant/basilica-backend#419 Stage 4 take-3 Cell B.

Bug

The decorator-based deploy path ships only the wrapped function body via inspect.getsource(fn):

  • DistributedFunction._extract_source in crates/basilica-sdk-python/python/basilica/decorators.py:313-337 (pre-fix)
  • DeployedFunction._extract_source in crates/basilica-sdk-python/python/basilica/decorators.py:92-115 (pre-fix)
  • SourcePackager.from_function in crates/basilica-sdk-python/python/basilica/source.py:336-389 (pre-fix)

Module-level import statements in the user's source file are not in scope inside the body when the body is exec'd on the worker pod. Any reference to a module-level name raises NameError. The failure was visible end-to-end on every worker rank of ex20:

Traceback (most recent call last):
  File "/tmp/__basilica_source.py", line 10, in <module>
    local_rank = int(os.environ.get("LOCAL_RANK", 0))
                     ^^
NameError: name 'os' is not defined

Fix

New helper _extract_module_level_imports(func) walks the AST of sys.modules[func.__module__] and emits every top-level ast.Import / ast.ImportFrom node via ast.unparse. Only import statements are captured; other top-level state (function defs, dataclasses, constants) is NOT leaked into the shipped source.

Applied at all three extraction sites. Each produces source whose first lines are now:

import os
import time
import basilica
from basilica import ProviderFilter, WorldSize
def train() -> None:
    ...

Tests

New tests/test_decorator_source_extraction.py pins the import-capture contract with 6 cases:

  • @basilica.distributed captures import os / import time from module scope
  • @basilica.distributed captures from typing import Optional (from-imports)
  • @basilica.distributed with a body that uses no module-level names still produces a valid call
  • @basilica.distributed body can be exec'd in a clean namespace with __name__ == "__main__" without raising NameError (the canonical pre-fix failure mode)
  • @basilica.deployment captures import os from module scope (same code path)
  • @basilica.deployment body can be exec'd in a clean namespace

Pre-fix:

  • 3 head-content tests FAIL (empty head: no imports captured)
  • 1 exec test FAILS with NameError: name 'os' is not defined — exactly the worker-pod failure mode from one-covenant/basilica-backend#419 Cell B

Post-fix:

  • All 6 new tests PASS
  • Full SDK test suite: 138 passed, 0 failed (132 existing + 6 new; zero regressions)

Test plan

  • Failing tests pre-fix (4 failures including the canonical NameError)
  • Tests pass post-fix (all 138)
  • Existing examples still py_compile cleanly
  • import basilica still works post-fix
  • Version bumped to 0.29.2 (patch-level; bug fix)
  • CHANGELOG entry added
  • Runtime verification on a real cluster (ex20 worker pod produces /tmp/__basilica_source.py that includes import os, and the NameError is gone). Will be exercised against api.basilica.ai after CI green and merge.

Cross-repo trace

Summary by CodeRabbit

  • Bug Fixes
    • Fixed NameError exceptions in distributed and deployment decorators by ensuring module-level import statements are properly captured and shipped to worker environments.

Review Change Stack

…evel imports

Closes #477. The decorators previously shipped only the function body
via inspect.getsource(fn); module-level imports were not in scope on
the worker pod, so any reference to a module-level name (e.g. the
`import os` in examples/20_distributed_diloco.py) raised
`NameError: name 'os' is not defined` at runtime.

Fix walks the defining module's AST and prepends every top-level
`Import` / `ImportFrom` node to the extracted function source. Only
import statements are captured; other top-level state is not leaked
into the shipped source. Applied to:
- DistributedFunction._extract_source (decorators.py)
- DeployedFunction._extract_source (decorators.py)
- SourcePackager.from_function (source.py)

Adds tests/test_decorator_source_extraction.py with 6 cases pinning
the import-capture contract: simple imports, from-imports, no-imports
no-op, and a NameError exec-check that mirrors the worker-pod
failure mode from one-covenant/basilica-backend#419 Stage 4 take-3
Cell B.

Bumps version to 0.29.2.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Walkthrough

This PR fixes a bug where @basilica.distributed and @basilica.deployment decorators, along with SourcePackager.from_function(), fail to provide module-level imports to worker code, causing NameError for functions referencing module-scope names. The fix extracts import statements via AST, prepends them to packaged source, and validates with comprehensive test coverage.

Changes

Module-level import capture for distributed and packaging paths

Layer / File(s) Summary
AST-based module import extraction helpers
crates/basilica-sdk-python/python/basilica/decorators.py, crates/basilica-sdk-python/python/basilica/source.py
Added _extract_module_level_imports(func) in both decorators.py and source.py. Uses ast and inspect to parse the function's defining module and collect only top-level import and from ... import statements, with defensive fallback to empty string on parse/lookup failures.
Decorator source extraction integration
crates/basilica-sdk-python/python/basilica/decorators.py
Updated DeployedFunction._extract_source() and DistributedFunction._extract_source() to prepend extracted module-level imports before the function definition and entrypoint invocation, ensuring module-scope names resolve on the worker without shipping full module state.
SourcePackager integration
crates/basilica-sdk-python/python/basilica/source.py
Modified SourcePackager.from_function() to prepend extracted module-level imports to the dedented function source before optional invocation, applying the same import-capture pattern to the lower-level packaging API.
Distributed and deployment decorator source extraction tests
crates/basilica-sdk-python/tests/test_decorator_source_extraction.py
Added test module with decorated fixture functions referencing module-level names (os, time, Optional) and test classes validating that extracted source includes required imports and executes without NameError for both @basilica.distributed and @basilica.deployment paths.
Version and changelog updates
crates/basilica-sdk-python/Cargo.toml, crates/basilica-sdk-python/pyproject.toml, crates/basilica-sdk-python/CHANGELOG.md
Bumped version from 0.29.1 to 0.29.2 and documented the fix in CHANGELOG.md describing the module-level import capture for both decorator and SourcePackager paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • one-covenant/basilica#477: Addresses the NameError bug when decorated functions reference module-level imports by implementing AST-based extraction and prepending of import statements in both decorator and SourcePackager paths.

Poem

🐰 The humble import, once lost in the void,
Now travels with functions to workers deployed,
No more NameError's bitter sting,
Just module-level names on the wing!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% 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(sdk): @basilica.distributed decorator captures module-level imports (fixes #477)' accurately describes the main change—capturing module-level imports in decorators to fix NameError issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/477-decorator-module-imports

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.

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (3)
crates/basilica-sdk-python/python/basilica/decorators.py (1)

22-65: ⚡ Quick win

Consider extracting this helper to a shared module.

_extract_module_level_imports is duplicated nearly verbatim in source.py (lines 36-70). Consider moving it to a shared internal utility module (e.g., _utils.py or _source_utils.py) to avoid divergence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-sdk-python/python/basilica/decorators.py` around lines 22 -
65, The function _extract_module_level_imports is duplicated between
decorators.py and source.py; extract it into a shared internal module (e.g.,
create _source_utils.py or _utils.py) and export a single function (keep the
name _extract_module_level_imports for minimal churn), then import and use that
shared function from both decorators.py and source.py (replace the local
implementation in each file with "from ._source_utils import
_extract_module_level_imports"); ensure tests/imports still resolve and preserve
the current behavior and docstring.
crates/basilica-sdk-python/tests/test_decorator_source_extraction.py (2)

81-93: 💤 Low value

Consider a more robust approach for extracting the import header.

The pattern source.split("def ")[0] is repeated across multiple test methods and could be fragile if the string "def " appears in comments, docstrings, or import statements in the extracted source. While this works for the current implementation, a more robust approach might use a helper method or regex to locate the first function definition.

♻️ Example of a more robust approach
def _extract_import_header(source: str) -> str:
    """Extract everything before the first function definition."""
    import re
    match = re.search(r'^def\s+\w+', source, re.MULTILINE)
    return source[:match.start()] if match else source

Then use: head = _extract_import_header(source)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-sdk-python/tests/test_decorator_source_extraction.py` around
lines 81 - 93, The tests repeatedly use fragile head = source.split("def ")[0]
to capture the module import header; replace this with a small helper (e.g.,
_extract_import_header) used by test functions that takes the extracted source
from _train_with_aliased_import._extract_source() and returns everything before
the first actual function definition by searching for the first multiline match
of '^def\\s+\\w+' (or returning whole source if no match). Update each test in
test_decorator_source_extraction.py to call this helper instead of using split
to make import-header extraction robust to "def " appearing in
comments/docstrings.

147-168: 💤 Low value

Consider simplifying the line trimming logic.

The loop at lines 157-163 that removes the trailing function call is complex and could be more readable. The current approach iteratively pops lines with multiple conditions, which makes the intent less clear.

♻️ Simpler alternative
# Find the last line of the function definition (excluding trailing call)
lines = source.splitlines()
# Remove the decorator-appended call (last non-empty line)
func_name = _serve_with_module_imports.__name__
while lines and (lines[-1].strip() == "" or lines[-1].strip() == f"{func_name}()"):
    lines.pop()
trimmed = "\n".join(lines) + "\n"

Or even simpler using rsplit:

# Split on the trailing call and take everything before it
func_name = _serve_with_module_imports.__name__
trimmed = source.rsplit(f"\n{func_name}()", 1)[0] + "\n"

Note: The exec() call on line 167 is flagged by static analysis (S102) but is a false positive—this is legitimate test code validating source execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-sdk-python/tests/test_decorator_source_extraction.py` around
lines 147 - 168, The trimming loop that removes the decorator-appended call is
more complex than needed; simplify it by removing trailing blank lines and any
trailing line equal to f"{_serve_with_module_imports.__name__}()" in one clear
step before joining into trimmed. Locate the test function
test_extracted_source_executes_without_name_error and replace the while/pop
logic that manipulates lines with a concise loop or rsplit approach that strips
empty lines and the exact trailing call, keeping the rest of the source intact
so compile(trimmed, "<test-source>", "exec") and exec(compiled, ns) behave the
same.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/basilica-sdk-python/CHANGELOG.md`:
- Around line 10-23: Update the changelog footer links to include a link target
for [0.29.2] and correct the comparison target for [Unreleased]; replace the
current reference to v0.14.0 with the appropriate v0.29.2 comparison (and add a
new `[0.29.2]` link target) so the `[Unreleased]` vs release links and the
`[0.29.2]` entry resolve correctly instead of pointing to v0.14.0.

---

Nitpick comments:
In `@crates/basilica-sdk-python/python/basilica/decorators.py`:
- Around line 22-65: The function _extract_module_level_imports is duplicated
between decorators.py and source.py; extract it into a shared internal module
(e.g., create _source_utils.py or _utils.py) and export a single function (keep
the name _extract_module_level_imports for minimal churn), then import and use
that shared function from both decorators.py and source.py (replace the local
implementation in each file with "from ._source_utils import
_extract_module_level_imports"); ensure tests/imports still resolve and preserve
the current behavior and docstring.

In `@crates/basilica-sdk-python/tests/test_decorator_source_extraction.py`:
- Around line 81-93: The tests repeatedly use fragile head = source.split("def
")[0] to capture the module import header; replace this with a small helper
(e.g., _extract_import_header) used by test functions that takes the extracted
source from _train_with_aliased_import._extract_source() and returns everything
before the first actual function definition by searching for the first multiline
match of '^def\\s+\\w+' (or returning whole source if no match). Update each
test in test_decorator_source_extraction.py to call this helper instead of using
split to make import-header extraction robust to "def " appearing in
comments/docstrings.
- Around line 147-168: The trimming loop that removes the decorator-appended
call is more complex than needed; simplify it by removing trailing blank lines
and any trailing line equal to f"{_serve_with_module_imports.__name__}()" in one
clear step before joining into trimmed. Locate the test function
test_extracted_source_executes_without_name_error and replace the while/pop
logic that manipulates lines with a concise loop or rsplit approach that strips
empty lines and the exact trailing call, keeping the rest of the source intact
so compile(trimmed, "<test-source>", "exec") and exec(compiled, ns) behave the
same.
🪄 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: 5a1f5540-212f-458b-8ee5-a4fac96c0bd3

📥 Commits

Reviewing files that changed from the base of the PR and between caac5ee and f16b478.

📒 Files selected for processing (6)
  • crates/basilica-sdk-python/CHANGELOG.md
  • crates/basilica-sdk-python/Cargo.toml
  • crates/basilica-sdk-python/pyproject.toml
  • crates/basilica-sdk-python/python/basilica/decorators.py
  • crates/basilica-sdk-python/python/basilica/source.py
  • crates/basilica-sdk-python/tests/test_decorator_source_extraction.py

Comment on lines +10 to +23
## [0.29.2] - 2026-05-16

### Fixed
- `@basilica.distributed` and `@basilica.deployment` now capture the
defining module's top-level `import` and `from ... import ...`
statements and prepend them to the source shipped to the worker pod.
Before this fix, only the function body was shipped; module-level
names referenced inside the body (e.g. the `import os` in
`examples/20_distributed_diloco.py`) raised `NameError` at worker
runtime. Closes #477. Cross-repo reference:
`one-covenant/basilica-backend#419` Stage 4 take-3 Cell B. The same
capture is applied in `SourcePackager.from_function()` for the
lower-level packaging path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update changelog reference links for the new release.

After adding 0.29.2, the footer links were not updated: Line 320 still compares [Unreleased] from v0.14.0, and there is no [0.29.2] link target. Please add/update those references so changelog navigation is correct.

📝 Suggested patch
-[Unreleased]: https://github.com/one-covenant/basilica/compare/basilica-sdk-python-v0.14.0...HEAD
+[Unreleased]: https://github.com/one-covenant/basilica/compare/basilica-sdk-python-v0.29.2...HEAD
+[0.29.2]: https://github.com/one-covenant/basilica/compare/basilica-sdk-python-v0.29.1...basilica-sdk-python-v0.29.2
 [0.14.0]: https://github.com/one-covenant/basilica/compare/basilica-sdk-python-v0.13.0...basilica-sdk-python-v0.14.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-sdk-python/CHANGELOG.md` around lines 10 - 23, Update the
changelog footer links to include a link target for [0.29.2] and correct the
comparison target for [Unreleased]; replace the current reference to v0.14.0
with the appropriate v0.29.2 comparison (and add a new `[0.29.2]` link target)
so the `[Unreleased]` vs release links and the `[0.29.2]` entry resolve
correctly instead of pointing to v0.14.0.

@epappas epappas merged commit b9ece24 into main May 17, 2026
14 checks passed
epappas added a commit that referenced this pull request May 17, 2026
…sage-filter

fix(sdk): filter captured module imports to those referenced by body (follow-up to #478)
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.

bug(sdk): @basilica.distributed decorator doesn't capture module-level imports — wrapped function fails with NameError

1 participant