feat(lfx): Add type stubs for IDE autocomplete#10810
Conversation
Add runtime methods to Component class for better developer experience: - __dir__() to expose inputs and output methods for autocomplete - _update_set_signature() to show set() parameters in IDE hints - describe() for REPL exploration of component structure
Add stub generator that creates .pyi files for all lfx components: - Generates set() method signatures with input parameters - Extracts return types from method annotations - Handles optional dependencies gracefully - Script at scripts/generate_lfx_stubs.py with --inline flag for releases
Add PEP 561 py.typed marker and update hatch build config to include .pyi files in the wheel distribution.
Add step to generate type stubs before wheel build, and verify the wheel contains at least 100 stub files.
Stubs are generated at build time and should not be committed.
WalkthroughThis PR introduces type stub (.pyi) file generation for the lfx package to improve IDE autocomplete support. It adds a stub generator that discovers Component subclasses, generates type-annotated stubs for their inputs/outputs, integrates stub generation into the build pipeline, and includes stubs in wheel distributions. Component classes gain IDE-friendly features including dynamic signature updates and introspection methods. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (6 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (10)
scripts/generate_lfx_stubs.py (1)
28-73: CLI behavior is sound; consider failing fast when no stubs are generated in--inlinemodeThe path setup and mode handling (
--inline, explicit dir, or defaulttypings/) look good and integrate cleanly with the release workflow.For better feedback, especially in CI, you might want to treat “zero generated stubs” as an error when running with
--inline(and optionally when an explicit output dir is provided), e.g.:- stubs = generate_stubs(output_dir) - - console.print(f"[green]Generated {len(stubs)} stub files[/green]") + stubs = generate_stubs(output_dir) + + if inline_mode and not stubs: + console.print("[red]No stubs were generated in inline mode. Failing.[/red]") + raise SystemExit(1) + + console.print(f"[green]Generated {len(stubs)} stub files[/green]")That would surface misconfigurations (e.g.,
lfx.componentsimport issues) at the stub-generation step instead of only at wheel verification.src/lfx/pyproject.toml (1)
56-63: Wheel include patterns correctly capture sources, stubs, and typing markerIncluding
src/lfx/**/*.py,src/lfx/**/*.pyi, andsrc/lfx/py.typedunder[tool.hatch.build]aligns with the stub-generation flow and guarantees stubs are present in built wheels. The empty[tool.hatch.build.targets.wheel.shared-data]section is currently unused; you can drop it later if no shared-data mappings are needed, but it doesn’t cause a problem as-is.src/lfx/tests/unit/stubs/test_stub_generator.py (1)
1-62: Stub generator tests cover key DX guaranteesThese tests hit the important behaviors of
generate_stubs_for_component: presence ofset()withSelfreturn, inclusion of input names, output method stubs, basic type annotations, and default values, plus a sanity check forChatInput. The pytest style and placement undertests/unit/stubs/fit the project’s testing guidelines.If you ever want to harden this further, a lightweight addition would be to
ast.parse(stub)in one test to assert that generated stubs are syntactically valid, but that’s optional for this PR..github/workflows/release-lfx.yml (1)
167-190: Release workflow correctly enforces presence of type stubs in wheelsThe new steps to:
- run
scripts/generate_lfx_stubs.py --inlinebefore building, and- assert a minimum number of
.pyientries in the wheelprovide a solid safety net that stubs are both generated and packaged. The
unzip | grep -c '\.pyi$'check is straightforward and will fail loudly if stubs disappear.If you ever need to tune this for different component counts, consider making the
100threshold a small env/config variable, but it’s fine as a hard-coded sanity check for now.src/lfx/src/lfx/custom/custom_component/component.py (2)
183-469: Dynamicset()signature works but is effectively global across componentsThe
_update_set_signatureapproach is clever and gives nice runtime signatures forset():
- Parameters are keyword-only, deduped by input name.
- Defaults come from
inp.valuewhen present.- Annotations are derived from
input_typeswhen available.There are a couple of design caveats worth being aware of:
Signature is shared across all Component subclasses
self.set.__func__refers to the underlying function object defined onComponent, so assigning__signature__there makes the last component instance to call_update_set_signature()determine the signature for all components. In practice your DX tests work because each test re-instantiates the component type it inspects, but in a long-lived process,inspect.signature(SomeOtherComponent().set)can reflect whatever component was initialized most recently.If you want per-class signatures, you’d likely need to move this logic to
__init_subclass__(or a metaclass) and attach a distinctsetwrapper per subclass before instances are created.Signature isn’t updated when new inputs are added at runtime
Methods like
_get_or_create_inputcan append newInputobjects toself.inputsand_inputsafter initialization (e.g., viaset(extra_param=...)creating a fallback input). The current implementation only runs in__init__, so those dynamically-added inputs won’t show up ininspect.signature(self.set)unless_update_set_signature()is called again (e.g., at the end ofset()ormap_inputs).Neither point is a blocker for this PR—IDE autocomplete will still primarily lean on the generated stubs—but it’s worth keeping in mind if you intend runtime
inspect.signatureto be authoritative per component. If you’d like, I can sketch a__init_subclass__-based variant that produces a per-classsetwrapper with its own__signature__.
470-1030:describe()and__dir__()implementations are clear and match the intended DX
describe()produces a concise, human-readable summary of inputs (names, types, required/optional, defaults) and outputs (names, types, bound methods), which aligns well with the tests and helps REPL users.__dir__()starts fromsuper().__dir__(), then unions in dynamic input names and output method names from_inputs/_outputs_map, giving IDEs and interactive shells the right surface for autocomplete.One tiny nit:
set()’s docstring still claims it returnsNone, but the method actually returnsself. Updating that return description later would avoid confusion, though it’s not critical for this change set.src/lfx/src/lfx/stubs/generator.py (4)
68-89: Consider escaping newlines and other control characters in string defaultsRight now, string defaults only escape backslashes and double quotes. A default containing
\nor other control characters would be emitted literally, likely breaking the generated.pyisyntax:default = "line1\nline2" # becomes param: str = "line1 line2"Using
repr(value)(with minor post‑processing if needed) or explicitly escaping common control characters (\n,\r,\t) would make stub output safer without changing behavior for simple strings.
163-224:generate_stubs_for_componentis well structured; minor defaults/base‑class nitsThe overall structure (class header,
set(...)with keyword‑only inputs, then output methods with async detection) is clean and should give good IDE experiences.Two small refinements to consider:
Base class name imports
Whenbasesis empty you fall back to"Component", but the stub files never importComponent. Type checkers usually treat unknown bases asAny, but importingComponent(or whatever base is actually used at runtime) in the generated stub would make things more precise.Preserving
Nonedefaults
Parameters whose runtime default isNoneare currently emitted as= ...instead of= None(since you only call_get_default_reprwhenvalue is not None). This loses information in the stubs for optional inputs. If feasible, you might treatNoneseparately and emit= Nonefor those, while keeping...for “no default provided”.Neither of these is blocking but they would tighten the fidelity between runtime behavior and stubs.
227-255: Import collection does the job; substring checks are slightly coarse
_collect_importscorrectly ensuresAnyandSelfare always imported and adds schema imports forData,DataFrame, andMessagebased on the rendered type strings.Using substring checks (
"Data" in py_type, etc.) is a pragmatic shortcut, but note it could pick up unrelated identifiers containing those substrings. That’s usually harmless (extra imports in stubs are OK), but if you later introduce types likeMetadata, you may get an unnecessaryDataimport.If you want to tighten this, you could parse the type string for whole‑word matches or track the concrete type names before formatting, but it’s fine to keep as‑is if the current inputs/outputs are constrained.
308-406: Global stub generation is robust; consider logging and richer__init__.pyistubsThe walk over
lfx.componentswith broad exception handling makes the generator resilient to optional dependencies and lazy attributes, which is appropriate for a release‑time tool.Two non‑blocking improvements to consider:
Visibility into skipped modules/attributes
Swallowing allExceptionduring module import and attribute access is safe but opaque. Lightweight logging (even at debug level) of the module name or attribute that failed would aid debugging if certain components unexpectedly lack stubs.Package
__init__.pyire‑exports
Auto‑generated__init__.pyifiles currently only contain a comment. That meansfrom lfx.components import SomeComponentmay not get autocomplete/type info even ifSomeComponentis defined in a submodule stub. Generating basic re‑exports for classes within the same package (or mirroring the runtime package’s__all__) would improve the DX when importing from package roots.Neither is required for correctness of the current PR, but both would strengthen the long‑term utility of the stub generator.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.github/workflows/release-lfx.yml(2 hunks).gitignore(1 hunks)scripts/generate_lfx_stubs.py(1 hunks)src/lfx/pyproject.toml(1 hunks)src/lfx/src/lfx/custom/custom_component/component.py(4 hunks)src/lfx/src/lfx/stubs/__init__.py(1 hunks)src/lfx/src/lfx/stubs/generator.py(1 hunks)src/lfx/tests/unit/custom/component/test_component_dx.py(1 hunks)src/lfx/tests/unit/stubs/test_stub_generator.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/{test_*.py,*.test.ts,*.test.tsx}
📄 CodeRabbit inference engine (Custom checks)
Check that test files follow the project's naming conventions (test_*.py for backend, *.test.ts for frontend)
Files:
src/lfx/tests/unit/custom/component/test_component_dx.pysrc/lfx/tests/unit/stubs/test_stub_generator.py
**/test_*.py
📄 CodeRabbit inference engine (Custom checks)
**/test_*.py: Backend tests should follow pytest structure with proper test_*.py naming
For async functions, ensure proper async testing patterns are used with pytest for backend
Files:
src/lfx/tests/unit/custom/component/test_component_dx.pysrc/lfx/tests/unit/stubs/test_stub_generator.py
🧠 Learnings (8)
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Inherit from the correct `ComponentTestBase` family class located in `src/backend/tests/base.py` based on API access needs: `ComponentTestBase` (no API), `ComponentTestBaseWithClient` (needs API), or `ComponentTestBaseWithoutClient` (pure logic). Provide three required fixtures: `component_class`, `default_kwargs`, and `file_names_mapping`
Applied to files:
src/lfx/tests/unit/custom/component/test_component_dx.py
📚 Learning: 2025-11-24T19:46:09.104Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/backend_development.mdc:0-0
Timestamp: 2025-11-24T19:46:09.104Z
Learning: Applies to tests/unit/components/**/*.py : Create unit tests in `src/backend/tests/unit/components/` mirroring the component directory structure, using `ComponentTestBaseWithClient` or `ComponentTestBaseWithoutClient` base classes
Applied to files:
src/lfx/tests/unit/custom/component/test_component_dx.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: When adding a new component test, inherit from the correct `ComponentTestBase` class and provide the three required fixtures (`component_class`, `default_kwargs`, `file_names_mapping`) to greatly reduce boilerplate and enforce version compatibility
Applied to files:
src/lfx/tests/unit/custom/component/test_component_dx.py
📚 Learning: 2025-08-05T22:51:27.961Z
Learnt from: edwinjosechittilappilly
Repo: langflow-ai/langflow PR: 0
File: :0-0
Timestamp: 2025-08-05T22:51:27.961Z
Learning: The TestComposioComponentAuth test in src/backend/tests/unit/components/bundles/composio/test_base_composio.py demonstrates proper integration testing patterns for external API components, including real API calls with mocking for OAuth completion, comprehensive resource cleanup, and proper environment variable handling with pytest.skip() fallbacks.
Applied to files:
src/lfx/tests/unit/custom/component/test_component_dx.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Test both sync and async code paths, mock external dependencies appropriately, test error handling and edge cases, validate input/output behavior, and test component initialization and configuration
Applied to files:
src/lfx/tests/unit/stubs/test_stub_generator.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Create comprehensive unit tests for all new backend components. If unit tests are incomplete, create a corresponding Markdown file documenting manual testing steps and expected outcomes
Applied to files:
src/lfx/tests/unit/stubs/test_stub_generator.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Use `pytest.mark.api_key_required` and `pytest.mark.no_blockbuster` markers for components that need external APIs; use `MockLanguageModel` from `tests.unit.mock_language_model` for testing without external API keys
Applied to files:
src/lfx/tests/unit/stubs/test_stub_generator.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Be aware of ContextVar propagation in async tests; test both direct event loop execution and `asyncio.to_thread` scenarios; ensure proper context isolation between test cases
Applied to files:
src/lfx/src/lfx/custom/custom_component/component.py
🧬 Code graph analysis (4)
src/lfx/src/lfx/stubs/__init__.py (1)
src/lfx/src/lfx/stubs/generator.py (2)
generate_stubs(308-406)generate_stubs_for_component(163-224)
scripts/generate_lfx_stubs.py (1)
src/lfx/src/lfx/stubs/generator.py (1)
generate_stubs(308-406)
src/lfx/src/lfx/stubs/generator.py (1)
src/lfx/src/lfx/base/tools/flow_tool.py (1)
args(32-34)
src/lfx/tests/unit/stubs/test_stub_generator.py (2)
src/lfx/src/lfx/components/input_output/chat_output.py (1)
ChatOutput(22-184)src/lfx/src/lfx/stubs/generator.py (1)
generate_stubs_for_component(163-224)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Update Starter Projects
- GitHub Check: Run Ruff Check and Format
- GitHub Check: Update Component Index
🔇 Additional comments (6)
.gitignore (1)
289-291: Ignore rule for generated stubs is consistent with packaging strategyIgnoring
src/lfx/src/lfx/**/*.pyiwhile including them in the wheel viapyproject.tomlis a good balance between clean VCS and distributable typing assets. No issues here.src/lfx/src/lfx/stubs/__init__.py (1)
1-5: Public stubs API is minimal and well-scopedRe-exporting
generate_stubsandgenerate_stubs_for_componentfromlfx.stubswith an explicit__all__is a clean, discoverable surface for tooling and scripts. Looks good.src/lfx/tests/unit/custom/component/test_component_dx.py (1)
1-181: DX tests give solid coverage for__dir__,setsignature, anddescribeThis suite does a good job validating the new developer-experience features:
__dir__exposes input names, output methods, and standard helpers on both a simple component and core ChatInput/ChatOutput.inspect.signature(..set)is verified to include input names, preserve defaults, and use keyword-only parameters.describe()is checked for basic structure and meaningful content (class/display names, inputs, outputs, and types).The tests are pytest-idiomatic and live in an appropriate location under
tests/unit/custom/component/. No changes needed.src/lfx/src/lfx/stubs/generator.py (3)
14-66: Input type mapping and_get_python_type_for_inputlook solid overallThe mapping and override logic via
IGNORE_INPUT_TYPES_FORplusinput_typesintrospection cover the common cases and allow Handle‑style inputs to express richer unions. Behavior is reasonable as long asinput_typesis consistently a list of strings, which seems to be the intended contract.Please double‑check that all existing
Inputimplementations setinput_typesto strings (not classes) so unions like' | '.join(types)always yield valid annotations in stubs.
91-161: Output type resolution and_type_to_strare generally robustThe combination of preferring
output.typesand then falling back to method return annotations (with_type_to_str) is a good balance between explicit configuration and introspection. Handling ofUnion,Optional,list[T], anddict[K, V]via__origin__/__args__should cover most annotations used in components.Given
from __future__ import annotations, the earlyisinstance(type_hint, str)path is also important and looks correct.If you have components using PEP 604 unions (
T | None) without stringized annotations, it may be worth adding a small test to confirm_type_to_strproduces the expected string form for those as well.
68-89: > Likely an incorrect or invalid review comment.
| def generate_stubs_for_module(module_path: str) -> dict[str, str]: | ||
| """Generate stubs for all components in a module. | ||
|
|
||
| Args: | ||
| module_path: The import path to the module (e.g., 'lfx.components.input_output'). | ||
|
|
||
| Returns: | ||
| Dict mapping relative file paths to stub content. | ||
| """ | ||
| from lfx.custom.custom_component.component import Component | ||
|
|
||
| stubs: dict[str, str] = {} | ||
|
|
||
| try: | ||
| module = importlib.import_module(module_path) | ||
| except ImportError: | ||
| return stubs | ||
|
|
||
| # Find all Component subclasses in the module | ||
| component_classes: list[type[Component]] = [] | ||
| for name in dir(module): | ||
| obj = getattr(module, name) | ||
| if isinstance(obj, type) and issubclass(obj, Component) and obj is not Component: | ||
| component_classes.append(obj) | ||
|
|
||
| if not component_classes: | ||
| return stubs | ||
|
|
||
| # Generate imports | ||
| imports = _collect_imports(component_classes) | ||
|
|
||
| # Generate stub content | ||
| lines = list(imports) | ||
| lines.append("") | ||
| lines.append("") | ||
|
|
||
| for component_class in sorted(component_classes, key=lambda c: c.__name__): | ||
| lines.append(generate_stubs_for_component(component_class)) | ||
| lines.append("") | ||
| lines.append("") | ||
|
|
||
| # Determine output path | ||
| module_parts = module_path.split(".") | ||
| stub_path = "/".join(module_parts) + ".pyi" | ||
|
|
||
| stubs[stub_path] = "\n".join(lines) | ||
|
|
||
| return stubs |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -name "generator.py" -path "*/lfx/stubs/*" 2>/dev/null | head -5Repository: langflow-ai/langflow
Length of output: 46
🏁 Script executed:
git ls-files | grep -E "generator\.py|stubs" | head -20Repository: langflow-ai/langflow
Length of output: 396
🏁 Script executed:
find . -type f -name "generator.py" 2>/dev/nullRepository: langflow-ai/langflow
Length of output: 100
🏁 Script executed:
cat -n src/lfx/src/lfx/stubs/generator.pyRepository: langflow-ai/langflow
Length of output: 16289
Sort imports in generate_stubs_for_module for deterministic output
On line 290, imports are converted to a list without sorting:
lines = list(imports)Since _collect_imports returns a set[str] (line 227), the import order is non-deterministic. However, the generate_stubs function (line 366) sorts imports explicitly:
lines = sorted(imports)For reproducible builds and consistent stub file output, generate_stubs_for_module should do the same:
- lines = list(imports)
+ lines = sorted(imports)🤖 Prompt for AI Agents
In src/lfx/src/lfx/stubs/generator.py around lines 258 to 305, the code converts
the imports set to a list without sorting which produces non-deterministic
output; change the conversion to use a deterministic sorted order (e.g., replace
the unsorted list conversion with sorted(imports)) so the stub file imports are
written in a stable, reproducible order consistent with generate_stubs.
Summary
__dir__,describe(), dynamicset()signature) for runtime introspectionWhat this enables
Users installing
lfxvia pip will get:component.set(...)parameters-> Messageinstead of-> Any)py.typedmarker for PEP 561 complianceTest plan
cd src/lfx && uv sync && uv run pytest tests/unit/stubs/cd src/lfx && uv sync && uv run pytest tests/unit/custom/component/test_component_dx.pyuv run python scripts/generate_lfx_stubs.py --inline && cd src/lfx && uv build --wheelSummary by CodeRabbit
New Features
Build & Distribution
Tests
✏️ Tip: You can customize this high-level summary in your review settings.