From 989ecf57200c850b279e14d93b41d8f67e180658 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Wed, 10 Dec 2025 13:52:11 -0600 Subject: [PATCH 1/5] Add pyrefly type checker & lsp support (and fix defects) Pyrefly is fast and the defaults seem to be a bit less aggressive than basedpyright. Ty is another option but it doesn't seem to be as mature, potentially. This adds pyrefly and introduces changes in order to get a clean run via `make check`. It does seem to have found a few bugs in my early testing; I stumbled upon it as it was an LSP option for zed. One thing it did seem to catch was the session reuse bug already fixed in upstream where we weren't checking await_request for None. --- Makefile | 3 ++- examples/game-of-life.py | 2 +- fastly_compute/testing.py | 28 +++++++++++++--------------- fastly_compute/wsgi.py | 2 +- pyproject.toml | 5 +++++ uv.lock | 20 +++++++++++++++++++- 6 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 88ab02e..56b2310 100644 --- a/Makefile +++ b/Makefile @@ -69,8 +69,9 @@ clean: rm -rf $(BUILD_DIR) $(STUBS_DIR) # Development tools -lint: +lint: $(EXAMPLE_WASMS) 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/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..9df8750 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 @@ -153,11 +159,11 @@ def capture_output_thread(): ) server = ViceroyServer( - process=process, base_url=base_url, output_lines=output_lines + process=process, base_url=base_url, output_lines=list(output_lines) ) # 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" From 8502f7a71447cca52d4ef495c112d6e5896938b3 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 16 Dec 2025 10:22:26 -0600 Subject: [PATCH 2/5] Model stubs generation in Makefile Previously, we fully removed and regenerated stubs for a bunch of stuff. Now, only do so based on changes to compute.wit which should generally be a good proxy. This also makes the lint checks much lighter by not requiring the we build all the examples just to get stubs. --- Makefile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 56b2310..ec38f35 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: $(STUBS_DIR) $(EXAMPLES_DIR)/%.py wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py | $(BUILD_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,7 +72,7 @@ clean: rm -rf $(BUILD_DIR) $(STUBS_DIR) # Development tools -lint: $(EXAMPLE_WASMS) +lint: $(STUBS_DIR) uv run --extra dev ruff check . uv run --extra dev --extra test pyrefly check . From 929f1bceae43ff351fe2d1597b9333c931d91189 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 16 Dec 2025 10:47:44 -0600 Subject: [PATCH 3/5] Fix type checking in bottle example This is a case where pyrefly was inferring the type incorrectly. Interestingly, I found that this was also the case with mypy and pyright as well, so just adding the cast. Other frameworks seem to have better support for typing than bottle. --- examples/bottle-app.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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), } From e0938c22dd00ba7cff06033bbcb150c6551421df Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 16 Dec 2025 16:17:06 -0600 Subject: [PATCH 4/5] Remove extraneous list() call on list type --- fastly_compute/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastly_compute/testing.py b/fastly_compute/testing.py index 9df8750..43c3a38 100644 --- a/fastly_compute/testing.py +++ b/fastly_compute/testing.py @@ -159,7 +159,7 @@ def capture_output_thread(): ) server = ViceroyServer( - process=process, base_url=base_url, output_lines=list(output_lines) + process=process, base_url=base_url, output_lines=output_lines ) # Set the server as a class attribute so methods can access it From 79eda457324b6a76490319be8bdac9bb827eaa2f Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 16 Dec 2025 17:23:09 -0600 Subject: [PATCH 5/5] Model stubs directory as order-only This is a feawture of GNU make designed and documented to help with directories and similar. See https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ec38f35..6d4f87d 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ $(BUILD_DIR)/%.composed.wasm: $(BUILD_DIR)/%.wasm $(WASILESS_WASM) 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: $(STUBS_DIR) $(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..." uv run componentize-py -d wit -w $(TARGET_WORLD) componentize $* -p $(EXAMPLES_DIR) -p . -o $@ @@ -72,7 +72,7 @@ clean: rm -rf $(BUILD_DIR) $(STUBS_DIR) # Development tools -lint: $(STUBS_DIR) +lint: | $(STUBS_DIR) uv run --extra dev ruff check . uv run --extra dev --extra test pyrefly check .