Skip to content

Commit c05b1e8

Browse files
committed
fix missing static files on git+ installs
1 parent ba8d321 commit c05b1e8

5 files changed

Lines changed: 211 additions & 71 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
uses: ./.github/workflows/.hatch-run.yml
1515
with:
1616
job-name: "Publish to PyPI"
17-
run-cmd: "hatch run javascript:build && hatch build --clean && hatch publish --yes"
17+
run-cmd: "hatch build --clean && hatch publish --yes"
1818
secrets:
1919
pypi-username: ${{ secrets.PYPI_USERNAME }}
2020
pypi-password: ${{ secrets.PYPI_PASSWORD }}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ installer = "uv"
7171
reactpy = "reactpy._console.cli:entry_point"
7272

7373
[[tool.hatch.build.hooks.build-scripts.scripts]]
74-
commands = ['python "src/build_scripts/build_local_wheel.py"']
74+
commands = ['python "src/build_scripts/build_py_wheel.py"']
7575
artifacts = ["src/reactpy/static/wheels/*.whl"]
7676

7777
#############################
Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pathlib import Path
1616

1717
_logger = logging.getLogger(__name__)
18-
_SKIP_ENV_VAR = "REACTPY_SKIP_LOCAL_WHEEL_BUILD"
18+
_SKIP_ENV_VAR = "REACTPY_SKIP_PY_WHEEL_BUILD"
1919

2020

2121
def _reactpy_version(root_dir: Path) -> str:
@@ -38,29 +38,80 @@ def _matching_reactpy_wheel(dist_dir: Path, version: str) -> Path | None:
3838
return matching_wheels[0] if matching_wheels else None
3939

4040

41-
def _hatch_build_command(root_dir: Path) -> list[str] | None:
41+
def _hatch_command(root_dir: Path, *args: str) -> list[str] | None:
4242
for candidate in (
4343
root_dir / ".venv" / "Scripts" / "hatch.exe",
4444
root_dir / ".venv" / "bin" / "hatch",
4545
):
4646
if candidate.exists():
47-
return [str(candidate), "build", "-t", "wheel"]
47+
return [str(candidate), *args]
4848

4949
if hatch_command := shutil.which("hatch"):
50-
return [hatch_command, "build", "-t", "wheel"]
50+
return [hatch_command, *args]
5151

5252
if importlib.util.find_spec("hatch") is not None:
53-
return [sys.executable, "-m", "hatch", "build", "-t", "wheel"]
53+
return [sys.executable, "-m", "hatch", *args]
5454

5555
return None
5656

5757

58+
def _hatch_build_command(root_dir: Path) -> list[str] | None:
59+
return _hatch_command(root_dir, "build", "-t", "wheel")
60+
61+
62+
def _without_hatch_env_vars(env: dict[str, str]) -> dict[str, str]:
63+
cleaned_env = env.copy()
64+
for key in tuple(cleaned_env):
65+
if key.startswith("HATCH_ENV_"):
66+
cleaned_env.pop(key)
67+
return cleaned_env
68+
69+
70+
def _run_hatch_command(root_dir: Path, command: list[str], failure_message: str) -> int:
71+
result = subprocess.run( # noqa: S603
72+
command,
73+
capture_output=True,
74+
text=True,
75+
check=False,
76+
cwd=root_dir,
77+
env=_without_hatch_env_vars(os.environ.copy()),
78+
)
79+
80+
if result.returncode != 0:
81+
_logger.error(
82+
"%s\nstdout:\n%s\nstderr:\n%s",
83+
failure_message,
84+
result.stdout,
85+
result.stderr,
86+
)
87+
return result.returncode
88+
89+
return 0
90+
91+
92+
def _build_packaged_static_assets(root_dir: Path) -> int:
93+
hatch_command = _hatch_command(root_dir, "run", "javascript:build")
94+
if not hatch_command:
95+
_logger.error("Could not locate Hatch while building packaged static assets.")
96+
return 1
97+
98+
return _run_hatch_command(
99+
root_dir,
100+
hatch_command,
101+
"Failed to build packaged static assets.",
102+
)
103+
104+
58105
def main() -> int:
59106
if os.environ.get(_SKIP_ENV_VAR):
60107
print("Skipping local ReactPy wheel build.") # noqa: T201
61108
return 0
62109

63110
root_dir = Path(__file__).parent.parent.parent
111+
112+
if static_assets_result := _build_packaged_static_assets(root_dir):
113+
return static_assets_result
114+
64115
version = _reactpy_version(root_dir)
65116
static_wheels_dir = root_dir / "src" / "reactpy" / "static" / "wheels"
66117
dist_dir = root_dir / "dist"
@@ -74,28 +125,16 @@ def main() -> int:
74125
for wheel_file in static_wheels_dir.glob("reactpy-*.whl"):
75126
wheel_file.unlink()
76127

77-
env = os.environ.copy()
78-
env[_SKIP_ENV_VAR] = "1"
79-
for key in tuple(env):
80-
if key.startswith("HATCH_ENV_"):
81-
env.pop(key)
82-
83-
result = subprocess.run( # noqa: S603
84-
hatch_build_command,
85-
capture_output=True,
86-
text=True,
87-
check=False,
88-
cwd=root_dir,
89-
env=env,
90-
)
91-
92-
if result.returncode != 0:
93-
_logger.error(
94-
"Failed to build the embedded ReactPy wheel.\nstdout:\n%s\nstderr:\n%s",
95-
result.stdout,
96-
result.stderr,
97-
)
98-
return result.returncode
128+
os.environ[_SKIP_ENV_VAR] = "1"
129+
try:
130+
if wheel_build_result := _run_hatch_command(
131+
root_dir,
132+
hatch_build_command,
133+
"Failed to build the embedded ReactPy wheel.",
134+
):
135+
return wheel_build_result
136+
finally:
137+
os.environ.pop(_SKIP_ENV_VAR, None)
99138

100139
built_wheel = _matching_reactpy_wheel(dist_dir, version)
101140
if not built_wheel:

tests/test_build_local_wheel.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

tests/test_build_py_wheel.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
import subprocess
5+
from pathlib import Path
6+
from unittest import mock
7+
8+
9+
def _load_build_py_wheel_module():
10+
module_path = (
11+
Path(__file__).resolve().parents[1]
12+
/ "src"
13+
/ "build_scripts"
14+
/ "build_py_wheel.py"
15+
)
16+
spec = importlib.util.spec_from_file_location("build_py_wheel", module_path)
17+
assert spec is not None
18+
assert spec.loader is not None
19+
20+
module = importlib.util.module_from_spec(spec)
21+
spec.loader.exec_module(module)
22+
return module
23+
24+
25+
def _write_text(path: Path, content: str) -> Path:
26+
path.parent.mkdir(parents=True, exist_ok=True)
27+
path.write_text(content, encoding="utf-8")
28+
return path
29+
30+
31+
def test_hatch_build_command_uses_python_module_when_available(tmp_path):
32+
build_py_wheel = _load_build_py_wheel_module()
33+
34+
with (
35+
mock.patch.object(build_py_wheel.shutil, "which", return_value=None),
36+
mock.patch.object(
37+
build_py_wheel.importlib.util,
38+
"find_spec",
39+
return_value=object(),
40+
),
41+
):
42+
assert build_py_wheel._hatch_build_command(tmp_path) == [
43+
build_py_wheel.sys.executable,
44+
"-m",
45+
"hatch",
46+
"build",
47+
"-t",
48+
"wheel",
49+
]
50+
51+
52+
def test_build_packaged_static_assets_runs_javascript_build(tmp_path):
53+
build_py_wheel = _load_build_py_wheel_module()
54+
55+
with (
56+
mock.patch.object(build_py_wheel.shutil, "which", return_value=None),
57+
mock.patch.object(
58+
build_py_wheel.importlib.util,
59+
"find_spec",
60+
return_value=object(),
61+
),
62+
mock.patch.object(build_py_wheel.subprocess, "run") as run,
63+
):
64+
run.return_value = subprocess.CompletedProcess([], 0, "built", "")
65+
66+
assert build_py_wheel._build_packaged_static_assets(tmp_path) == 0
67+
68+
run.assert_called_once()
69+
assert run.call_args.args[0] == [
70+
build_py_wheel.sys.executable,
71+
"-m",
72+
"hatch",
73+
"run",
74+
"javascript:build",
75+
]
76+
77+
78+
def test_main_skips_all_work_when_skip_env_var_is_set(tmp_path):
79+
build_py_wheel = _load_build_py_wheel_module()
80+
build_script = _write_text(
81+
tmp_path / "src" / "build_scripts" / "build_py_wheel.py",
82+
"",
83+
)
84+
85+
with (
86+
mock.patch.object(build_py_wheel, "__file__", str(build_script)),
87+
mock.patch.object(
88+
build_py_wheel, "_build_packaged_static_assets"
89+
) as build_static_assets,
90+
mock.patch.dict(
91+
build_py_wheel.os.environ,
92+
{build_py_wheel._SKIP_ENV_VAR: "1"},
93+
clear=False,
94+
),
95+
):
96+
assert build_py_wheel.main() == 0
97+
98+
build_static_assets.assert_not_called()
99+
100+
101+
def test_main_builds_static_assets_before_embedded_wheel(tmp_path):
102+
build_py_wheel = _load_build_py_wheel_module()
103+
build_script = _write_text(
104+
tmp_path / "src" / "build_scripts" / "build_py_wheel.py",
105+
"",
106+
)
107+
built_wheel = _write_text(
108+
tmp_path / "dist" / "reactpy-2.0.0b11-py3-none-any.whl",
109+
"wheel",
110+
)
111+
steps: list[str] = []
112+
113+
with (
114+
mock.patch.object(build_py_wheel, "__file__", str(build_script)),
115+
mock.patch.object(
116+
build_py_wheel,
117+
"_build_packaged_static_assets",
118+
side_effect=lambda root_dir: steps.append("javascript") or 0,
119+
),
120+
mock.patch.object(
121+
build_py_wheel,
122+
"_reactpy_version",
123+
return_value="2.0.0b11",
124+
),
125+
mock.patch.object(
126+
build_py_wheel,
127+
"_hatch_build_command",
128+
return_value=["hatch", "build", "-t", "wheel"],
129+
),
130+
mock.patch.object(
131+
build_py_wheel,
132+
"_run_hatch_command",
133+
side_effect=lambda root_dir, command, message: steps.append("wheel") or 0,
134+
),
135+
mock.patch.object(
136+
build_py_wheel,
137+
"_matching_reactpy_wheel",
138+
return_value=built_wheel,
139+
),
140+
):
141+
assert build_py_wheel.main() == 0
142+
143+
assert steps == ["javascript", "wheel"]

0 commit comments

Comments
 (0)