Skip to content

fix(api): make api_server importable without gradio installed - #1276

Open
tsondo wants to merge 3 commits into
ace-step:mainfrom
tsondo:pr/headless-api-server
Open

fix(api): make api_server importable without gradio installed#1276
tsondo wants to merge 3 commits into
ace-step:mainfrom
tsondo:pr/headless-api-server

Conversation

@tsondo

@tsondo tsondo commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

acestep.api_server only needs one helper from the UI layer (_build_generation_info), but importing it via acestep.ui.gradio.events.results_handlers drags in the entire gradio UI package at import time — so the API server cannot start in environments where gradio is not installed (headless / API-only deployments that consume ACE-Step purely through REST).

Three changes keep the API path headless while leaving Gradio UI behavior unchanged when gradio is installed:

  • acestep/ui/gradio/__init__.py — expose create_gradio_interface lazily (PEP 562 __getattr__) instead of importing the interfaces package eagerly
  • acestep/ui/gradio/events/__init__.py — move the .wiring import (whose handler modules import gradio at module level) inside the two setup functions, which only run under the Gradio UI
  • acestep/api_server.py — import _build_generation_info from its defining module (events/results/generation_info.py, already headless-safe with its own lazy gradio fallback) instead of the results_handlers facade

Testing

  • import acestep.api_server succeeds with gradio imports blocked (simulating an environment without gradio)
  • from acestep.ui.gradio import create_gradio_interface and from acestep.ui.gradio.events import setup_event_handlers, setup_training_event_handlers still work with gradio installed
  • results_handlers_facade_test.py and i18n_thread_safety_test.py pass

Related: #1275 declares python-multipart directly — headless installs need it since it currently only arrives via gradio.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved headless and API usage when Gradio is not installed.
    • Prevented unnecessary UI-related loading during standard application imports.
    • Preserved existing generation result handling and event registration behavior.

The API server only needs _build_generation_info, but importing it via
acestep.ui.gradio.events.results_handlers dragged in the entire gradio UI
package at import time. Three changes keep the API path headless:

- ui/gradio/__init__.py: expose create_gradio_interface lazily (PEP 562)
- ui/gradio/events/__init__.py: move the .wiring import (gradio at module
  level) inside the two setup functions, which only run under the Gradio UI
- api_server.py: import _build_generation_info from its defining module
  (results/generation_info.py, already headless-safe) instead of the facade

Gradio UI behavior is unchanged when gradio is installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe9d7e29-6bf4-4afb-a40e-478b6f564177

📥 Commits

Reviewing files that changed from the base of the PR and between 44dce97 and 4fe3773.

📒 Files selected for processing (2)
  • acestep/ui/gradio/__init__.py
  • acestep/ui/gradio/events/__init__.py
 ___________________________
< I refactor in bunny hops. >
 ---------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
📝 Walkthrough

Walkthrough

Gradio-dependent imports are deferred across the API, Gradio package, and event package. Generation metadata uses its defining module, the interface factory loads lazily, and event wiring imports occur inside setup functions.

Changes

Headless import behavior

Layer / File(s) Summary
Lazy Gradio exports and API imports
acestep/api_server.py, acestep/ui/gradio/__init__.py
The API server imports generation metadata directly, and create_gradio_interface is exposed through lazy attribute loading.
Deferred event wiring imports
acestep/ui/gradio/events/__init__.py
Event and training setup functions import their wiring contexts and registration functions locally, while documenting the headless import behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

A bunny found imports tucked out of sight,
So headless paths stay clean and light.
The UI wakes when called by name,
Event wires wait before the game.
Hop, hop—Gradio loads just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making api_server importable without Gradio installed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 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 `@acestep/ui/gradio/__init__.py`:
- Around line 9-14: Declare create_gradio_interface in the module’s __all__ so
star imports include the lazily resolved facade export, while retaining the
existing __getattr__ implementation. Add a concise comment documenting that
__getattr__ is required to lazily expose new Python functions.

In `@acestep/ui/gradio/events/__init__.py`:
- Around line 101-107: Update the docstring for setup_training_event_handlers to
add Args entries for all four inputs and a Returns section documenting that
handlers are registered in place, while retaining its existing purpose
description.
🪄 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 Plus

Run ID: 20341226-5a3d-4021-8df1-358d9b637147

📥 Commits

Reviewing files that changed from the base of the PR and between 6d467e4 and 44dce97.

📒 Files selected for processing (3)
  • acestep/api_server.py
  • acestep/ui/gradio/__init__.py
  • acestep/ui/gradio/events/__init__.py

Comment on lines +9 to +14
def __getattr__(name):
if name == "create_gradio_interface":
from acestep.ui.gradio.interfaces import create_gradio_interface

return create_gradio_interface
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -U 'from\s+acestep\.ui\.gradio\s+import\s+\*' --glob '*.py' .

Repository: ace-step/ACE-Step-1.5

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)acestep/ui/gradio/__init__\.py$|gradio/'

echo
echo "== target file =="
if [ -f acestep/ui/gradio/__init__.py ]; then
  nl -ba acestep/ui/gradio/__init__.py
fi

echo
echo "== imports/usages of acestep.ui.gradio =="
rg -n 'acestep\.ui\.gradio|create_gradio_interface|from acestep.ui.gradio import|import acestep.ui.gradio' --glob '*.py' .

Repository: ace-step/ACE-Step-1.5

Length of output: 8511


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file contents =="
awk '{printf "%6d\t%s\n", NR, $0}' acestep/ui/gradio/__init__.py

echo
echo "== uses of acestep.ui.gradio interface exports =="
rg -n 'acestep\.ui\.gradio|create_gradio_interface' --glob '*.py' .

echo
echo "== import forms for acestep.ui.gradio =="
rg -nU 'from\s+acestep\.ui\.gradio\s+(?:import\s+\*|import)|import\s+acestep\.ui\.gradio|from\s+acestep\.ui\s+import\s+gradio|import\s+acestep\.ui\.gradio' --glob '*.py' .

echo
echo "== any __getattr__ in acestep ui gradio =="
rg -n '__getattr__|__all__' --glob 'acestep/ui/gradio/**/*.py' --glob 'acestep/ui/gradio/__init__.py' .

Repository: ace-step/ACE-Step-1.5

Length of output: 30439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Python __getattr__ star-import behavior probe =="
python3 - <<'PY'
import ast
import sys
import tempfile
import os
from importlib.machinery import ModuleSpec
from importlib.abc import Loader, MetaPathFinder
from types import ModuleType

pkg = ModuleType("testpkg")
pkg.__file__ = "<testpkg-file>"
subpkg = ModuleType("testpkg.submod")
subpkg.__file__ = "<testpkg-submod-file>"
subpkg.__path__ = []
subpkg.__getattr__ = globals()["__getattr__"] = lambda name: "VALUE" if name == "x" else None
sys.modules["testpkg"] = pkg
sys.modules["testpkg.submod"] = subpkg

ns = {}
exec("from testpkg.submod import *", ns)
print("__all__", ns.get("__all__"))
print("x", ns.get("x"))
print("hasattr", hasattr(subpkg, "x"))
print("getattr", getattr(subpkg, "x", "<missing>",))

code = ast.parse("from testpkg.submod import *")
print("Star import nodes:", ast.dump(code.body))
PY

Repository: ace-step/ACE-Step-1.5

Length of output: 511


🌐 Web query:

Python PEP 562 module __getattr__ from import * __all__ getattr list returned behavior

💡 Result:

PEP 562 introduced module-level getattr and dir to Python (starting in version 3.7), allowing for dynamic attribute resolution and customized introspection [1][2]. The interaction between module getattr, all, and from module import * is governed by the following rules: 1. Import * Behavior and all: The statement from module import * is controlled by the module's all attribute [3][4]. If all is defined, only the names listed in it are imported [3][4]. If all is not defined, Python imports all names in the module that do not begin with an underscore [4]. all does not implicitly call getattr for names not explicitly listed [5]. 2. Role of getattr: Module-level getattr is called only when an attribute lookup fails through the standard mechanisms (i.e., it is not found in the module's dict) [1][6][5]. It is not automatically invoked by the import * mechanism to discover or generate names [5]. 3. Interaction with from module import *: When you use from module import *, the interpreter looks at all [3]. If a name listed in all is missing from the module's namespace, the import machinery will attempt to resolve it. If the module has defined getattr, this may trigger a call to it as the system searches for the attribute [7]. 4. Important Technical Note: Developers often use getattr combined with all to implement lazy loading of submodules [1][8]. In such patterns, all lists the submodule names, and getattr performs the import only when the attribute is first accessed [1]. Because getattr is called by the attribute access mechanism, it is called only when a name is actually used, which can differ from the eager loading behavior typically associated with from module import * [1][9]. In summary, getattr does not change the fundamental behavior of all or how import * filters names [5]. To include dynamically generated names in an import * operation, you must ensure those names are present in the all list [5]. If you want to customize the names shown by dir, you should implement module-level dir [1][6].

Citations:


Preserve star-import compatibility for the facade export.

Without __all__, from acestep.ui.gradio import * currently imports names defined directly in the module namespace and ignores lazy names resolved through __getattr__, so create_gradio_interface is not included. Declare it in __all__ if this facade is meant to support star imports, and document the lazy __getattr__ hook as required for new Python functions.

🤖 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 `@acestep/ui/gradio/__init__.py` around lines 9 - 14, Declare
create_gradio_interface in the module’s __all__ so star imports include the
lazily resolved facade export, while retaining the existing __getattr__
implementation. Add a concise comment documenting that __getattr__ is required
to lazily expose new Python functions.

Source: Coding guidelines

Comment thread acestep/ui/gradio/events/__init__.py
@ChuxiJ

ChuxiJ commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

This is the right direction for keeping acestep.api_server importable in headless installs. Before merge, could you please address the two small review items CodeRabbit left?

  • expose create_gradio_interface through __all__ while keeping the lazy __getattr__ facade;
  • expand setup_training_event_handlers docs so its inputs/side-effect return behavior are documented.

Once those are in, I can do a focused final pass and merge it.

claude added 2 commits August 17, 2026 15:28
- Declare __all__ (plus a matching __dir__) on acestep.ui.gradio so the
  lazily exposed create_gradio_interface stays visible to star imports
  and dir() under PEP 562.
- Expand the setup_training_event_handlers docstring with Args/Returns,
  mirroring setup_event_handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGYzKnqg6ti5DENrxcfwnp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGYzKnqg6ti5DENrxcfwnp
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.

3 participants