fix(workspace-changes): keep the package importable as a first import (#6602) (#6608)

`workspace_changes/api.py` imports `deerflow.runtime.user_context` for the
AUTO sentinel (#6282). Importing that submodule runs the runtime package init,
which imports `.runs` -> `worker` -> `deerflow.workspace_changes` back for
`capture_workspace_snapshot` and friends, so importing `.api` as the package's
first statement failed with ImportError while those names were still unbound.

Resolve the re-export lazily via a module-level `__getattr__`, the pattern
`deerflow/subagents/__init__.py` already uses.
This commit is contained in:
Jason Cat 2026-10-10 21:27:35 +08:00 committed by GitHub
parent 35f6a98cd9
commit 65a186187d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 1 deletions

View File

@ -1,4 +1,3 @@
from .api import get_workspace_changes_response
from .diff import compare_snapshots, get_changed_output_paths, get_changed_paths
from .recorder import capture_workspace_snapshot, record_workspace_changes
from .scanner import scan_workspace_roots
@ -32,3 +31,17 @@ __all__ = [
"record_workspace_changes",
"scan_workspace_roots",
]
def __getattr__(name: str):
# `.api` reads `deerflow.runtime.user_context` for the AUTO sentinel, and
# importing that package runs `deerflow.runtime`'s init, which imports
# `.runs` -> `worker` -> this package. Resolving it lazily keeps this
# package importable as a process's first import instead of failing with
# ImportError while its own names are still unbound.
if name == "get_workspace_changes_response":
from .api import get_workspace_changes_response
globals()[name] = get_workspace_changes_response
return get_workspace_changes_response
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@ -72,3 +72,27 @@ def test_subagent_package_public_executor_exports_are_lazy_importable() -> None:
)
assert result.returncode == 0, result.stderr
assert "SubagentExecutor SubagentResult" in result.stdout
def test_workspace_changes_package_imports_standalone() -> None:
"""The workspace-changes package must import as the process's first import.
``workspace_changes.api`` reads ``deerflow.runtime.user_context`` for the
``AUTO`` sentinel, which runs ``deerflow.runtime``'s package init and imports
``.runs`` -> ``worker`` -> ``deerflow.workspace_changes`` again. Importing
``.api`` before this package bound its own names made that re-entrant import
fail with ImportError whenever ``deerflow.runtime`` had not been imported
yet.
"""
result = subprocess.run(
[
sys.executable,
"-c",
"import deerflow.workspace_changes as w; print(w.scan_workspace_roots.__name__, w.get_workspace_changes_response.__name__)",
],
capture_output=True,
text=True,
env=_gateway_import_env(),
)
assert result.returncode == 0, result.stderr
assert "scan_workspace_roots get_workspace_changes_response" in result.stdout