Skip to content

Commit d2416c4

Browse files
committed
some tests
1 parent b89c78b commit d2416c4

File tree

7 files changed

+91
-18
lines changed

7 files changed

+91
-18
lines changed

docs/building-projects/build_steps.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ $ view build
1717

1818
This will export your app into a static folder called `build`, which can then be served via something like [http.server](https://docs.python.org/3/library/http.server.html). An exported route cannot contain:
1919

20-
- Route Inputs
21-
- Path Parameters
22-
- A method other than `GET`
20+
- Route Inputs
21+
- Path Parameters
22+
- A method other than `GET`
2323

2424
As stated above, you can also build your app programatically via `build_app`:
2525

@@ -37,7 +37,7 @@ build_app(app)
3737

3838
## Build Steps
3939

40-
Instead of exporting static HTML, you might just want to call some build script at runtime for your app to use. For example, this could be something like a [Next.js](https://nextjs.org) app, which you want to use as the UI for your website. Each different build is called a **build step** in View. View's build system does not aim to be a full fledged build system, but instead a bridge to use other package managers or tools to build requirements for your app. It tries to be *extendable*, instead of batteries-included.
40+
Instead of exporting static HTML, you might just want to call some build script at runtime for your app to use. For example, this could be something like a [Next.js](https://nextjs.org) app, which you want to use as the UI for your website. Each different build is called a **build step** in View. View's build system does not aim to be a full fledged build system, but instead a bridge to use other package managers or tools to build requirements for your app. It tries to be _extendable_, instead of batteries-included.
4141

4242
To specify a build step, add it under `build.steps` in your configuration. A build step should contain a list of requirements under `requires` and a `command`:
4343

@@ -122,18 +122,18 @@ command = "php -f payment.php"
122122

123123
## Build Requirements
124124

125-
As you've seen above, build requirements are specified via the `requires` value. Out of the box, view.py supports a number of different build tools, compilers, and interpreters. To specify a requirement for one, simply add the name of their executable (*i.e.*, how you access their CLI). For example, since `pip` is accessed via using the `pip` command in your terminal, `pip` is the name of the requirement.
125+
As you've seen above, build requirements are specified via the `requires` value. Out of the box, view.py supports a number of different build tools, compilers, and interpreters. To specify a requirement for one, simply add the name of their executable (_i.e._, how you access their CLI). For example, since `pip` is accessed via using the `pip` command in your terminal, `pip` is the name of the requirement.
126126

127127
However, view.py might not support checking for a command by default (this is the case if you get a `Unknown build requirement` error). If so, you need a custom requirement. If you would like to, you can make an [issue](https://github.com/ZeroIntensity/view.py/issues) requesting support for it as well.
128128

129129
### Custom Requirements
130130

131131
There are four types of custom requirements, which are specified by adding a prefix to the requirement name:
132132

133-
- Importing a Python module (`mod+`)
134-
- Executing a Python script (`script+`)
135-
- Checking if a path exists (`path+`)
136-
- Checking if a command exists (`command+`)
133+
- Importing a Python module (`mod+`)
134+
- Executing a Python script (`script+`)
135+
- Checking if a path exists (`path+`)
136+
- Checking if a command exists (`command+`)
137137

138138
For example, the `command+gcc` would make sure that `gcc --version` return `0`:
139139

@@ -163,13 +163,12 @@ async def __view_requirement__() -> bool:
163163
return sys.version_info >= (3, 10)
164164
```
165165

166-
The above could actually be used via both `script+check_310.py` and `mod+check_310`.
166+
The above could actually be used via both `script+check_310.py` and `mod+check_310`.
167167

168168
!!! tip
169169

170170
Don't use the view.py build system to check the Python version or if a Python package is installed. Instead, use the `dependencies` section of a `pyproject.toml` file, or [PEP 723](https://peps.python.org/pep-0723/) script metadata.
171171

172-
173172
## Review
174173

175174
View can build static HTML with the `view build` command, or via `view.build.build_app`. Build steps in view.py are used to call external build systems, which can then in turn be used to build things your app needs at runtime (such as static HTML generated by [Next.js](https://nextjs.org)). Builds can run commands, Python scripts, or both.

src/_view/app.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,7 @@ static int lifespan(PyObject* awaitable, PyObject* result) {
155155
&self,
156156
NULL,
157157
&receive,
158-
&send,
159-
NULL
158+
&send
160159
) < 0)
161160
return -1;
162161

@@ -308,6 +307,18 @@ static PyObject* app(
308307
return NULL;
309308
}
310309

310+
if (PyAwaitable_SaveValues(
311+
awaitable,
312+
4,
313+
self,
314+
scope,
315+
receive,
316+
send
317+
) < 0) {
318+
Py_DECREF(awaitable);
319+
return NULL;
320+
}
321+
311322
if (PyAwaitable_AddAwait(
312323
awaitable,
313324
recv_coro,

src/view/build.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ async def _call_script(path: Path, *, call_func: str | None = None) -> None:
5252
if call_func:
5353
func = globls.get(call_func)
5454
if func:
55-
await func()
56-
55+
try:
56+
await func()
57+
except Exception as e:
58+
raise BuildError(f"Script at {path} raised exception!") from e
5759

5860
_COMMAND_REQS = [
5961
# C
@@ -163,6 +165,9 @@ async def _check_requirement(req: str) -> None:
163165
elif prefix == "path":
164166
if not Path(target).exists():
165167
raise MissingRequirementError(f"{target} does not exist")
168+
elif prefix == "command":
169+
if not await _check_version_command(target):
170+
raise MissingRequirementError(f"{target} is not installed")
166171
else:
167172
raise BuildError(f"Invalid requirement prefix: {prefix}")
168173

@@ -189,9 +194,9 @@ async def _build_step(step: _BuildStepWithName) -> None:
189194
if data.script:
190195
if isinstance(data.script, list):
191196
for script in data.script:
192-
await _call_script(script)
197+
await _call_script(script, call_func="__view_build__")
193198
else:
194-
await _call_script(data.script)
199+
await _call_script(data.script, call_func="__view_build__")
195200

196201

197202
async def run_step(app: App, name: str) -> None:

src/view/routing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ async def wrap_step(app: App, step: str) -> None:
8686
await run_step(app, step)
8787
except Exception as e:
8888
Service.exception(e)
89+
raise e
8990

9091

9192
@dataclass

tests/configs/build_commands.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build]
2+
default_steps = ["test"]
3+
4+
[build.steps.test]
5+
requires = ["command+python3"]
6+
command = "python3 -c 'print()'"
7+
8+
[build.steps.fail]
9+
requires = ["command+fdsafasdjfkhas"]
10+
command = "exit -1"

tests/configs/build_scripts.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[build]
2+
default_steps = ["myscript"]
3+
4+
[build.steps.myscript]
5+
script = ["./tests/build/my_build_script.py"]
6+
7+
[build.steps.fail]
8+
script = "./tests/build/failing_build_script.py"

tests/test_build.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
from contextlib import redirect_stdout
3+
from io import StringIO
14
from pathlib import Path
25

36
import pytest
@@ -19,8 +22,44 @@ async def index():
1922

2023
@app.get("/foo", steps=("foo",))
2124
async def wont_work():
22-
raise RuntimeError("shouldn't be here!")
25+
return "shouldn't be here"
2326

2427
async with app.test() as test:
2528
assert (await test.get("/")).message != ""
2629
assert (await test.get("/foo")).status == 500
30+
31+
32+
@pytest.mark.asyncio
33+
async def test_build_scripts():
34+
app = new_app(
35+
config_path=Path.cwd() / "tests" / "configs" / "build_scripts.toml"
36+
)
37+
38+
called = False
39+
40+
@app.get("/", steps=("fail",))
41+
async def index():
42+
nonlocal called
43+
called = True
44+
return "..."
45+
46+
async with app.test() as test:
47+
assert "_VIEW_TEST_BUILD_SCRIPT" in os.environ
48+
assert (await test.get("/")).status == 500
49+
assert not called
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_build_commands():
54+
app = new_app(
55+
config_path=Path.cwd() / "tests" / "configs" / "build_commands.toml"
56+
)
57+
58+
@app.get("/", steps=["fail"])
59+
async def fail():
60+
return "."
61+
62+
buffer = StringIO()
63+
with redirect_stdout(buffer):
64+
async with app.test() as test:
65+
assert (await test.get("/")).status == 500

0 commit comments

Comments
 (0)