Skip to content

Commit 83038db

Browse files
authored
Make exceptions more idiomatic. Fix #19. (#30)
2 parents 4b7c5f9 + 1894e1f commit 83038db

18 files changed

Lines changed: 779 additions & 81 deletions

File tree

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Python/uv artifacts
22
.venv/
33
*.egg-info
4+
__pycache__
45

6+
# Generated code
57
/stubs/
6-
__pycache__
8+
/fastly_compute/exceptions/*
9+
!/fastly_compute/exceptions/__init__.py
10+
/fastly_compute/runtime_patching/patches.py
711

812
# Build artifacts
913
/build/
@@ -12,4 +16,3 @@ bin/
1216
# Rust
1317
target/
1418
*.so
15-
target/

Makefile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ $(STUBS_DIR): $(COMPUTE_WIT)
5252
uv run componentize-py -d wit --world-module wit_world -w $(TARGET_WORLD) bindings $(STUBS_DIR)
5353

5454
# Build our composed wasm using fastly-compute-py build
55-
$(BUILD_DIR)/%.composed.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py | $(BUILD_DIR) $(STUBS_DIR)
55+
$(BUILD_DIR)/%.composed.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py fastly_compute/runtime_patching/patches.py | $(BUILD_DIR) $(STUBS_DIR)
5656
@echo "Building $* example with fastly-compute-py..."
5757
@test -d $(EXAMPLES_DIR)/$* || (echo "Error: Example directory $(EXAMPLES_DIR)/$* not found" && exit 1)
5858
@test -f $(EXAMPLES_DIR)/$*/$*.py || (echo "Error: Example file $(EXAMPLES_DIR)/$*/$*.py not found" && exit 1)
5959
cd $(EXAMPLES_DIR)/$* && $(FASTLY_COMPUTE_PY) build --output ../../$@
6060

61+
# The script that writes the exceptions and the patches always rewrites
62+
# everything, so we can depend on the mod date of only 1 file. We choose
63+
# patches.py, because its name doesn't depend on the WIT contents.
64+
fastly_compute/runtime_patching/patches.py: scripts/generate_patches/*.py $(COMPUTE_WIT)
65+
uv run python -m scripts.generate_patches
66+
6167
# Create build directory
6268
$(BUILD_DIR):
6369
mkdir -p $(BUILD_DIR)
@@ -83,10 +89,12 @@ list-examples:
8389
# Clean build artifacts
8490
clean:
8591
rm -rf $(BUILD_DIR) $(STUBS_DIR)
92+
rm -f fastly_compute/runtime_patching/patches.py
93+
cd fastly_compute/exceptions && rm -rf acl http_body http_req kv_store types
8694
cd crates/fastly-compute-py && cargo clean
8795

8896
# Development tools
89-
lint: | $(STUBS_DIR)
97+
lint: fastly_compute/runtime_patching/patches.py | $(STUBS_DIR)
9098
@echo "Checking version synchronization..."
9199
uv run python scripts/check_version_sync.py
92100
@echo "Linting Python code..."
@@ -95,7 +103,7 @@ lint: | $(STUBS_DIR)
95103
@echo "Linting Rust code..."
96104
cd crates/fastly-compute-py && cargo clippy -- -D warnings
97105

98-
lint-fix:
106+
lint-fix: fastly_compute/runtime_patching/patches.py
99107
@echo "Fixing Python code..."
100108
uv run --extra dev ruff check --fix .
101109
@echo "Fixing Rust code..."

fastly_compute/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@
55

66
# Testing utilities are available but not imported by default
77
# Users can import them explicitly: from fastly_compute.testing import ViceroyTestBase
8+
9+
from fastly_compute.runtime_patching.patches import patch
10+
11+
# Before anything from the fastly_compute package is used, do our monkeypatching
12+
# to make the WIT-generated code act more Pythonically:
13+
patch()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Top-level exceptions emitted by the Fastly API"""
2+
3+
4+
class FastlyError(Exception):
5+
"""Abstract base class for all errors raised by Fastly APIs
6+
7+
This allows catching all errors emanating from Fastly APIs at once.
8+
"""
9+
10+
11+
class UnexpectedFastlyError(FastlyError):
12+
"""An error arising from a Fastly API but of an unanticipated kind, such
13+
that we merely package up the low-level error and send it along.
14+
15+
Any of these encountered in the wild means we neglected to keep our Python
16+
wrappers up to date with the WIT.
17+
"""
18+
19+
def __init__(self, error_value: object):
20+
"""Construct.
21+
22+
:arg error_value: The ``value`` attr of the raised ``Err``
23+
"""
24+
self.value = error_value
25+
26+
27+
# I went with the exact verbatim names of the error cases, not appending "Error"
28+
# to the ends of the ones that didn't have it to make them strictly conform to
29+
# Python conventions. "except HttpInvalid" reads fine to me.

fastly_compute/requests/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@
4141
import urllib.parse
4242
from typing import Any, TypedDict, Unpack
4343

44-
from componentize_py_types import Err
4544
from wit_world.imports import http_body, http_req
4645

46+
from fastly_compute.exceptions.http_req import ErrorWithDetail
47+
from fastly_compute.exceptions.types.error import Error
4748
from fastly_compute.requests.backend import resolve_backend
4849

4950
from .exceptions import (
@@ -241,8 +242,8 @@ def request(
241242
wit_request = http_req.Request.new()
242243
wit_request.set_method(method.upper())
243244
wit_request.set_uri(url_parsed.geturl())
244-
except Err as e:
245-
raise RequestException.from_wit_error(e, "create_req") from e
245+
except Error as e:
246+
raise RequestException.from_fastly_error(e, "create_req") from e
246247

247248
# Set headers
248249
headers = headers if headers is not None else {}
@@ -270,8 +271,8 @@ def request(
270271
for name, value in headers.items():
271272
try:
272273
wit_request.insert_header(name, value.encode("utf-8"))
273-
except Err as e:
274-
raise RequestException.from_wit_error(e, "insert_header") from e
274+
except Error as e:
275+
raise RequestException.from_fastly_error(e, "insert_header") from e
275276

276277
# Prepare request body
277278
wit_body = http_body.new()
@@ -280,17 +281,17 @@ def request(
280281
written = 0
281282
while written < len(body):
282283
written += http_body.write(wit_body, body)
283-
except Err as e:
284-
raise RequestException.from_wit_error(e, "http_body.write") from e
284+
except Error as e:
285+
raise RequestException.from_fastly_error(e, "http_body.write") from e
285286

286287
# Send the request
287288
try:
288289
wit_response, response_body = http_req.send(
289290
wit_request, wit_body, resolution.backend
290291
)
291-
except Err as e:
292+
except ErrorWithDetail as e:
292293
# WIT-level errors during request execution - use proper error classification
293-
raise RequestException.from_http_req_error(e, "http_req.send") from e
294+
raise RequestException.from_detailed_error(e, "http_req.send") from e
294295

295296
# Wrap in FastlyResponse
296297
return FastlyResponse(wit_response, response_body, url_parsed.geturl())

fastly_compute/requests/backend.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
from dataclasses import dataclass
1111
from typing import TYPE_CHECKING
1212

13-
from componentize_py_types import Err
1413
from wit_world.imports import backend as wit_backend
15-
from wit_world.imports.types import OpenError
14+
15+
from fastly_compute.exceptions.types.error import Error
16+
from fastly_compute.exceptions.types.open_error import OpenError
1617

1718
from .exceptions import MissingSchema, RequestException
1819

@@ -65,14 +66,10 @@ def resolve_backend(
6566
# Check if backend exists by trying to open it
6667
try:
6768
backend_obj = wit_backend.Backend.open(fastly_backend)
68-
except Err as e:
69-
# Check if this is an OpenError (backend not found)
70-
if isinstance(e.value, OpenError):
71-
raise RequestException(
72-
f"Static backend '{fastly_backend}' does not exist"
73-
) from e
74-
# Re-raise if it's a different error
75-
raise
69+
except OpenError as e:
70+
raise RequestException(
71+
f"Static backend '{fastly_backend}' does not exist"
72+
) from e
7673
else:
7774
# dynamic backend
7875
if not parsed.scheme or not parsed.netloc:
@@ -117,5 +114,5 @@ def _register_dynamic_backend(
117114
return wit_backend.register_dynamic_backend(
118115
prefix=backend_name, target=parsed_url.netloc, options=options
119116
)
120-
except Err as e:
121-
raise RequestException.from_wit_error(e, "register_dynamic_backend") from e
117+
except Error as e:
118+
raise RequestException.from_fastly_error(e, "register_dynamic_backend") from e

fastly_compute/requests/exceptions.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@
66
from typing import TYPE_CHECKING
77

88
if TYPE_CHECKING:
9-
from componentize_py_types import Err as WitErr
10-
119
from .response import FastlyResponse
1210

1311
# Runtime imports needed for error mappings at module level
1412
from wit_world.imports import http_req
1513
from wit_world.imports import types as wit_types
1614
from wit_world.imports.http_req import SendErrorDetail
1715

16+
from fastly_compute.exceptions import FastlyError
17+
from fastly_compute.exceptions.http_req import ErrorWithDetail
18+
from fastly_compute.exceptions.types.error import (
19+
CannotRead,
20+
HttpHeadTooLarge,
21+
HttpIncomplete,
22+
HttpInvalid,
23+
HttpInvalidStatus,
24+
HttpUser,
25+
)
26+
1827

1928
def _map_error_to_exception(
2029
error: object,
@@ -59,19 +68,19 @@ def __init__(
5968
self.request: http_req.Request | None = request
6069

6170
@classmethod
62-
def from_http_req_error(
63-
cls, err: WitErr[http_req.ErrorWithDetail], operation: str
71+
def from_detailed_error(
72+
cls, err: ErrorWithDetail, operation: str
6473
) -> RequestException:
65-
"""Create appropriate exception from http_req WIT error.
74+
"""Create a ``requests`` exception from an ErrorWithDetail.
6675
6776
Args:
68-
err: WIT Err exception containing ErrorWithDetail
77+
err: The error to map from
6978
operation: Description of what operation failed
7079
7180
Returns:
7281
Appropriate RequestException subclass instance
7382
"""
74-
error_with_detail = err.value
83+
error_with_detail = err.args[0]
7584

7685
# Try detailed error classification first; this is not guaranteed
7786
# to be present in all cases.
@@ -92,21 +101,19 @@ def from_http_req_error(
92101
)
93102

94103
@classmethod
95-
def from_wit_error(
96-
cls, err: WitErr[wit_types.Error], operation: str
97-
) -> RequestException:
98-
"""Create appropriate exception from generic WIT error.
104+
def from_fastly_error(cls, err: FastlyError, operation: str) -> RequestException:
105+
"""Create a ``requests`` exception from a FastlyError or subclass.
99106
100107
Args:
101-
err: WIT Err exception containing generic Error
108+
err: The error to map from
102109
operation: Description of what operation failed
103110
104111
Returns:
105112
Appropriate RequestException subclass instance
106113
"""
107114
return _map_error_to_exception(
108-
err.value,
109-
WIT_ERROR_MAPPINGS,
115+
err,
116+
FASTLY_ERROR_MAPPINGS,
110117
f"Operation {operation} failed",
111118
cls,
112119
)
@@ -200,3 +207,17 @@ class StreamConsumedError(RequestException, TypeError):
200207
}
201208
)
202209
)
210+
211+
# Map FastlyErrors to the errors `requests` returns.
212+
FASTLY_ERROR_MAPPINGS: MappingProxyType[type[FastlyError], type[RequestException]] = (
213+
MappingProxyType(
214+
{
215+
HttpInvalid: HTTPError,
216+
HttpUser: HTTPError,
217+
HttpIncomplete: HTTPError,
218+
HttpHeadTooLarge: HTTPError,
219+
HttpInvalidStatus: HTTPError,
220+
CannotRead: ConnectionError,
221+
}
222+
)
223+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Monkeypatches (and supporting machinery) which make WIT behavior more Pythonic"""

fastly_compute/exceptions.py renamed to fastly_compute/runtime_patching/decorators.py

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,15 @@
1-
"""Top-level exceptions emitted by the Fastly API"""
1+
"""Decorators used in runtime patching"""
22

33
from collections.abc import Callable, Mapping
44
from enum import Enum
55
from functools import wraps
66
from typing import Any
77

8-
from wit_world.imports.types import Err
8+
from componentize_py_types import Err
99

10+
from fastly_compute.exceptions import FastlyError, UnexpectedFastlyError
1011

11-
class FastlyError(Exception):
12-
"""Abstract base class for all errors raised by Fastly APIs
1312

14-
This allows catching all errors emanating from Fastly APIs at once.
15-
"""
16-
17-
18-
class UnexpectedFastlyError(FastlyError):
19-
"""An error arising from a Fastly API but of an unanticipated kind, such
20-
that we merely package up the low-level error and send it along.
21-
22-
Any of these encountered in the wild means we neglected to keep our Python
23-
wrappers up to date with the WIT.
24-
"""
25-
26-
def __init__(self, error_value: object):
27-
"""Construct.
28-
29-
:arg error_value: The ``value`` attr of the raised ``Err``
30-
"""
31-
self.value = error_value
32-
33-
34-
# TODO: Move to somewhere more private once it becomes clear where.
3513
def remap_wit_errors(
3614
idiomatic_exceptions: Mapping[Any, type[FastlyError]] | None = None,
3715
) -> Callable:

fastly_compute/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Utility functions for fastly_compute package."""
22

3-
from componentize_py_types import Err
43
from wit_world.imports import async_io, http_body
54

5+
from fastly_compute.exceptions.types.error import Error
66
from fastly_compute.requests.exceptions import RequestException
77

88

@@ -25,8 +25,8 @@ def read_response_body(
2525
while True:
2626
try:
2727
chunk = http_body.read(response_body, chunk_size)
28-
except Err as e:
29-
raise RequestException.from_wit_error(e, "http_body.read") from e
28+
except Error as e:
29+
raise RequestException.from_fastly_error(e, "http_body.read") from e
3030

3131
if len(chunk) == 0:
3232
break

0 commit comments

Comments
 (0)