Skip to content

Upgrade dev tooling and fix docstring style - #6893

Open
masenf wants to merge 1 commit into
mainfrom
claude/upgrade-ruff-pyright-typer-u3z09k
Open

Upgrade dev tooling and fix docstring style#6893
masenf wants to merge 1 commit into
mainfrom
claude/upgrade-ruff-pyright-typer-u3z09k

Conversation

@masenf

@masenf masenf commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?

Description

This PR upgrades locked dev tooling and addresses linting issues introduced by the new versions:

Tooling upgrades:

  • ruff 0.15.12 β†’ 0.16.2
  • pyright 1.1.408 β†’ 1.1.411
  • typer 0.25.1 β†’ 0.27.1

Key changes:

  1. Property docstring style (ruff D421): Updated all property docstrings from imperative form ("Get the ...", "Return the ...") to noun phrases ("The ..."). This applies across:

    • reflex/app.py
    • reflex/compiler/compiler.py
    • reflex/experimental/client_state.py
    • reflex/istate/
    • packages/reflex-base/src/reflex_base/ (vars, event, config, registry, plugins)
    • packages/reflex-components-core/src/reflex_components_core/
    • packages/reflex-docgen/src/reflex_docgen/
    • packages/reflex-components-internal/src/reflex_components_internal/
  2. Type safety improvements:

    • Added cast() calls in reflex/compiler/compiler.py and reflex/reflex.py for untyped libsass and typer APIs
    • Fixed type annotation in packages/reflex-base/src/reflex_base/vars/base.py for default_value with explanatory comment
    • Removed redundant EventSpec import from TYPE_CHECKING block in packages/reflex-base/src/reflex_base/event/processor/base_state_processor.py
    • Changed chain_updates() parameter type from EventSpec | list[EventSpec] | None to Any with updated docstring explaining runtime validation
  3. Code quality fixes:

    • Simplified condition in packages/reflex-base/src/reflex_base/utils/types.py (removed redundant cls is not None check)
    • Fixed import organization in packages/reflex-base/src/reflex_base/vars/datetime.py (moved ImportVar to correct location)
    • Improved control flow in packages/reflex-base/src/reflex_base/vars/dep_tracking.py (split compound condition for clarity)
  4. Test infrastructure:

    • Created tests/units/reflex_cli/v2/utils.py with as_click_command() helper to reduce duplication across CLI tests
    • Updated all CLI test files to use the new helper
    • Removed redundant scope="function" parameter from pytest_asyncio.fixture decorators (already set by loop_scope)
  5. Linting configuration:

    • Added ASYNC119 to ignore list (background event handlers hold async with self across yield by design)
    • Added RUF075 to ignore list (state managers deliberately skip write-back when with body raises)
    • Added RUF105 and RUF201 to ignore list (preserve noqa comment readability)

Test Plan

Existing unit tests pass. The changes are primarily:

  • Docstring style updates (no functional impact)
  • Type annotations and casts for better type checking
  • Test infrastructure refactoring (DRY principle)
  • Linting configuration updates

All changes maintain backward compatibility and improve code quality without altering runtime behavior.

https://claude.ai/code/session_01RLumacyNrCZDqvyptaVxkE

Review in cubic

uv.lock: ruff 0.15.12 -> 0.16.2, pyright 1.1.408 -> 1.1.411,
typer 0.25.1 -> 0.27.1. ruff 0.16.3 published inside the 7 day
exclude-newer window, so 0.16.2 is the newest resolvable release.

ruff 0.16 (preview rules are enabled repo-wide):

- D421 property-docstring-starts-with-verb: reword 53 property
  docstrings from "Get the X." / "Return the X." to "The X.", per the
  Google style guide the repo already follows.
- PT003: drop the redundant scope="function" from five
  pytest_asyncio.fixture calls, which 0.16 now recognizes.
- ASYNC119, RUF075: ignored. Both fire on deliberate patterns --
  background handlers hold `async with self` across `yield`, and the
  state managers skip write-back when the `with` body raises.
- RUF105, RUF201: ignored. Both are stylistic; RUF105's own docs call
  it opinionated, and migrating 190 `noqa` comments to `ruff: ignore`
  would drop them for other tooling and for older ruff. RUF201 would
  leave the config half codes, half names, since prefix selectors have
  no name form.

pyright 1.1.411 narrows `x is None` on an `Any` value to `Any | None`,
where it previously stayed `Any`. That surfaced two dead guards, both
removed: `cls is not None` in `_isinstance()` (the line above already
returns for None) and `instruction.argval is not None` in the
dependency-tracking scanner, where the check moves inside the branch so
it stops leaking None into the sibling branches. Its bundled typeshed
also types `inspect.isgenerator`/`isasyncgen` as yielding `object`, so
`chain_updates()` now declares `events: Any`, matching the runtime
validation it delegates to. Also: annotate `Field.__init__`'s computed
default as FIELD_TYPE, cast untyped `sass.compile()` to str, and import
`ImportVar`/`unionize` from the modules that define them rather than
re-exporting them through `reflex_base.vars.base`.

typer 0.27 vendors its own copy of click, so `typer.main.get_command()`
no longer returns a nominal `click.Command`. The objects stay
structurally compatible (`reflex cloud` and the 289 hosting CLI tests
pass), so the conversion is cast at its two call sites; the six test
modules that duplicated the Typer-to-click preamble now share an
`as_click_command()` helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLumacyNrCZDqvyptaVxkE
@masenf
masenf requested a review from a team as a code owner August 14, 2026 23:08
@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR upgrades the locked Ruff, Pyright, and Typer development tools and resolves the resulting lint and type-check diagnostics.

  • Rephrases property docstrings to satisfy the new Ruff rule.
  • Adds type-only casts and annotations around untyped or vendored APIs.
  • Refactors hosting CLI tests around a shared command adapter.
  • Applies behavior-preserving import, condition, fixture, and bytecode-scanner cleanups.

Confidence Score: 5/5

The PR appears safe to merge because no changed-code-triggered blocking or independently actionable non-blocking issue remains.

The behaviorally relevant edits are type-only casts, annotations, and equivalent control-flow refactors, while the dependency lock changes are limited to the intended development tools.

Important Files Changed

Filename Overview
uv.lock Updates only the locked Pyright, Ruff, and Typer versions; reported vulnerable dependency versions were already present on the base revision.
pyproject.toml Adds documented Ruff ignores corresponding to intentional framework and state-manager behavior.
reflex/reflex.py Adds a type-only cast for Typer’s vendored-Click command before preserving the existing cloud command registration.
reflex/compiler/compiler.py Casts the untyped libsass filename compilation result to str without changing runtime behavior.
packages/reflex-base/src/reflex_base/event/processor/base_state_processor.py Widens chain_updates typing to match its existing runtime validation while leaving event routing unchanged.
packages/reflex-base/src/reflex_base/vars/dep_tracking.py Rewrites the IMPORT_NAME null guard for type narrowing while preserving the previous no-op behavior.
packages/reflex-base/src/reflex_base/vars/base.py Adds a type annotation for computed field defaults and updates property docstrings without changing value construction.
tests/units/reflex_cli/v2/utils.py Centralizes the repeated Typer-to-command adaptation used by hosting CLI tests.

Reviews (1): Last reviewed commit: "Upgrade ruff, pyright, and typer; fix th..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

βœ… 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/upgrade-ruff-pyright-typer-u3z09k (08c78da) with main (fddcb6b)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 59 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid β€” if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/reflex-base/src/reflex_base/vars/base.py">

<violation number="1" location="packages/reflex-base/src/reflex_base/vars/base.py:3522">
P3: The `default_value: FIELD_TYPE` annotation is unsound for the branch where `get_default_value_for_type` returns `None` for a non-optional type (e.g. `Field[SomeCustomClass]` where the class is not one of `TYPES_THAT_HAS_DEFAULT_VALUE`, a Mapping, Literal, or dataframe). In that case `default_value` is `None`, but `FIELD_TYPE` bound to the non-optional custom class excludes `None`, so the value does not inhabit the annotated type. The widening `annotated_type = annotated_type | None` runs after the annotation and rebinds the local `annotated_type` variable only; it does not make `None` assignable to the `FIELD_TYPE` that the earlier annotation references. The comment's invariant ("the value always inhabits FIELD_TYPE") therefore does not hold, and since the value is typed through `Any` from `get_default_value_for_type`, pyright silently trusts the lie β€” masking the fact that these fields can hold a `None` default contrary to their claimed non-None `FIELD_TYPE`. Apply the widening before typing the value, or drop the `FIELD_TYPE` assertion.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

default_value = types.get_default_value_for_type(annotated_type)
# `annotated_type` is widened to include None below when the
# computed default is None, so the value always inhabits FIELD_TYPE.
default_value: FIELD_TYPE = types.get_default_value_for_type(

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.

P3: The default_value: FIELD_TYPE annotation is unsound for the branch where get_default_value_for_type returns None for a non-optional type (e.g. Field[SomeCustomClass] where the class is not one of TYPES_THAT_HAS_DEFAULT_VALUE, a Mapping, Literal, or dataframe). In that case default_value is None, but FIELD_TYPE bound to the non-optional custom class excludes None, so the value does not inhabit the annotated type. The widening annotated_type = annotated_type | None runs after the annotation and rebinds the local annotated_type variable only; it does not make None assignable to the FIELD_TYPE that the earlier annotation references. The comment's invariant ("the value always inhabits FIELD_TYPE") therefore does not hold, and since the value is typed through Any from get_default_value_for_type, pyright silently trusts the lie β€” masking the fact that these fields can hold a None default contrary to their claimed non-None FIELD_TYPE. Apply the widening before typing the value, or drop the FIELD_TYPE assertion.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/vars/base.py, line 3522:

<comment>The `default_value: FIELD_TYPE` annotation is unsound for the branch where `get_default_value_for_type` returns `None` for a non-optional type (e.g. `Field[SomeCustomClass]` where the class is not one of `TYPES_THAT_HAS_DEFAULT_VALUE`, a Mapping, Literal, or dataframe). In that case `default_value` is `None`, but `FIELD_TYPE` bound to the non-optional custom class excludes `None`, so the value does not inhabit the annotated type. The widening `annotated_type = annotated_type | None` runs after the annotation and rebinds the local `annotated_type` variable only; it does not make `None` assignable to the `FIELD_TYPE` that the earlier annotation references. The comment's invariant ("the value always inhabits FIELD_TYPE") therefore does not hold, and since the value is typed through `Any` from `get_default_value_for_type`, pyright silently trusts the lie β€” masking the fact that these fields can hold a `None` default contrary to their claimed non-None `FIELD_TYPE`. Apply the widening before typing the value, or drop the `FIELD_TYPE` assertion.</comment>

<file context>
@@ -3517,7 +3517,11 @@ def __init__(
-                default_value = types.get_default_value_for_type(annotated_type)
+                # `annotated_type` is widened to include None below when the
+                # computed default is None, so the value always inhabits FIELD_TYPE.
+                default_value: FIELD_TYPE = types.get_default_value_for_type(
+                    annotated_type
+                )
</file context>

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.

2 participants