Skip to content

Compiler hooks plugins#6272

Open
FarhanAliRaza wants to merge 7 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks-plugins
Open

Compiler hooks plugins#6272
FarhanAliRaza wants to merge 7 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks-plugins

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Collaborator

@FarhanAliRaza FarhanAliRaza commented Apr 1, 2026

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

New Feature Submission:

  • [] Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

closes #6211

Replace 6 separate recursive tree walks (_get_all_imports, _get_all_hooks,
_get_all_custom_code, _get_all_dynamic_imports, _get_all_refs,
_get_all_app_wrap_components) with a single collect_component_tree_artifacts
walk that gathers all compilation data in one pass.

Wire the new collector into app.py, compiler.py, and utils.py. Add
CompilerPlugin protocol, CompilerHooks dispatcher, and BaseContext/
PageContext/CompileContext types as foundations for the async plugin
pipeline.
Refactor the monolithic plugins.py into a plugins/ package:
- base.py: core framework (CompilerPlugin base class, CompilerHooks, contexts)
- builtin.py: concrete plugins matching legacy recursive collectors
- __init__.py: re-exports all public names

Adds ApplyStylePlugin, DefaultPagePlugin, and Consolidate*Plugin classes
that replicate legacy page compilation behavior. Expands test coverage
with per-plugin verification and full pipeline integration tests.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing FarhanAliRaza:compiler-hooks-plugins (1d38393) with main (980a97d)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR introduces a new compiler plugin infrastructure (reflex/compiler/plugins/) that enables a structured, single-pass page compilation pipeline via an ordered chain of CompilerPlugin implementations. It also adds a new set of Redis state manager tests.

Key changes:

  • base.py: Defines CompilerPlugin (Protocol), CompilerHooks (dispatcher), and three async context managers (BaseContext, PageContext, CompileContext) for scoped compilation state. The two-phase generator pattern for compile_component — where plugins yield a pre-replacement, receive compiled children via asend, then optionally yield a post-replacement — is well-designed and cleanly handles plugin ordering and cleanup.
  • builtin.py: Implements eight built-in plugins (DefaultPagePlugin, ApplyStylePlugin, and six Consolidate* plugins) that replicate and replace the existing recursive collector methods. The default_page_plugins() factory assembles the canonical pipeline.
  • test_plugins.py: Thorough test coverage including legacy parity assertions and edge cases (context lifecycle, hook skipping for inherited Protocol methods, duplicate route rejection).
  • test_redis.py: New oplock contention and lease management tests for StateManagerRedis.
  • Notable issue: ApplyStylePlugin uses raise UserWarning(msg) instead of warnings.warn(msg, UserWarning), which would hard-abort compilation for components that override _add_style rather than issuing a developer warning.

Confidence Score: 5/5

  • Safe to merge — the plugin system is well-structured and thoroughly tested; remaining findings are style/convention issues
  • All findings are P2: the raise UserWarning pattern is semantically awkward but only affects components that incorrectly override _add_style (an internal API), the state_manager.close() ordering question depends on real_redis() internals not visible in the diff, and the missing time comments are a convention gap. None of these represent present defects in the primary compilation path.
  • reflex/compiler/plugins/builtin.py for the raise UserWarning pattern; tests/units/istate/manager/test_redis.py for the close-after-context-exit ordering

Important Files Changed

Filename Overview
reflex/compiler/plugins/base.py New core plugin infrastructure: CompilerPlugin Protocol, CompilerHooks dispatcher, BaseContext/PageContext/CompileContext async context managers — well-structured with thorough generator lifecycle management
reflex/compiler/plugins/builtin.py Built-in plugin implementations for page compilation; contains a raise UserWarning(...) that should use warnings.warn to avoid aborting compilation unexpectedly
reflex/compiler/plugins/init.py Clean public API surface — re-exports all plugin types and built-ins with a complete __all__ list
tests/units/compiler/test_plugins.py Comprehensive test coverage for the new plugin infrastructure: hook dispatch ordering, context lifecycle, all built-in plugins, and legacy parity checks
tests/units/istate/manager/test_redis.py New Redis state manager tests with oplock contention scenarios; state_manager.close() is called after the real_redis() context exits, and time-based magic numbers lack human-readable comments
pyproject.toml Adds "asend" to the codespell ignore list to avoid false positives from the async generator .asend() calls introduced by the new plugin infrastructure

Sequence Diagram

sequenceDiagram
    participant CC as CompileContext
    participant CH as CompilerHooks
    participant P1 as DefaultPagePlugin
    participant P2 as ApplyStylePlugin
    participant P3 as Consolidate*Plugins

    CC->>CH: eval_page(page_fn, page=page)
    CH->>P1: eval_page(page_fn) → PageContext
    P1-->>CH: PageContext(name, route, root_component)
    CH-->>CC: PageContext

    CC->>CC: async with page_ctx
    CC->>CH: compile_component(root_component)

    Note over CH,P3: Pre-child phase (anext each plugin)
    CH->>P2: compile_component(comp) → yield pre_replacement
    CH->>P3: compile_component(comp) → yield pre_replacement

    Note over CH: Recurse into structural children
    CH->>CH: _compile_children(structural_children)
    CH->>CH: traverse prop components (in_prop_tree=True)

    Note over CH,P3: Post-child phase (asend each plugin in reverse)
    CH->>P3: asend((compiled_comp, compiled_children))
    CH->>P2: asend((compiled_comp, compiled_children))
    CH-->>CC: compiled root component

    CC->>CH: compile_page(page_ctx)
    CH->>P3: compile_page(page_ctx)  [e.g. ConsolidateImportsPlugin collapses imports]

    CC->>CC: compiled_pages[route] = page_ctx
Loading

Comments Outside Diff (2)

  1. tests/units/istate/manager/test_redis.py, line 657-662 (link)

    P2 Missing human-readable time duration comments

    Per project convention, time-based calculations should include comments explaining the duration in plain terms. Several places in this file have bare millisecond values or unit-conversion math with no explanation.

    test_redis.py:658 — the ms values are silent about their meaning:

    test_redis.py:705short_lock_expiration / 1000 * 0.5 should clarify it sleeps for half the expiry window:

    await asyncio.sleep(short_lock_expiration / 1000 * 0.5)  # sleep 50% of lock_expiration

    test_redis.py:714 — same pattern:

    await asyncio.sleep(short_lock_expiration / 1000 * 0.8)  # sleep 80% of lock_expiration

    test_redis.py:58 — the * 1000 conversion to milliseconds is implicit:

    assert (time.monotonic() - test_start) * 1000 < state_manager.lock_expiration  # convert s → ms

    Rule Used: When using time-based calculations in code, includ... (source)

    Learnt From
    reflex-dev/flexgen#2190

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  2. tests/units/istate/manager/test_redis.py, line 39-60 (link)

    P2 state_manager.close() called after real_redis() context exits

    await state_manager.close() is called on line 60, outside and after the async with real_redis() block. If real_redis() manages the connection lifetime (i.e., its __aexit__ closes the underlying Redis connection), then state_manager.close() would be operating on an already-closed connection, which may raise errors or silently swallow them.

    The typical pattern would be to call close() inside the async with block before exiting:

    async with real_redis() as redis:
        if redis is None:
            redis = mock_redis()
        state_manager = StateManagerRedis(state=root_state, redis=redis)
        test_start = time.monotonic()
        try:
            yield state_manager
            assert (time.monotonic() - test_start) * 1000 < state_manager.lock_expiration  # convert s → ms
        finally:
            await state_manager.close()

    If real_redis() intentionally does not own the connection lifetime, this is fine as-is — a clarifying comment would help.

Reviews (1): Last reviewed commit: "Split compiler plugins into package with..." | Re-trigger Greptile

Comment on lines +94 to +96
if type(comp)._add_style != Component._add_style:
msg = "Do not override _add_style directly. Use add_style instead."
raise UserWarning(msg)
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.

P2 UserWarning raised as exception instead of warnings.warn

raise UserWarning(msg) raises UserWarning as an exception, which will abort compilation with an unhandled UserWarning. The conventional pattern for user-facing developer warnings is warnings.warn(msg, UserWarning, stacklevel=2), which emits the warning but allows compilation to continue.

If hard-failing is intentional, use a more semantically appropriate exception like TypeError or RuntimeError — raising UserWarning as an exception is confusing and unexpected for callers.

Suggested change
if type(comp)._add_style != Component._add_style:
msg = "Do not override _add_style directly. Use add_style instead."
raise UserWarning(msg)
if type(comp)._add_style != Component._add_style:
import warnings
msg = "Do not override _add_style directly. Use add_style instead."
warnings.warn(msg, UserWarning, stacklevel=2)

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.

Single-pass compiler: Implement core compilation plugins (imports, hooks, custom code, dynamic imports, refs)

1 participant