diff --git a/.gitignore b/.gitignore index f299143..8543346 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ __pycache__ # Generated code /stubs/ -/fastly_compute/exceptions/* -!/fastly_compute/exceptions/__init__.py -/fastly_compute/runtime_patching/patches.py # Build artifacts /build/ diff --git a/Makefile b/Makefile index 4815115..7a5c170 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ $(BUILD_DIR)/%.composed.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit fastly # The script that writes the exceptions and the patches always rewrites # everything, so we can depend on the mod date of only 1 file. We choose # patches.py, because its name doesn't depend on the WIT contents. -fastly_compute/runtime_patching/patches.py: scripts/generate_patches/*.py $(COMPUTE_WIT) +fastly_compute/runtime_patching/patches.py: scripts/generate_patches/*.py $(shell find scripts/generate_patches/templates -name "*.jinja") $(COMPUTE_WIT) uv run python -m scripts.generate_patches # Create build directory diff --git a/examples/backend-requests/uv.lock b/examples/backend-requests/uv.lock index b9290fb..88d12b0 100644 --- a/examples/backend-requests/uv.lock +++ b/examples/backend-requests/uv.lock @@ -46,4 +46,7 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] diff --git a/examples/bottle-app/uv.lock b/examples/bottle-app/uv.lock index 9d45ce9..05dbfde 100644 --- a/examples/bottle-app/uv.lock +++ b/examples/bottle-app/uv.lock @@ -46,4 +46,7 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] diff --git a/examples/flask-app/uv.lock b/examples/flask-app/uv.lock index 9f60925..1358147 100644 --- a/examples/flask-app/uv.lock +++ b/examples/flask-app/uv.lock @@ -52,7 +52,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask" diff --git a/examples/game-of-life/uv.lock b/examples/game-of-life/uv.lock index 00cb4e1..d401e93 100644 --- a/examples/game-of-life/uv.lock +++ b/examples/game-of-life/uv.lock @@ -52,7 +52,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask" diff --git a/fastly_compute/exceptions/acl/__init__.py b/fastly_compute/exceptions/acl/__init__.py new file mode 100644 index 0000000..7931326 --- /dev/null +++ b/fastly_compute/exceptions/acl/__init__.py @@ -0,0 +1,4 @@ +"""Blocklists using [Access Control Lists] (ACLs) + +[Access Control Lists]: https://www.fastly.com/documentation/reference/api/acls/ +""" diff --git a/fastly_compute/exceptions/acl/acl_error.py b/fastly_compute/exceptions/acl/acl_error.py new file mode 100644 index 0000000..1d6a73b --- /dev/null +++ b/fastly_compute/exceptions/acl/acl_error.py @@ -0,0 +1,23 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Errors returned on ACL lookup failure.""" + +from fastly_compute.exceptions import FastlyError + + +class AclError(FastlyError): + """Errors returned on ACL lookup failure.""" + + +class TooManyRequests(AclError): + """Too many requests have been made. + + This corresponds to an HTTP error code of 429, “Too Many Requests”. + """ + + +class GenericError(AclError): + """Generic error value. + + This means that some unexpected error occurred. + """ diff --git a/fastly_compute/exceptions/http_body/__init__.py b/fastly_compute/exceptions/http_body/__init__.py new file mode 100644 index 0000000..96f6454 --- /dev/null +++ b/fastly_compute/exceptions/http_body/__init__.py @@ -0,0 +1 @@ +"""HTTP bodies.""" diff --git a/fastly_compute/exceptions/http_body/trailer_error.py b/fastly_compute/exceptions/http_body/trailer_error.py new file mode 100644 index 0000000..7fb4227 --- /dev/null +++ b/fastly_compute/exceptions/http_body/trailer_error.py @@ -0,0 +1,21 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Trailers aren't available until the body has been completely transmitted, so this error +type can either indicate that the errors aren't available yet, or that an error occurred. +""" + +from fastly_compute.exceptions import FastlyError + + +class TrailerError(FastlyError): + """Trailers aren't available until the body has been completely transmitted, so this error + type can either indicate that the errors aren't available yet, or that an error occurred. + """ + + +class NotAvailableYet(TrailerError): + """The trailers aren't available yet.""" + + +class Error(TrailerError): + """An error occurred.""" diff --git a/fastly_compute/exceptions/http_req/__init__.py b/fastly_compute/exceptions/http_req/__init__.py new file mode 100644 index 0000000..eeab097 --- /dev/null +++ b/fastly_compute/exceptions/http_req/__init__.py @@ -0,0 +1,9 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""An `error` code, optionally with extra request error information.""" + +from fastly_compute.exceptions import FastlyError + + +class ErrorWithDetail(FastlyError): + """An `error` code, optionally with extra request error information.""" diff --git a/fastly_compute/exceptions/kv_store/__init__.py b/fastly_compute/exceptions/kv_store/__init__.py new file mode 100644 index 0000000..9f28322 --- /dev/null +++ b/fastly_compute/exceptions/kv_store/__init__.py @@ -0,0 +1,7 @@ +"""Interface to Fastly's [Compute KV Store]. + +For a high-level introduction to this feature, see this [blog post]. + +[Compute KV Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/data-stores/#kv-stores +[blog post]: https://www.fastly.com/blog/introducing-the-compute-edge-kv-store-global-persistent-storage-for-compute-functions +""" diff --git a/fastly_compute/exceptions/kv_store/kv_error.py b/fastly_compute/exceptions/kv_store/kv_error.py new file mode 100644 index 0000000..3adf7ff --- /dev/null +++ b/fastly_compute/exceptions/kv_store/kv_error.py @@ -0,0 +1,57 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""A value indicating the status of a KV store operation.""" + +from fastly_compute.exceptions import FastlyError + + +class KvError(FastlyError): + """A value indicating the status of a KV store operation.""" + + +class BadRequest(KvError): + """KV store cannot or will not process the request due to something that is perceived to be a + client error. + + This will map to the api's 400 codes. + """ + + +class PreconditionFailed(KvError): + """KV store cannot fulfill the request, as defined by the client's prerequisites, for example + `if-generation-match`. + + This will map to the api's 412 codes. + """ + + +class PayloadTooLarge(KvError): + """The size limit for a KV store key was exceeded. + + This will map to the api's 413 codes. + """ + + +class InternalError(KvError): + """The system encountered an unexpected internal error. + + This will map to all remaining http error codes. + """ + + +class TooManyRequests(KvError): + """Too many requests have been made to the KV store. + + This will map to the api's 429 codes. + """ + + +class GenericError(KvError): + """Generic error value. + + This means that some unexpected error occurred. + """ + + +class Extra(KvError): + """Additional error information may be added in the future via this resource type.""" diff --git a/fastly_compute/exceptions/types/__init__.py b/fastly_compute/exceptions/types/__init__.py new file mode 100644 index 0000000..025dc75 --- /dev/null +++ b/fastly_compute/exceptions/types/__init__.py @@ -0,0 +1 @@ +"""Types used by many interfaces in this package.""" diff --git a/fastly_compute/exceptions/types/error.py b/fastly_compute/exceptions/types/error.py new file mode 100644 index 0000000..d31ee0b --- /dev/null +++ b/fastly_compute/exceptions/types/error.py @@ -0,0 +1,116 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""A common error type used by many functions in this package. + +TODO: In the future this should be split up into more-specific error +enums so that it better documents which errors each function can actually +return and what they mean. +""" + +from fastly_compute.exceptions import FastlyError + + +class Error(FastlyError): + """A common error type used by many functions in this package. + + TODO: In the future this should be split up into more-specific error + enums so that it better documents which errors each function can actually + return and what they mean. + """ + + +class GenericError(Error): + """Generic error value. + + This means that some unexpected error occurred. + """ + + +class InvalidArgument(Error): + """Invalid argument.""" + + +class AuxiliaryError(Error): + """Auxiliary error value. + + For `cache.get-body` and `cache.replace-get-body`, it means the cache implementation was + busy and not ready to retrieve the body data. + + For cache APIs that attempt to write to or update the body of a cache transaction, it means + that an error occurred while attempting the write or update. + + For other cache APIs, it indicates that the underlying cache entry or cache replace entry + is no longer available. + + For writing to a streaming HTTP body, indicates that the body has already been closed. + + For a dictionary lookup, indicates that the dictionary was not found. + """ + + +class Unsupported(Error): + """Unsupported operation error. + + This error is returned when some operation cannot be performed, because it is not supported. + """ + + +class HttpInvalid(Error): + """Invalid HTTP error. + + This can be returned when a method, URI, header, or status is not valid. This can also + be returned if a message head is too large. + """ + + +class HttpUser(Error): + """HTTP user error. + + This is returned in cases where user code caused an HTTP error. For example, attempt to send + a 1xx response code, or a request with a non-absolute URI. This can also be caused by + an unexpected header: both `content-length` and `transfer-encoding`, for example. + """ + + +class HttpIncomplete(Error): + """HTTP incomplete message error. + + This can be returned when a stream ended unexpectedly. + """ + + +class CannotRead(Error): + """Cannot read. + + An error occurred while attempting to read the body of a cache transaction. + """ + + +class HttpHeadTooLarge(Error): + """Message head too large.""" + + +class HttpInvalidStatus(Error): + """Invalid HTTP status.""" + + +class LimitExceeded(Error): + """Limit exceeded + + This is returned when an attempt to allocate a resource has exceeded the maximum number of + resources permitted. For example, creating too many response handles. + """ + + +class BufferLen(Error): + """Buffer length error + + Returned when a buffer is the wrong size. + Includes the buffer length that would allow the operation to succeed. + """ + + def __init__(self, wit_error): + self.length = wit_error.value + + def __str__(self): + return f"Buffer was too short to hold the result. At least {self.length} bytes are needed." diff --git a/fastly_compute/exceptions/types/open_error.py b/fastly_compute/exceptions/types/open_error.py new file mode 100644 index 0000000..5b8acfa --- /dev/null +++ b/fastly_compute/exceptions/types/open_error.py @@ -0,0 +1,47 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""An error returned by `open`-like functions.""" + +from fastly_compute.exceptions import FastlyError + + +class OpenError(FastlyError): + """An error returned by `open`-like functions.""" + + +class InvalidSyntax(OpenError): + """The given name of the entity to open was invalid.""" + + +class NameTooLong(OpenError): + """The given name is longer the maximum permitted length.""" + + +class Reserved(OpenError): + """The given name is a reserved name that may not be opened.""" + + +class NotFound(OpenError): + """No entity by the given name was found.""" + + +class Unsupported(OpenError): + """Unsupported operation error. + + This error is returned when some operation cannot be performed, because it is not supported. + """ + + +class LimitExceeded(OpenError): + """Limit exceeded + + This is returned when an attempt to allocate a resource has exceeded the maximum number of + resources permitted. For example, creating too many response handles. + """ + + +class GenericError(OpenError): + """Generic error value. + + This means that some unexpected error occurred. + """ diff --git a/fastly_compute/runtime_patching/patches.py b/fastly_compute/runtime_patching/patches.py new file mode 100644 index 0000000..cce0296 --- /dev/null +++ b/fastly_compute/runtime_patching/patches.py @@ -0,0 +1,276 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Monkeypatches which wrap the routines generated by componentize-py to make +them raise more specific exceptions, not just Err. +""" + +try: + import wit_world.imports.acl + import wit_world.imports.backend + import wit_world.imports.cache + import wit_world.imports.config_store + import wit_world.imports.device_detection + import wit_world.imports.dictionary + import wit_world.imports.erl + import wit_world.imports.geo + import wit_world.imports.http_body + import wit_world.imports.http_cache + import wit_world.imports.http_downstream + import wit_world.imports.http_req + import wit_world.imports.http_resp + import wit_world.imports.image_optimizer + import wit_world.imports.kv_store + import wit_world.imports.log + import wit_world.imports.purge + import wit_world.imports.secret_store + import wit_world.imports.security + import wit_world.imports.shielding + import wit_world.imports.types + + import fastly_compute.exceptions.acl.acl_error + import fastly_compute.exceptions.http_body.trailer_error + import fastly_compute.exceptions.http_req + import fastly_compute.exceptions.kv_store.kv_error + import fastly_compute.exceptions.types.error + import fastly_compute.exceptions.types.open_error + + from .decorators import remap_wit_errors +except ImportError: + # Tolerate that momentary import for the testrunner before Viceroy, and thus + # the wit_world, is around. + def patch(): + """Pretend to patch.""" + print("Faking the run of exception-mapping monkeypatches for test runner.") +else: + MAPPINGS = { + wit_world.imports.acl.AclError.GENERIC_ERROR: fastly_compute.exceptions.acl.acl_error.GenericError, + wit_world.imports.acl.AclError.TOO_MANY_REQUESTS: fastly_compute.exceptions.acl.acl_error.TooManyRequests, + wit_world.imports.http_body.TrailerError_Error: fastly_compute.exceptions.http_body.trailer_error.Error, + wit_world.imports.http_body.TrailerError_NotAvailableYet: fastly_compute.exceptions.http_body.trailer_error.NotAvailableYet, + wit_world.imports.http_req.ErrorWithDetail: fastly_compute.exceptions.http_req.ErrorWithDetail, + wit_world.imports.kv_store.KvError_BadRequest: fastly_compute.exceptions.kv_store.kv_error.BadRequest, + wit_world.imports.kv_store.KvError_Extra: fastly_compute.exceptions.kv_store.kv_error.Extra, + wit_world.imports.kv_store.KvError_GenericError: fastly_compute.exceptions.kv_store.kv_error.GenericError, + wit_world.imports.kv_store.KvError_InternalError: fastly_compute.exceptions.kv_store.kv_error.InternalError, + wit_world.imports.kv_store.KvError_PayloadTooLarge: fastly_compute.exceptions.kv_store.kv_error.PayloadTooLarge, + wit_world.imports.kv_store.KvError_PreconditionFailed: fastly_compute.exceptions.kv_store.kv_error.PreconditionFailed, + wit_world.imports.kv_store.KvError_TooManyRequests: fastly_compute.exceptions.kv_store.kv_error.TooManyRequests, + wit_world.imports.types.Error_AuxiliaryError: fastly_compute.exceptions.types.error.AuxiliaryError, + wit_world.imports.types.Error_BufferLen: fastly_compute.exceptions.types.error.BufferLen, + wit_world.imports.types.Error_CannotRead: fastly_compute.exceptions.types.error.CannotRead, + wit_world.imports.types.Error_GenericError: fastly_compute.exceptions.types.error.GenericError, + wit_world.imports.types.Error_HttpHeadTooLarge: fastly_compute.exceptions.types.error.HttpHeadTooLarge, + wit_world.imports.types.Error_HttpIncomplete: fastly_compute.exceptions.types.error.HttpIncomplete, + wit_world.imports.types.Error_HttpInvalid: fastly_compute.exceptions.types.error.HttpInvalid, + wit_world.imports.types.Error_HttpInvalidStatus: fastly_compute.exceptions.types.error.HttpInvalidStatus, + wit_world.imports.types.Error_HttpUser: fastly_compute.exceptions.types.error.HttpUser, + wit_world.imports.types.Error_InvalidArgument: fastly_compute.exceptions.types.error.InvalidArgument, + wit_world.imports.types.Error_LimitExceeded: fastly_compute.exceptions.types.error.LimitExceeded, + wit_world.imports.types.Error_Unsupported: fastly_compute.exceptions.types.error.Unsupported, + wit_world.imports.types.OpenError.GENERIC_ERROR: fastly_compute.exceptions.types.open_error.GenericError, + wit_world.imports.types.OpenError.INVALID_SYNTAX: fastly_compute.exceptions.types.open_error.InvalidSyntax, + wit_world.imports.types.OpenError.LIMIT_EXCEEDED: fastly_compute.exceptions.types.open_error.LimitExceeded, + wit_world.imports.types.OpenError.NAME_TOO_LONG: fastly_compute.exceptions.types.open_error.NameTooLong, + wit_world.imports.types.OpenError.NOT_FOUND: fastly_compute.exceptions.types.open_error.NotFound, + wit_world.imports.types.OpenError.RESERVED: fastly_compute.exceptions.types.open_error.Reserved, + wit_world.imports.types.OpenError.UNSUPPORTED: fastly_compute.exceptions.types.open_error.Unsupported, + type(None): fastly_compute.exceptions.FastlyError, + } + + did_patch = False + + def patch(): + """Apply patches if they haven't already been applied.""" + global did_patch + if did_patch: + # This test shouldn't be needed, but it avoids double-wrapping the + # routines if somehow patch() did get called twice. + return + did_patch = True + wit_world.imports.log.Endpoint.open = remap_wit_errors(MAPPINGS)(wit_world.imports.log.Endpoint.open) + wit_world.imports.dictionary.Dictionary.open = remap_wit_errors(MAPPINGS)(wit_world.imports.dictionary.Dictionary.open) + wit_world.imports.dictionary.Dictionary.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.dictionary.Dictionary.lookup) + wit_world.imports.geo.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.geo.lookup) + wit_world.imports.device_detection.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.device_detection.lookup) + wit_world.imports.erl.RateCounter.open = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.open) + wit_world.imports.erl.RateCounter.check_rate = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.check_rate) + wit_world.imports.erl.RateCounter.increment = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.increment) + wit_world.imports.erl.RateCounter.lookup_rate = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.lookup_rate) + wit_world.imports.erl.RateCounter.lookup_count = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.lookup_count) + wit_world.imports.erl.PenaltyBox.open = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.open) + wit_world.imports.erl.PenaltyBox.add = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.add) + wit_world.imports.erl.PenaltyBox.has = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.has) + wit_world.imports.secret_store.Secret.from_bytes = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Secret.from_bytes) + wit_world.imports.secret_store.Secret.plaintext = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Secret.plaintext) + wit_world.imports.secret_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Store.open) + wit_world.imports.secret_store.Store.get = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Store.get) + wit_world.imports.backend.register_dynamic_backend = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.register_dynamic_backend) + wit_world.imports.backend.Backend.open = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.open) + wit_world.imports.backend.Backend.is_healthy = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_healthy) + wit_world.imports.backend.Backend.is_dynamic = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_dynamic) + wit_world.imports.backend.Backend.get_host = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_host) + wit_world.imports.backend.Backend.get_override_host = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_override_host) + wit_world.imports.backend.Backend.get_port = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_port) + wit_world.imports.backend.Backend.get_connect_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_connect_timeout_ms) + wit_world.imports.backend.Backend.get_first_byte_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_first_byte_timeout_ms) + wit_world.imports.backend.Backend.get_between_bytes_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_between_bytes_timeout_ms) + wit_world.imports.backend.Backend.is_tls = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_tls) + wit_world.imports.backend.Backend.get_tls_min_version = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tls_min_version) + wit_world.imports.backend.Backend.get_tls_max_version = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tls_max_version) + wit_world.imports.backend.Backend.get_http_keepalive_time = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_http_keepalive_time) + wit_world.imports.backend.Backend.get_tcp_keepalive_enable = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_enable) + wit_world.imports.backend.Backend.get_tcp_keepalive_interval = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_interval) + wit_world.imports.backend.Backend.get_tcp_keepalive_probes = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_probes) + wit_world.imports.backend.Backend.get_tcp_keepalive_time = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_time) + wit_world.imports.http_body.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.new) + wit_world.imports.http_body.append = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.append) + wit_world.imports.http_body.read = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.read) + wit_world.imports.http_body.write = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.write) + wit_world.imports.http_body.write_front = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.write_front) + wit_world.imports.http_body.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.close) + wit_world.imports.http_body.append_trailer = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.append_trailer) + wit_world.imports.http_body.get_trailer_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_names) + wit_world.imports.http_body.get_trailer_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_value) + wit_world.imports.http_body.get_trailer_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_values) + wit_world.imports.http_resp.Response.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.new) + wit_world.imports.http_resp.Response.get_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_names) + wit_world.imports.http_resp.Response.get_header_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_value) + wit_world.imports.http_resp.Response.get_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_values) + wit_world.imports.http_resp.Response.set_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_header_values) + wit_world.imports.http_resp.Response.insert_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.insert_header) + wit_world.imports.http_resp.Response.append_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.append_header) + wit_world.imports.http_resp.Response.remove_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.remove_header) + wit_world.imports.http_resp.Response.get_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_version) + wit_world.imports.http_resp.Response.set_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_version) + wit_world.imports.http_resp.Response.get_status = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_status) + wit_world.imports.http_resp.Response.set_status = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_status) + wit_world.imports.http_resp.Response.set_framing_headers_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_framing_headers_mode) + wit_world.imports.http_resp.Response.set_http_keepalive_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_http_keepalive_mode) + wit_world.imports.http_resp.send_downstream = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.send_downstream) + wit_world.imports.http_resp.send_downstream_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.send_downstream_streaming) + wit_world.imports.http_resp.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.close) + wit_world.imports.http_req.Request.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.new) + wit_world.imports.http_req.Request.set_cache_override = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_cache_override) + wit_world.imports.http_req.Request.get_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_names) + wit_world.imports.http_req.Request.get_header_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_value) + wit_world.imports.http_req.Request.get_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_values) + wit_world.imports.http_req.Request.set_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_header_values) + wit_world.imports.http_req.Request.insert_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.insert_header) + wit_world.imports.http_req.Request.append_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.append_header) + wit_world.imports.http_req.Request.remove_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.remove_header) + wit_world.imports.http_req.Request.get_method = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_method) + wit_world.imports.http_req.Request.set_method = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_method) + wit_world.imports.http_req.Request.get_uri = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_uri) + wit_world.imports.http_req.Request.set_uri = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_uri) + wit_world.imports.http_req.Request.get_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_version) + wit_world.imports.http_req.Request.set_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_version) + wit_world.imports.http_req.Request.set_auto_decompress_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_auto_decompress_response) + wit_world.imports.http_req.Request.redirect_to_websocket_proxy = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.redirect_to_websocket_proxy) + wit_world.imports.http_req.Request.set_framing_headers_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_framing_headers_mode) + wit_world.imports.http_req.Request.redirect_to_grip_proxy = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.redirect_to_grip_proxy) + wit_world.imports.http_req.send = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send) + wit_world.imports.http_req.send_uncached = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_uncached) + wit_world.imports.http_req.send_async = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async) + wit_world.imports.http_req.send_async_uncached = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_uncached) + wit_world.imports.http_req.send_async_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_streaming) + wit_world.imports.http_req.send_async_uncached_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_uncached_streaming) + wit_world.imports.http_req.await_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.await_response) + wit_world.imports.http_req.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.close) + wit_world.imports.http_req.upgrade_websocket = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.upgrade_websocket) + wit_world.imports.http_downstream.next_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.next_request) + wit_world.imports.http_downstream.await_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.await_request) + wit_world.imports.http_downstream.downstream_original_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_original_header_names) + wit_world.imports.http_downstream.downstream_original_header_count = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_original_header_count) + wit_world.imports.http_downstream.downstream_client_h2_fingerprint = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_h2_fingerprint) + wit_world.imports.http_downstream.downstream_client_request_id = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_request_id) + wit_world.imports.http_downstream.downstream_client_oh_fingerprint = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_oh_fingerprint) + wit_world.imports.http_downstream.downstream_client_ddos_detected = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_ddos_detected) + wit_world.imports.http_downstream.downstream_tls_cipher_openssl_name = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_cipher_openssl_name) + wit_world.imports.http_downstream.downstream_tls_protocol = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_protocol) + wit_world.imports.http_downstream.downstream_tls_client_hello = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_hello) + wit_world.imports.http_downstream.downstream_tls_raw_client_certificate = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_raw_client_certificate) + wit_world.imports.http_downstream.downstream_tls_client_cert_verify_result = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_cert_verify_result) + wit_world.imports.http_downstream.downstream_tls_client_servername = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_servername) + wit_world.imports.http_downstream.downstream_tls_ja3_md5 = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_ja3_md5) + wit_world.imports.http_downstream.downstream_tls_ja4 = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_ja4) + wit_world.imports.http_downstream.downstream_compliance_region = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_compliance_region) + wit_world.imports.http_downstream.fastly_key_is_valid = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.fastly_key_is_valid) + wit_world.imports.security.inspect = remap_wit_errors(MAPPINGS)(wit_world.imports.security.inspect) + wit_world.imports.kv_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.open) + wit_world.imports.kv_store.Store.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.lookup) + wit_world.imports.kv_store.Store.lookup_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.lookup_async) + wit_world.imports.kv_store.Store.insert = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.insert) + wit_world.imports.kv_store.Store.insert_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.insert_async) + wit_world.imports.kv_store.Store.delete = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.delete) + wit_world.imports.kv_store.Store.delete_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.delete_async) + wit_world.imports.kv_store.Store.list = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.list) + wit_world.imports.kv_store.Store.list_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.list_async) + wit_world.imports.kv_store.await_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_lookup) + wit_world.imports.kv_store.await_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_insert) + wit_world.imports.kv_store.await_delete = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_delete) + wit_world.imports.kv_store.await_list = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_list) + wit_world.imports.kv_store.Entry.metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Entry.metadata) + wit_world.imports.acl.Acl.open = remap_wit_errors(MAPPINGS)(wit_world.imports.acl.Acl.open) + wit_world.imports.acl.Acl.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.acl.Acl.lookup) + wit_world.imports.purge.purge_surrogate_key = remap_wit_errors(MAPPINGS)(wit_world.imports.purge.purge_surrogate_key) + wit_world.imports.purge.purge_surrogate_key_verbose = remap_wit_errors(MAPPINGS)(wit_world.imports.purge.purge_surrogate_key_verbose) + wit_world.imports.cache.Entry.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.lookup) + wit_world.imports.cache.Entry.transaction_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_lookup) + wit_world.imports.cache.Entry.transaction_lookup_async = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_lookup_async) + wit_world.imports.cache.Entry.transaction_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_insert) + wit_world.imports.cache.Entry.transaction_insert_and_stream_back = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_insert_and_stream_back) + wit_world.imports.cache.Entry.transaction_update = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_update) + wit_world.imports.cache.Entry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_state) + wit_world.imports.cache.Entry.get_user_metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_user_metadata) + wit_world.imports.cache.Entry.get_body = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_body) + wit_world.imports.cache.Entry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_length) + wit_world.imports.cache.Entry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_max_age_ns) + wit_world.imports.cache.Entry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_stale_while_revalidate_ns) + wit_world.imports.cache.Entry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_age_ns) + wit_world.imports.cache.Entry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_hits) + wit_world.imports.cache.Entry.transaction_cancel = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_cancel) + wit_world.imports.cache.ReplaceEntry.replace = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.replace) + wit_world.imports.cache.ReplaceEntry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_age_ns) + wit_world.imports.cache.ReplaceEntry.get_body = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_body) + wit_world.imports.cache.ReplaceEntry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_hits) + wit_world.imports.cache.ReplaceEntry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_length) + wit_world.imports.cache.ReplaceEntry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_max_age_ns) + wit_world.imports.cache.ReplaceEntry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_stale_while_revalidate_ns) + wit_world.imports.cache.ReplaceEntry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_state) + wit_world.imports.cache.ReplaceEntry.get_user_metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_user_metadata) + wit_world.imports.cache.insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.insert) + wit_world.imports.cache.await_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.await_entry) + wit_world.imports.cache.close_pending_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_pending_entry) + wit_world.imports.cache.close_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_entry) + wit_world.imports.cache.replace_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.replace_insert) + wit_world.imports.cache.close_replace_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_replace_entry) + wit_world.imports.http_cache.Entry.transaction_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_lookup) + wit_world.imports.http_cache.Entry.transaction_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_insert) + wit_world.imports.http_cache.Entry.transaction_insert_and_stream_back = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_insert_and_stream_back) + wit_world.imports.http_cache.Entry.transaction_update = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_update) + wit_world.imports.http_cache.Entry.transaction_update_and_return_fresh = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_update_and_return_fresh) + wit_world.imports.http_cache.Entry.transaction_record_not_cacheable = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_record_not_cacheable) + wit_world.imports.http_cache.Entry.get_suggested_backend_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_suggested_backend_request) + wit_world.imports.http_cache.Entry.get_suggested_write_options = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_suggested_write_options) + wit_world.imports.http_cache.Entry.prepare_response_for_storage = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.prepare_response_for_storage) + wit_world.imports.http_cache.Entry.get_found_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_found_response) + wit_world.imports.http_cache.Entry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_state) + wit_world.imports.http_cache.Entry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_length) + wit_world.imports.http_cache.Entry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_max_age_ns) + wit_world.imports.http_cache.Entry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_stale_while_revalidate_ns) + wit_world.imports.http_cache.Entry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_age_ns) + wit_world.imports.http_cache.Entry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_hits) + wit_world.imports.http_cache.Entry.get_sensitive_data = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_sensitive_data) + wit_world.imports.http_cache.Entry.get_surrogate_keys = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_surrogate_keys) + wit_world.imports.http_cache.Entry.get_vary_rule = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_vary_rule) + wit_world.imports.http_cache.Entry.transaction_abandon = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_abandon) + wit_world.imports.http_cache.is_request_cacheable = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.is_request_cacheable) + wit_world.imports.http_cache.get_suggested_cache_key = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.get_suggested_cache_key) + wit_world.imports.http_cache.close_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.close_entry) + wit_world.imports.http_cache.SuggestedWriteOptions.get_vary_rule = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.SuggestedWriteOptions.get_vary_rule) + wit_world.imports.http_cache.SuggestedWriteOptions.get_surrogate_keys = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.SuggestedWriteOptions.get_surrogate_keys) + wit_world.imports.config_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.config_store.Store.open) + wit_world.imports.config_store.Store.get = remap_wit_errors(MAPPINGS)(wit_world.imports.config_store.Store.get) + wit_world.imports.shielding.shield_info = remap_wit_errors(MAPPINGS)(wit_world.imports.shielding.shield_info) + wit_world.imports.shielding.backend_for_shield = remap_wit_errors(MAPPINGS)(wit_world.imports.shielding.backend_for_shield) + wit_world.imports.image_optimizer.transform_image_optimizer_request = remap_wit_errors(MAPPINGS)(wit_world.imports.image_optimizer.transform_image_optimizer_request) diff --git a/pyproject.toml b/pyproject.toml index 62517d8..472842c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ ignore = [ "D415", # Don't require punctuation at the end of a docstring summary: sometimes they are noun phrases, not sentences. "D205", # This spuriously complains about line-wrapped single-sentence docstring summaries. Sometimes 80 chars isn't enough. "D107", # Sometimes there's nothing non-obvious to say about a constructor. + "D105", # Usually there's nothing to say about __str__, etc. ] [tool.ruff.format] @@ -63,6 +64,7 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" +exclude = ["fastly_compute/runtime_patching/patches.py"] [tool.ruff.lint.pydocstyle] convention = "google" @@ -80,6 +82,7 @@ build-backend = "maturin" [dependency-groups] dev = [ + "jinja2>=3.1.6", "maturin>=1.11.5", ] diff --git a/scripts/generate_patches/generation.py b/scripts/generate_patches/generation.py index 4d8f742..8cbbc1f 100755 --- a/scripts/generate_patches/generation.py +++ b/scripts/generate_patches/generation.py @@ -7,15 +7,25 @@ import json from collections import defaultdict from collections.abc import Iterable, Mapping +from functools import partial from pathlib import Path from subprocess import check_output +from typing import Any + +from jinja2 import Environment, PackageLoader, Template, TemplateNotFound from .wit import Function, NullType, Type, Wit WIT_DIR = "wit" +FASTLY_COMPUTE = Path(__file__).parent.parent.parent / "fastly_compute" + + +jinja_env = Environment( + loader=PackageLoader("scripts.generate_patches"), autoescape=False +) -def exception_code_tree(error_types: Iterable[Type]) -> Mapping[str, Mapping[str, str]]: +def generate_exceptions(error_types: Iterable[Type]): """Generate Python exception classes we can map error types to. Inherit names and docstrings from the WIT. Create a common superclass for @@ -28,39 +38,64 @@ def exception_code_tree(error_types: Iterable[Type]) -> Mapping[str, Mapping[str contained code. For example, acl (from the interface name) -> acl_error.py (from the enum name) -> class AclError(FastlyError)... """ - # interface name -> module name -> code chunks: - code = defaultdict(lambda: defaultdict(str)) + # package -> module -> code: + exceptions = defaultdict(lambda: defaultdict(dict[str, str])) + # package -> module -> docstring: + module_docstrings = defaultdict(dict) + # package -> docstring: + package_docstrings = {} for error_type in error_types: package = error_type.py_package() module = error_type.py_module() + ".py" - # Create package's empty __init__.py if not already there: - code[package]["__init__.py"] + try: + module_docstrings[package][module] + except KeyError: + module_docstrings[package][module] = error_type.docstring(indent=0) - if not code[package][module]: - code[package][module] += ( - "from fastly_compute.exceptions import FastlyError\n\n\n" - ) + # Create package's __init__.py if not already there: + if package not in package_docstrings: + package_docstrings[package] = error_type.interface().docstring(indent=0) # Common superclass for exceptions based on the enum or variant's # members. Or the raised exception itself for records. - code[package][module] += ( - f"""class {error_type.py_exception_name()}(FastlyError):\n""" - f''' """{error_type.docstring_or_pass()}"""\n\n\n''' + top_level_exception_name = error_type.py_exception_name() + exceptions[package][module][top_level_exception_name] = ( + f"""class {top_level_exception_name}(FastlyError):\n""" + f""" {error_type.docstring(indent=4) or "pass"}""" ) # Insert enum or variant cases. for case in error_type.cases(): - code[package][module] += ( - f"""class {case.py_exception_name()}({error_type.py_exception_name()}):\n""" - f''' """{case.docstring_or_pass()}"""\n\n\n''' + case_exception_name = case.py_exception_name() + exceptions[package][module][case_exception_name] = ( + f"""class {case_exception_name}({top_level_exception_name}):\n""" + f""" {case.docstring(indent=4) or "pass"}""" + ) + + for package, docstring in package_docstrings.items(): + write_templated_file( + FASTLY_COMPUTE / "exceptions" / package / "__init__.py", + {"module_docstring": docstring}, + jinja_env.get_template("exception_init_module.py.jinja"), + ) + for package, modules in exceptions.items(): + for module, exceptions_by_name in modules.items(): + write_templated_file( + FASTLY_COMPUTE / "exceptions" / package / module, + { + "generated_exceptions": partial( + join_named_chunks, exceptions_by_name, "\n\n\n" + ), + "module_docstring": module_docstrings[package][module], + }, + jinja_env.get_template("default_exception.py.jinja"), ) - return code -def mappings_code_tree( +def generate_patches( error_types: Iterable[Type], functions_to_patch: Iterable[Function] -) -> dict[str, dict[str, str]]: +): """Generate code which makes componentize-py-generated routines raise more specific, idiomatically shaped exceptions. @@ -68,20 +103,14 @@ def mappings_code_tree( monkeypatches that wrap componentize-py's generated Python routines to raise them. """ - code = ( - """# This file is automatically generated by generate_patches.py.\n""" - """# It is not intended for manual editing.\n""" - '''"""Monkeypatches which wrap the routines generated by componentize-py to make\n''' - '''them raise more specific exceptions, not just Err."""\n\n''' - ) - # Collect info: mappings = set() - imports = set() + wit_imports = set() + fastly_imports = set() for error_type in error_types: # Get where it is found in wit_world. Use shallow imports to avoid collisions. - imports.add(error_type.wit_module_path()) - imports.add(error_type.py_module_path()) + wit_imports.add(error_type.wit_module_path()) + fastly_imports.add(error_type.py_module_path()) if error_type.has_cases(): # We need only add the cases; it doesn't make sense in WIT to return # the Enum or Variant itself in a result. @@ -104,70 +133,61 @@ def mappings_code_tree( # Collect import paths for the functions themselves: for func in functions_to_patch: - imports.add(func.wit_module_path()) + wit_imports.add(func.wit_module_path()) - # Do templating: - code += "try:\n" - code += ( - " from .decorators import remap_wit_errors\n" - " import fastly_compute.exceptions\n" - ) - for import_ in sorted(imports): - code += f" import {import_}\n" - code += ( - "except ImportError:\n" - " # Tolerate that momentary import for the testrunner before Viceroy, and thus\n" - " # the wit_world, is around.\n" - " def patch():\n" - ' print("Faking the run of exception-mapping monkeypatches for test runner.")\n' - "else:\n" - " MAPPINGS = {\n" - ) - for wit_path, py_module_path, py_exception_name in sorted(mappings): - code += f" {wit_path}: {py_module_path}.{py_exception_name},\n" - code += ( - " type(None): fastly_compute.exceptions.FastlyError,\n" # Linter: don't wrap. - " }\n" - ) - - code += ''' - did_patch = False - - def patch(): - """Apply patches if they haven't already been applied.""" + # TODO: Maybe automatically improve the docstring of each method to list the + # exceptions it raises. - global did_patch - if did_patch: - # This test shouldn't be needed, but it avoids double-wrapping the - # routines if somehow patch() did get called twice. - return - did_patch = True\n\n''' + write_templated_file( + FASTLY_COMPUTE / "runtime_patching" / "patches.py", + { + "fastly_imports": sorted(fastly_imports), + "wit_imports": sorted(wit_imports), + "mappings": sorted(mappings), + "functions_to_patch": functions_to_patch, + }, + jinja_env.get_template("patches.py.jinja"), + ) - for func in functions_to_patch: - func_path = func.wit_path() - code += f" {func_path} = remap_wit_errors(MAPPINGS)({func_path})\n" - # TODO: Make affordance for manually adding ergonomic getter properties, - # __str__s, etc. to exception classes. +def join_named_chunks( + chunks: dict[str, str], sep: str, omit: list[str] | None = None +) -> str: + """Return an ordered concatenation of all items in a dict except those of + the given keys. + """ + if omit is None: + omit = [] + return sep.join( + chunk for name, chunk in chunks.items() if name not in omit + ) # O(n^2) but small - # TODO: Maybe automatically improve the docstring of each method to list the - # exceptions it raises. - return {"runtime_patching": {"patches.py": code}} +def write_templated_file( + dest_file: Path, template_vars: Mapping[str, Any], default_template: Template +): + """Render templates to generate code on disk, providing hook points for + replacing generated pieces with manual improvements. + We examine the ``templates`` folder for a file at the same relative path as + ``dest_file`` is from ``fastly_compute``. We use it if found. Otherwise, we + call back to ``default_template``. -def write_files(tree: Mapping[str, Mapping[str, str]], base_folder: Path): - """Create filesystem artifacts mirroring a nested dict representing folders, - then files, then file contents. + :arg dest_file: Path to the file to write, relative to ``fastly_compute`` + :arg template_vars: Data to populate the template + :arg default_template: Template to fall back to if a parallel one to does + not exist in the templates folder - Overwrite files that are mentioned in ``tree``, but don't delete anything else. """ - for folder, files in tree.items(): - folder_path = base_folder / folder - folder_path.mkdir(parents=True, exist_ok=True) - for file, contents in files.items(): - file_path = folder_path / file - file_path.write_text(contents) + subpath = dest_file.relative_to(FASTLY_COMPUTE) + try: + # Render a parallel template if it's there: + template = jinja_env.get_template(str(subpath) + ".jinja") + except TemplateNotFound: + template = default_template + rendered = template.render(template_vars) + dest_file.parent.mkdir(parents=True, exist_ok=True) + dest_file.write_text(rendered) def generate(): @@ -210,12 +230,5 @@ def generate(): # identifiable: functions_to_patch.append(function) - fastly_compute = Path(__file__).parent.parent.parent / "fastly_compute" - write_files( - exception_code_tree(exceptions_to_generate.keys()), - fastly_compute / "exceptions", - ) - write_files( - mappings_code_tree(exceptions_to_generate.keys(), functions_to_patch), - fastly_compute, - ) + generate_exceptions(exceptions_to_generate.keys()) + generate_patches(exceptions_to_generate.keys(), functions_to_patch) diff --git a/scripts/generate_patches/templates/default_exception.py.jinja b/scripts/generate_patches/templates/default_exception.py.jinja new file mode 100644 index 0000000..fa021d4 --- /dev/null +++ b/scripts/generate_patches/templates/default_exception.py.jinja @@ -0,0 +1,12 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +{{ module_docstring }} + +{% block imports -%} +from fastly_compute.exceptions import FastlyError +{%- endblock %} + + +{% block exceptions -%} +{{ generated_exceptions() }} +{% endblock -%} diff --git a/scripts/generate_patches/templates/exception_init_module.py.jinja b/scripts/generate_patches/templates/exception_init_module.py.jinja new file mode 100644 index 0000000..62eec96 --- /dev/null +++ b/scripts/generate_patches/templates/exception_init_module.py.jinja @@ -0,0 +1,2 @@ +{{ module_docstring }} + diff --git a/scripts/generate_patches/templates/exceptions/types/error.py.jinja b/scripts/generate_patches/templates/exceptions/types/error.py.jinja new file mode 100644 index 0000000..16a6854 --- /dev/null +++ b/scripts/generate_patches/templates/exceptions/types/error.py.jinja @@ -0,0 +1,19 @@ +{% extends "default_exception.py.jinja" -%} + +{% block exceptions -%} +{{ generated_exceptions(omit=["BufferLen"]) }} + + +class BufferLen(Error): + """Buffer length error + + Returned when a buffer is the wrong size. + Includes the buffer length that would allow the operation to succeed. + """ + + def __init__(self, wit_error): + self.length = wit_error.value + + def __str__(self): + return f"Buffer was too short to hold the result. At least {self.length} bytes are needed." +{% endblock %} diff --git a/scripts/generate_patches/templates/patches.py.jinja b/scripts/generate_patches/templates/patches.py.jinja new file mode 100644 index 0000000..f205c5b --- /dev/null +++ b/scripts/generate_patches/templates/patches.py.jinja @@ -0,0 +1,45 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Monkeypatches which wrap the routines generated by componentize-py to make +them raise more specific exceptions, not just Err. +""" + +try: + {%- for import_ in wit_imports %} + import {{ import_ }} + {%- endfor %} +{# Skip a line here. #} + {%- for import_ in fastly_imports %} + import {{ import_ }} + {%- endfor %} + + from .decorators import remap_wit_errors +except ImportError: + # Tolerate that momentary import for the testrunner before Viceroy, and thus + # the wit_world, is around. + def patch(): + """Pretend to patch.""" + print("Faking the run of exception-mapping monkeypatches for test runner.") +else: + MAPPINGS = { + {% for wit_path, py_module_path, py_exception_name in mappings -%} + {{wit_path}}: {{py_module_path}}.{{py_exception_name}}, + {% endfor -%} + type(None): fastly_compute.exceptions.FastlyError, + } + + did_patch = False + + def patch(): + """Apply patches if they haven't already been applied.""" + global did_patch + if did_patch: + # This test shouldn't be needed, but it avoids double-wrapping the + # routines if somehow patch() did get called twice. + return + did_patch = True + + {%- for func in functions_to_patch %} + {{ func.wit_path() }} = remap_wit_errors(MAPPINGS)({{ func.wit_path() }}) + {%- endfor %} + diff --git a/scripts/generate_patches/utils.py b/scripts/generate_patches/utils.py index 66e9d52..d1c1a92 100644 --- a/scripts/generate_patches/utils.py +++ b/scripts/generate_patches/utils.py @@ -1,7 +1,5 @@ """Little helpers used in patch generation""" -import textwrap - def only(iterable): """Return the one and only item of the iterable, raising ValueError if there @@ -26,13 +24,3 @@ def lower_snake(s: str) -> str: def shouty_snake(s: str) -> str: """Convert lower-kebab case to SHOUTY_SNAKE_CASE.""" return s.replace("-", "_").upper() - - -def indent(s: str): - """Indent as for a docstring. - - Indent all but the first line of a string by 4 spaces, strip leading and - trailing whitespace, and put a newline at the end if there's more than 1 - line. - """ - return textwrap.indent(s, " ").strip() diff --git a/scripts/generate_patches/wit.py b/scripts/generate_patches/wit.py index a452129..f54fe7e 100644 --- a/scripts/generate_patches/wit.py +++ b/scripts/generate_patches/wit.py @@ -10,10 +10,11 @@ import re from collections.abc import Iterable +import textwrap from types import NoneType from typing import Any, Self -from .utils import indent, lower_snake, only, shouty_snake, upper_camel +from .utils import lower_snake, only, shouty_snake, upper_camel class DocsHaver: @@ -25,16 +26,24 @@ class DocsHaver: _me: Any def docs(self) -> str: - """Return the documentation of the type, "" if omitted.""" - return self._me.get("docs", {}).get("contents", "") + """Return the documentation of the type, "" if omitted. - def docstring_or_pass(self) -> str: - """Return a one-level-indented version of the docs suitable for use as a - docstring in an otherwise empty construct. + Strip leading and trailing whitespace. + """ + return self._me.get("docs", {}).get("contents", "").strip() - Accordingly, emit "pass" if there is no docstring. + def docstring(self, indent=4) -> str: + """Return a one-level-indented, triple-quoted version of the docs + suitable for use as a docstring in a top-level construct. """ - return indent(self.docs()) or "pass" + docs = self.docs() + if docs: + if docs.count("\n") > 0: # multi-line + docs += "\n" + docs += '"""' + + return '"""' + textwrap.indent(docs, " " * indent).lstrip() + return "" class Thing(DocsHaver): @@ -308,7 +317,7 @@ def error_type_of_returned_result(self) -> Type | None: return return_type.error_type() -class Interface: +class Interface(DocsHaver): """A WIT interface""" def __init__(self, interface_json: dict[str, Any], wit_json: dict[str, list[dict]]): diff --git a/uv.lock b/uv.lock index c73e5b9..75a9734 100644 --- a/uv.lock +++ b/uv.lock @@ -144,6 +144,7 @@ test = [ [package.dev-dependencies] dev = [ + { name = "jinja2" }, { name = "maturin" }, ] @@ -162,7 +163,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask"