Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build_release_artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
shell: bash
run: |
uv venv smoke
uv pip install -p smoke dist/*.whl numpy anywidget "reflex>=0.9.6"
uv pip install -p smoke dist/*.whl numpy anywidget "reflex>=0.9.6,<=0.10.0"
./smoke/bin/python -c "import importlib.metadata as m, reflex_xy, xy.kernels as k; assert k.BACKEND=='native', k.BACKEND; assert reflex_xy.__version__ == m.version('xy'); print('native', k.__file__)" \
|| ./smoke/Scripts/python.exe -c "import importlib.metadata as m, reflex_xy, xy.kernels as k; assert k.BACKEND=='native', k.BACKEND; assert reflex_xy.__version__ == m.version('xy'); print('native')"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -318,7 +318,7 @@ jobs:
XY_REQUIRE_CARGO: "1"
run: |
uv venv smoke-native
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<=0.10.0"
./smoke-native/bin/python - <<'PY'
import importlib.metadata as metadata

Expand All @@ -338,7 +338,7 @@ jobs:
XY_SKIP_CARGO: "1"
run: |
uv venv smoke-no-rust
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<=0.10.0"
./smoke-no-rust/bin/python - <<'PY'
import importlib.metadata as metadata

Expand Down
51 changes: 48 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,51 @@ jobs:
.venv/bin/python scripts/verify_benchmark_report.py \
dashboard-smoke.json --kind dashboard-browser

reflex_compatibility:
name: Reflex compatibility (${{ matrix.reflex-version }})
needs: [changes, build]
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-24.04
env:
XY_SKIP_CARGO: "1"
strategy:
fail-fast: false
matrix:
# Verify the minimum supported release, newest 0.9.x, and 0.10.0.
# The extra is capped at the newest version represented by this matrix.
reflex-version: ["0.9.6", "0.9.13", "0.10.0"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Restore shared native core and client
uses: ./.github/actions/restore-ci-build
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.13.5"
enable-cache: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.23.3"
- name: Install exact Reflex compatibility target
run: |
npm ci
uv venv .venv
uv pip install -p .venv/bin/python \
"reflex==${{ matrix.reflex-version }}" \
"numpy>=1.24" "anywidget>=0.9" "pytest>=8" "aiohttp>=3.9" "uvicorn>=0.23"
uv pip install -p .venv/bin/python -e . --no-deps
- name: Verify exact Reflex target and metadata window
run: |
.venv/bin/python -c "import importlib.metadata as m; assert m.version('reflex') == '${{ matrix.reflex-version }}'"
.venv/bin/python -c "from packaging.requirements import Requirement; import importlib.metadata as m; req = next(Requirement(value) for value in m.metadata('xy').get_all('Requires-Dist') if Requirement(value).name == 'reflex'); assert str(req.specifier) == '<=0.10.0,>=0.9.6'"
- name: Component compile and event smoke
run: .venv/bin/pytest -q tests/reflex_adapter/test_component.py::test_component_compiles_with_events
- name: State event and rebuild smoke
run: .venv/bin/pytest -q tests/reflex_adapter/test_state_bridge.py::test_rebuild_reads_session_state

regression_checks:
name: Blocking regression checks
needs: [changes, build]
Expand Down Expand Up @@ -1086,7 +1131,7 @@ jobs:
XY_REQUIRE_CARGO: "1"
run: |
uv venv smoke-native
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<=0.10.0"
./smoke-native/bin/python - <<'PY'
import importlib.metadata as metadata

Expand All @@ -1107,7 +1152,7 @@ jobs:
XY_SKIP_CARGO: "1"
run: |
uv venv smoke-no-rust
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6,<=0.10.0"
./smoke-no-rust/bin/python - <<'PY'
import importlib.metadata as metadata

Expand Down Expand Up @@ -1313,7 +1358,7 @@ jobs:
name: CI result
runs-on: ubuntu-24.04
if: always()
needs: [changes, build, test, rust_checks, browser_smokes, regression_checks, python_floor, matplotlib_reference, matplotlib_patch_smoke, pyplot_gallery, pyplot_gallery_extended, browser_conformance, sdist, wheels, install_without_rust]
needs: [changes, build, test, rust_checks, browser_smokes, regression_checks, python_floor, matplotlib_reference, matplotlib_patch_smoke, pyplot_gallery, pyplot_gallery_extended, browser_conformance, sdist, wheels, install_without_rust, reflex_compatibility]
steps:
- name: Require selected checks to succeed
env:
Expand Down
3 changes: 2 additions & 1 deletion docs/advanced/runtime-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ interaction math.

The integration is experimental and ships inside the published `xy`
distribution. Install `xy[reflex]` so the environment also selects a supported
Reflex version, then import it as `reflex_xy`. See
Reflex version (`>=0.9.6,<=0.10.0`), then import it as `reflex_xy`. CI tests
the minimum, newest 0.9.x, and 0.10.0 releases. See
[Deployment Recipes](/docs/xy/guides/deployment-recipes/).

## Choose a data tier
Expand Down
10 changes: 5 additions & 5 deletions docs/overview/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ uv add pyarrow
### Reflex integration

The bundled Reflex integration supports state-backed application charts and
remains experimental. Install the extra to select a compatible Reflex
version. With uv:
remains experimental. Install the extra to select a supported Reflex version
(`>=0.9.6,<=0.10.0`). With uv:

~~~bash
uv add "xy[reflex]"
Expand All @@ -155,9 +155,9 @@ Or with pip:
python -m pip install "xy[reflex]"
~~~

The `xy` wheel already carries the `reflex_xy` integration; the extra adds
only the supported Reflex dependency floor. Pin resolved versions for
production deployments. Continue with the
The `xy` wheel already carries the `reflex_xy` integration; the extra selects
the supported Reflex bound. CI smoke coverage includes 0.9.6, 0.9.13, and
0.10.0. Continue with the
[Reflex integration guide](/docs/xy/integrations/reflex/) for its current
limitations and setup.

Expand Down
1 change: 1 addition & 0 deletions news/475.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bound the supported Reflex compatibility window and run its version matrix as a selected blocking CI lane.
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ matplotlib = [
"matplotlib>=3.11,<3.12",
]
# The adapter source ships in every xy wheel so the framework integration and
# render client can never drift. The extra only selects the supported Reflex
# floor; importing plain `xy` remains framework-free.
# render client can never drift. The extra selects the tested Reflex window;
# importing plain `xy` remains framework-free.
reflex = [
"reflex>=0.9.6",
"reflex>=0.9.6,<=0.10.0",
]

[project.urls]
Expand Down
9 changes: 6 additions & 3 deletions scripts/artifact_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
from email.message import Message

BASE_DEPENDENCY_FLOORS = (("anywidget", "0.9"), ("numpy", "1.24"))
REFLEX_MINIMUM = "0.9.6"
REFLEX_MAXIMUM = "0.10.0"
REFLEX_SPECIFIER = f"reflex>={REFLEX_MINIMUM},<={REFLEX_MAXIMUM}"
REFLEX_REQUIREMENT = f"Requires-Dist: {REFLEX_SPECIFIER}; extra == 'reflex'"
MATPLOTLIB_REQUIREMENT = "Requires-Dist: matplotlib>=3.11,<3.12; extra == 'matplotlib'"
REFLEX_REQUIREMENT = "Requires-Dist: reflex>=0.9.6; extra == 'reflex'"


def _dependency_name(requirement: str) -> str:
Expand All @@ -33,10 +36,10 @@ def _is_valid_base_requirement(requirement: str, package: str, minimum: str) ->


def _is_exact_reflex_extra(requirement: str) -> bool:
"""Accept whitespace/quote normalization, but no extra constraints."""
"""Accept the bounded Reflex window in either metadata ordering."""
return bool(
re.fullmatch(
r"\s*reflex\s*>=\s*0\.9\.6\s*;\s*extra\s*==\s*['\"]reflex['\"]\s*",
rf"\s*reflex\s*(?:(?:>=\s*{re.escape(REFLEX_MINIMUM)}\s*,\s*<=\s*{re.escape(REFLEX_MAXIMUM)})|(?:<=\s*{re.escape(REFLEX_MAXIMUM)}\s*,\s*>=\s*{re.escape(REFLEX_MINIMUM)}))\s*;\s*extra\s*==\s*['\"]reflex['\"]\s*",
requirement,
flags=re.IGNORECASE,
)
Expand Down
114 changes: 108 additions & 6 deletions scripts/verify_ci_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
import json
import re
import sys
import tomllib
from pathlib import Path
from typing import Optional

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CI_WORKFLOW = ROOT / ".github" / "workflows" / "ci.yml"
DEFAULT_CODSPEED_WORKFLOW = ROOT / ".github" / "workflows" / "codspeed.yml"
DEFAULT_WORKFLOW = DEFAULT_CI_WORKFLOW
REFLEX_COMPATIBILITY_JOB = "reflex_compatibility"
REQUIRED_CI_JOBS = {
"result",
"changes",
Expand All @@ -35,6 +37,7 @@
"pyplot_gallery_extended",
"test",
"rust_checks",
"reflex_compatibility",
"browser_smokes",
"regression_checks",
"python_floor",
Expand All @@ -44,8 +47,11 @@
"sdist",
"wheels",
"install_without_rust",
REFLEX_COMPATIBILITY_JOB,
}
REQUIRED_CODSPEED_JOBS = {"benchmarks"}
REFLEX_OPTIONAL_DEPENDENCY = "reflex>=0.9.6,<=0.10.0"
REFLEX_SPECIFIER = "<=0.10.0,>=0.9.6"

CI_SUMMARY_DEPENDENCIES = (
"changes",
Expand All @@ -63,6 +69,7 @@
"sdist",
"wheels",
"install_without_rust",
"reflex_compatibility",
)
CI_SUMMARY_COMMANDS = [
"python3 - <<'PY'",
Expand Down Expand Up @@ -595,7 +602,12 @@ def _step_run_lines(step_block: str) -> list[str]:


def _require_step_runs_exactly(
errors: list[str], job_text: str, step: str, description: str, *commands: str
errors: list[str],
job_text: str,
step: str,
description: str,
*commands: str,
allow_matrix_strategy: bool = False,
) -> None:
"""Require the exact active command list in a hard-gate named step."""
block = _named_step_blocks(job_text).get(step)
Expand All @@ -610,16 +622,19 @@ def _require_step_runs_exactly(
"continue-on-error",
"defaults",
"container",
"strategy",
)
) + (() if allow_matrix_strategy else ("strategy",))
has_forbidden_job_key = any(
_has_yaml_key(job_text, key, indent=4) for key in forbidden_job_keys
)
# Scope/dependency controls are allowed only in their reviewed form.
group = "pyplot" if job_text.startswith(" matplotlib_reference:") else "runtime"
for key, expected in (
("if", f"needs.changes.outputs.{group} == 'true'"),
("needs", "changes" if job_text.startswith(" rust_checks:") else "[changes, build]"),
()
if allow_matrix_strategy
else (
("if", f"needs.changes.outputs.{group} == 'true'"),
("needs", "changes" if job_text.startswith(" rust_checks:") else "[changes, build]"),
)
):
values, ambiguous = _direct_yaml_key_values(job_text, key, indent=4)
if ambiguous or values != [expected]:
Expand All @@ -634,6 +649,20 @@ def _require_step_runs_exactly(
errors.append(f"CI step {step!r} missing {description}: {list(commands)!r}")


def _require_matrix_step_runs_exactly(
Comment thread
harsh21234i marked this conversation as resolved.
Resolved
errors: list[str], job_text: str, step: str, description: str, *commands: str
) -> None:
"""Require the complete active command list in one matrix step."""
_require_step_runs_exactly(
errors,
job_text,
step,
description,
*commands,
allow_matrix_strategy=True,
)


def _require_job_contains(
errors: list[str],
jobs: dict[str, str],
Expand Down Expand Up @@ -1251,6 +1280,78 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"tests/pyplot/test_gallery_behavior.py",
"tests/pyplot/test_public_protocols.py",
)
_require_job_contains(
errors,
jobs,
REFLEX_COMPATIBILITY_JOB,
"CI",
"minimum and maximum supported Reflex compatibility matrix",
"fail-fast: false",
'reflex-version: ["0.9.6", "0.9.13", "0.10.0"]',
)
reflex_job = jobs.get(REFLEX_COMPATIBILITY_JOB, "")
matrix_values, matrix_unsafe = _direct_yaml_key_values(reflex_job, "reflex-version", indent=8)
if matrix_values != ['["0.9.6", "0.9.13", "0.10.0"]'] or matrix_unsafe:
errors.append(
"CI reflex_compatibility job must test exactly Reflex 0.9.6, 0.9.13, and 0.10.0"
)
if _has_yaml_key(reflex_job, "exclude", indent=8):
Comment thread
harsh21234i marked this conversation as resolved.
Resolved
errors.append("CI reflex_compatibility job must not exclude compatibility versions")
if _has_yaml_key(reflex_job, "include", indent=8):
errors.append("CI reflex_compatibility job must not include additional versions")
_require_matrix_step_runs_exactly(
errors,
reflex_job,
"Install exact Reflex compatibility target",
"exact Reflex installation",
"npm ci",
"uv venv .venv",
"uv pip install -p .venv/bin/python \\",
'"reflex==${{ matrix.reflex-version }}" \\',
'"numpy>=1.24" "anywidget>=0.9" "pytest>=8" "aiohttp>=3.9" "uvicorn>=0.23"',
"uv pip install -p .venv/bin/python -e . --no-deps",
)
_require_matrix_step_runs_exactly(
errors,
reflex_job,
"Verify exact Reflex target and metadata window",
"Reflex version and metadata assertions",
".venv/bin/python -c \"import importlib.metadata as m; assert m.version('reflex') == '${{ matrix.reflex-version }}'\"",
f".venv/bin/python -c \"from packaging.requirements import Requirement; import importlib.metadata as m; req = next(Requirement(value) for value in m.metadata('xy').get_all('Requires-Dist') if Requirement(value).name == 'reflex'); assert str(req.specifier) == '{REFLEX_SPECIFIER}'\"",
)
_require_matrix_step_runs_exactly(
errors,
reflex_job,
"Component compile and event smoke",
"component compile and event smoke",
".venv/bin/pytest -q tests/reflex_adapter/test_component.py::test_component_compiles_with_events",
)
_require_matrix_step_runs_exactly(
errors,
reflex_job,
"State event and rebuild smoke",
"state event and rebuild smoke",
".venv/bin/pytest -q tests/reflex_adapter/test_state_bridge.py::test_rebuild_reads_session_state",
)
try:
project_metadata = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
except OSError as exc:
errors.append(f"cannot read project metadata for Reflex compatibility gate: {exc}")
else:
try:
project = tomllib.loads(project_metadata).get("project", {})
except tomllib.TOMLDecodeError as exc:
errors.append(f"cannot parse pyproject.toml for Reflex compatibility gate: {exc}")
else:
try:
optional_dependencies = project.get("optional-dependencies", {})
reflex_dependencies = optional_dependencies.get("reflex")
except AttributeError:
reflex_dependencies = None
if reflex_dependencies != [REFLEX_OPTIONAL_DEPENDENCY]:
errors.append(
f"pyproject.toml must bound the xy[reflex] extra to {REFLEX_OPTIONAL_DEPENDENCY!r}"
)
_require_job_contains(
errors,
jobs,
Expand Down Expand Up @@ -1535,7 +1636,7 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"Rust-backed sdist install contract",
"XY_REQUIRE_CARGO",
"uv pip install --no-cache",
'"reflex>=0.9.6"',
f'"{REFLEX_OPTIONAL_DEPENDENCY}"',
"import reflex_xy",
"import xy.kernels as kernels",
'kernels.BACKEND == "native"',
Expand All @@ -1547,6 +1648,7 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]:
"coreless sdist import contract",
"XY_SKIP_CARGO",
"uv pip install --no-cache",
f'"{REFLEX_OPTIONAL_DEPENDENCY}"',
"import reflex_xy",
"assert reflex_xy.__version__ == version",
"native Rust core",
Expand Down
Loading
Loading