diff --git a/Makefile b/Makefile index 88ab02e..6d4f87d 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -29,15 +30,17 @@ WASILESS_WASM := $(WASILESS_ROOT)/wasiless.wasm # Default target builds all examples all: $(COMPOSED_WASMS) +$(STUBS_DIR): $(COMPUTE_WIT) + 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): @@ -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 . diff --git a/examples/bottle-app.py b/examples/bottle-app.py index 3f96df1..c0a1e40 100644 --- a/examples/bottle-app.py +++ b/examples/bottle-app.py @@ -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 @@ -15,11 +17,13 @@ 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", @@ -27,7 +31,7 @@ def info(): "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), } diff --git a/examples/game-of-life.py b/examples/game-of-life.py index b5bb860..2544661 100644 --- a/examples/game-of-life.py +++ b/examples/game-of-life.py @@ -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 diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index d8cd07d..43c3a38 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -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: @@ -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 @@ -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() @@ -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 @@ -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 @@ -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) def post(self, path: str, **kwargs) -> requests.Response: """Make a POST request to the viceroy server. @@ -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. diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index 56b7629..f84fd5d 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -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()) send_downstream(error_response, error_body) diff --git a/pyproject.toml b/pyproject.toml index 8617b33..c68dac0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ test = [ ] dev = [ "ruff (>=0.12.11,<0.13.0)", + "pyrefly (>=0.45.1,<0.46.0)", ] [tool.pytest.ini_options] @@ -64,3 +65,7 @@ build-backend = "setuptools.build_meta" [tool.setuptools] py-modules = ["app"] + +[tool.pyrefly] +python-version = "3.12" +search-path = ["stubs"] diff --git a/uv.lock b/uv.lock index 9eaa403..e3de0f5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [[package]] @@ -132,6 +132,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "pyrefly" }, { name = "ruff" }, ] test = [ @@ -144,6 +145,7 @@ requires-dist = [ { name = "bottle" }, { name = "componentize-py", specifier = ">=0.18.0,<0.19.0" }, { name = "flask" }, + { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.45.1,<0.46.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.0,<9.0.0" }, { name = "requests", marker = "extra == 'test'", specifier = ">=2.32.5,<3.0.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.11,<0.13.0" }, @@ -296,6 +298,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyrefly" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/7f/2a6f3921b72965eee3cae8c2bea4c31e058cc9319fed6bf1df290245c2f2/pyrefly-0.45.1.tar.gz", hash = "sha256:7e7a26622cac19359748d1837e97a9df6340cfe025de3cf8ff879572a66d5008", size = 4201517, upload-time = "2025-12-09T17:47:03.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/70/17fcb2430933096d28addd1130453072ca8a4de1abf35c0887ff6e49bec0/pyrefly-0.45.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:46a3ab3abdd35f52d574fc8a1b9b8c8620370ec2f32bf09e4f20417f908681c7", size = 10265014, upload-time = "2025-12-09T17:46:46.757Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/cc98402494027db16eea74eff1e8b9448a1664c51e37c4ee6e5572d9c0fb/pyrefly-0.45.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:90938349b700383e0e001cefccd963062ec1c3ebd592186efde2cca7d9a794c3", size = 9854410, upload-time = "2025-12-09T17:46:49.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/b2/73f14894e116daf086769ad7d575c286f04710938b0c7be55a7434fa3c28/pyrefly-0.45.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad99b9886bc042c1c30bc9a145185365d8ab6f8e82e2ac2303a6668beab32215", size = 10100376, upload-time = "2025-12-09T17:46:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ea/449b0300630d16bd13723091643bd72906cebb6e2c3599bd97e5302536e9/pyrefly-0.45.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3f4be006e554478aa13a37f6ce7a6f6ef873fc50679d469d0e6ca6c2d42e58", size = 10948733, upload-time = "2025-12-09T17:46:52.971Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c4/3a9bb251e05ff878fb9d13e427ba16546a603a6a7f363a7ebff8d664fb5d/pyrefly-0.45.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bffd2d55ba034cc793bac49d2b2393b677148cb922b1ec5b0b48d48239791505", size = 10590163, upload-time = "2025-12-09T17:46:55.07Z" }, + { url = "https://files.pythonhosted.org/packages/3d/da/3bdf07e9a85265c734e8c7d9138cb258688c87204b2fec76caa7fe7b3751/pyrefly-0.45.1-py3-none-win32.whl", hash = "sha256:553a99ad8d77dbaa82ef11265bed909b4afdd26b2b9e2a549bb8fb319044b668", size = 10018839, upload-time = "2025-12-09T17:46:57.017Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bc/78eb5d4ba3685801874855dad075e23474847ddb849c3d97358c89cef32b/pyrefly-0.45.1-py3-none-win_amd64.whl", hash = "sha256:af8ebed50728b0206c979761ebbcf71d37593dd103e3101be30d2fdaeab2b89b", size = 10683163, upload-time = "2025-12-09T17:46:59.288Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/e5b5afc821165399a2aeb575248ab1b60dd943b45c6cfb0c25c9c7a89a01/pyrefly-0.45.1-py3-none-win_arm64.whl", hash = "sha256:43727bd75c7b4dcd25e00a478b22a5b7e61e920b8824045acb6e2c71bc2cdd24", size = 10248551, upload-time = "2025-12-09T17:47:01.751Z" }, +] + [[package]] name = "pytest" version = "8.4.2"