Skip to content
Merged
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
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ VICEROY ?= viceroy
STUBS_DIR := stubs
BUILD_DIR := build
EXAMPLES_DIR := examples
COMPUTE_WIT := wit/deps/fastly/compute.wit

# Define all available examples (add new ones here)
EXAMPLES := bottle-app flask-app game-of-life
Expand All @@ -29,15 +30,17 @@ WASILESS_WASM := $(WASILESS_ROOT)/wasiless.wasm
# Default target builds all examples
all: $(COMPOSED_WASMS)

$(STUBS_DIR): $(COMPUTE_WIT)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might I venture to suggest we instead use stubs/wit_world/__init__.py as the target? Using a dir as a target tends toward fragility, as adding or removing anything from the dir updates its modification date. That includes OS detritus like .DS_Store and things I lack the foresight to enumerate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I'll do that in some way. I recall similar problems now that you mention it (and never really having a solution I'm entirely happy with).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to use Make targets to create virtualenvs. One strategy I'd use is to make the venv, then immediately touch some pointless little file inside it as a sentinel. The target would then be that sentinel. Avoids the dir modtime problem and doesn't assume anything about the eventual population of the folder.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like another option, which we employ for BUILD_DIR is a https://www.gnu.org/software/make/manual/make.html#Prerequisite-Types

I will go ahead and use that as it seems to be designed and documented for this sort of case specifically.

Copy link
Copy Markdown
Member

@erikrose erikrose Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though BUILD_DIR's only care is to create an empty dir. Stubs, OTOH, can go out of date, and we want them rebuilt if compute.wit changes. (I guess what I'm saying is that I wonder if | is transitive.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a change using | now, though I just saw the updates here concurrently.

Doing a test, I think the behavior is what we want:

$ make clean

$ make stubs
rm -rf stubs
uv run componentize-py -d wit -w fastly:compute/service bindings stubs

$ touch wit/deps/fastly/compute.wit
$ make stubs
rm -rf stubs
uv run componentize-py -d wit -w fastly:compute/service bindings stubs

# depends on stubs as `|` but we didn't regen after modifying a file
# within stubs
$ touch stubs/foo.txt
$ make lint
uv run --extra dev ruff check .
All checks passed!
uv run --extra dev --extra test pyrefly check .
 INFO 0 errors (1 suppressed)

# But we do regen if stubs was actually modified
$ touch wit/deps/fastly/compute.wit
$ make lint
rm -rf stubs
uv run componentize-py -d wit -w fastly:compute/service bindings stubs
uv run --extra dev ruff check .
All checks passed!
uv run --extra dev --extra test pyrefly check .
 INFO 0 errors (1 suppressed)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to merge; if we see cases where it isn't working as we want, we can revisit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! Looks great!

rm -rf $(STUBS_DIR)
uv run componentize-py -d wit -w $(TARGET_WORLD) bindings $(STUBS_DIR)

$(BUILD_DIR)/%.composed.wasm: $(BUILD_DIR)/%.wasm $(WASILESS_WASM)
@echo "Composing $* example"
wac compose --dep fastly:wasiless=$(WASILESS_WASM) --dep app:component=$< -o $@ wrap_app_in_wasiless.wac

# Pattern rule for building any example
$(BUILD_DIR)/%.wasm: $(EXAMPLES_DIR)/%.py wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py | $(BUILD_DIR)
$(BUILD_DIR)/%.wasm: $(EXAMPLES_DIR)/%.py wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py | $(BUILD_DIR) $(STUBS_DIR)
@echo "Building $* example..."
rm -rf $(STUBS_DIR)
uv run componentize-py -d wit -w $(TARGET_WORLD) bindings $(STUBS_DIR)
uv run componentize-py -d wit -w $(TARGET_WORLD) componentize $* -p $(EXAMPLES_DIR) -p . -o $@

$(WASILESS_WASM):
Expand Down Expand Up @@ -69,8 +72,9 @@ clean:
rm -rf $(BUILD_DIR) $(STUBS_DIR)

# Development tools
lint:
lint: | $(STUBS_DIR)
uv run --extra dev ruff check .
uv run --extra dev --extra test pyrefly check .

lint-fix:
uv run --extra dev ruff check --fix .
Expand Down
12 changes: 8 additions & 4 deletions examples/bottle-app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from bottle import Bottle
import typing

from bottle import Bottle, request
from wit_world.imports import compute_runtime

from fastly_compute.wsgi import WsgiHttpIncoming
Expand All @@ -15,19 +17,21 @@ def hello(name):
@app.route("/info")
def info():
"""Return JSON with request information we can test against"""
from bottle import request

# Get some runtime info we can test
vcpu_time = compute_runtime.get_vcpu_ms()

# type checker doesn't quite understand bottle's DictProperty
# so we need to give it a hint.
headers = typing.cast(typing.Mapping[str, str], request.headers)

return {
"service": "fastly-compute-python",
"status": "ok",
"message": "Hello from Fastly Compute!",
"vcpu_time_ms": vcpu_time,
"request_method": request.environ.get("REQUEST_METHOD"),
"path_info": request.environ.get("PATH_INFO"),
"request_headers": dict(request.headers),
"request_headers": dict(headers),
}


Expand Down
2 changes: 1 addition & 1 deletion examples/game-of-life.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,4 @@ def root():


if running_under_compute:
HttpIncoming = WsgiHttpIncoming(app, reuse_sandboxes_for_ms=300)
HttpIncoming = WsgiHttpIncoming(app, reuse_sandboxes_for_ms=300) # type: ignore
26 changes: 12 additions & 14 deletions fastly_compute/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ def test_my_endpoint(self):

REQUEST_TIMEOUT = 10
WASM_FILE = "build/bottle-app.composed.wasm" # Default to the main example
server: ViceroyServer = None # Will be set by the fixture
_server: ViceroyServer | None = None # Will be set by the fixture

@property
def server(self) -> ViceroyServer:
assert self._server is not None
return self._server

@staticmethod
def _find_free_port() -> int:
Expand All @@ -64,7 +69,7 @@ def _find_free_port() -> int:

@pytest.fixture(scope="class", autouse=True)
@classmethod
def viceroy_server(cls) -> ViceroyServer:
def viceroy_server(cls):
"""Start viceroy server for the duration of the test class.

Note: This assumes the WASM file already exists. Use your build system
Expand All @@ -85,7 +90,7 @@ def viceroy_server(cls) -> ViceroyServer:
# Find an available port
port = cls._find_free_port()
base_url = f"http://127.0.0.1:{port}"
output_lines = [] # Capture all output for debugging
output_lines: list[str] = [] # Capture all output for debugging
output_lock = threading.Lock()
stop_capture = threading.Event()

Expand All @@ -107,6 +112,7 @@ def viceroy_server(cls) -> ViceroyServer:
# Start background thread to continuously capture output
def capture_output_thread():
"""Continuously capture viceroy output throughout test execution."""
assert process.stdout is not None, "stdout should be PIPE"
while not stop_capture.is_set():
line = process.stdout.readline()
if not line: # EOF
Expand Down Expand Up @@ -157,7 +163,7 @@ def capture_output_thread():
)

# Set the server as a class attribute so methods can access it
cls.server = server
cls._server = server

yield server

Expand All @@ -181,11 +187,7 @@ def get(self, path: str, **kwargs) -> requests.Response:
Returns:
requests.Response: The HTTP response
"""
timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT)
response = requests.get(
f"{self.server.base_url}{path}", timeout=timeout, **kwargs
)
return response
return self.request("GET", path, **kwargs)
Comment thread
posborne marked this conversation as resolved.

def post(self, path: str, **kwargs) -> requests.Response:
"""Make a POST request to the viceroy server.
Expand All @@ -197,11 +199,7 @@ def post(self, path: str, **kwargs) -> requests.Response:
Returns:
requests.Response: The HTTP response
"""
timeout = kwargs.pop("timeout", self.REQUEST_TIMEOUT)
response = requests.post(
f"{self.server.base_url}{path}", timeout=timeout, **kwargs
)
return response
return self.request("POST", path, **kwargs)

def request(self, method: str, path: str, **kwargs) -> requests.Response:
"""Make an HTTP request to the viceroy server.
Expand Down
2 changes: 1 addition & 1 deletion fastly_compute/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def start_response(
error_response.set_status(500)
error_response.append_header("content-type", b"text/plain")
error_message = f"Internal Server Error: {e}"
http_body.write(error_body, error_message.encode(), http_body.WriteEnd.BACK)
http_body.write(error_body, error_message.encode())
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty good example of an error caught by the type checker.

send_downstream(error_response, error_body)


Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test = [
]
dev = [
"ruff (>=0.12.11,<0.13.0)",
"pyrefly (>=0.45.1,<0.46.0)",
]

[tool.pytest.ini_options]
Expand Down Expand Up @@ -64,3 +65,7 @@ build-backend = "setuptools.build_meta"

[tool.setuptools]
py-modules = ["app"]

[tool.pyrefly]
python-version = "3.12"
search-path = ["stubs"]
20 changes: 19 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.