errpath 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
errpath/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """errpath: Result types for Python. See every error path."""
2
+
3
+ from errpath.result import Err, Ok, Result, err_type, ok, traceback_of
4
+ from errpath.safe import safe, safe_async, safe_async_with, safe_with
5
+
6
+ __all__ = [
7
+ "Err",
8
+ "Ok",
9
+ "Result",
10
+ "err_type",
11
+ "ok",
12
+ "safe",
13
+ "safe_async",
14
+ "safe_async_with",
15
+ "safe_with",
16
+ "traceback_of",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
errpath/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from errpath.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
errpath/_pydantic.py ADDED
@@ -0,0 +1,102 @@
1
+ """Pydantic support for Ok / Err. Imported only when Pydantic asks for a schema.
2
+
3
+ JSON shape (the same as awaitly's):
4
+
5
+ {"ok": true, "value": ...}
6
+ {"ok": false, "error": ...}
7
+
8
+ An exception error is written as {"type": "KeyError", "message": "..."}. It can't be
9
+ read back from JSON (an exception isn't data), so use a model, str or enum as the
10
+ error type for Results that travel through APIs.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import types
16
+ import typing
17
+ from collections.abc import Callable
18
+ from typing import Any
19
+
20
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
21
+ from pydantic_core import PydanticSerializationUnexpectedValue
22
+ from pydantic_core import core_schema as cs
23
+
24
+
25
+ def _exception_types(tp: Any) -> tuple[type[BaseException], ...] | None:
26
+ """The exception classes in ``tp`` (a class or a union of them), or None if it isn't one."""
27
+ members = (
28
+ typing.get_args(tp) if typing.get_origin(tp) in (typing.Union, types.UnionType) else (tp,)
29
+ )
30
+ if members and all(isinstance(m, type) and issubclass(m, BaseException) for m in members):
31
+ return members
32
+ return None
33
+
34
+
35
+ def _describe_exception(error: BaseException) -> dict[str, str]:
36
+ return {"type": type(error).__name__, "message": str(error)}
37
+
38
+
39
+ def result_schema(
40
+ cls: type, source: Any, handler: GetCoreSchemaHandler, *, ok: bool
41
+ ) -> cs.CoreSchema:
42
+ field = "value" if ok else "error"
43
+ args = typing.get_args(source)
44
+ payload_type = args[0] if args else Any
45
+
46
+ exceptions = _exception_types(payload_type)
47
+ to_payload: Callable[[Any], Any]
48
+ if exceptions:
49
+ payload = cs.union_schema([cs.is_instance_schema(e) for e in exceptions])
50
+ payload_out = cs.typed_dict_schema(
51
+ {
52
+ "type": cs.typed_dict_field(cs.str_schema()),
53
+ "message": cs.typed_dict_field(cs.str_schema()),
54
+ }
55
+ )
56
+ to_payload = _describe_exception
57
+ else:
58
+ payload = handler.generate_schema(payload_type)
59
+ payload_out = payload
60
+ to_payload = lambda value: value # noqa: E731
61
+
62
+ def shape(inner: cs.CoreSchema) -> cs.CoreSchema:
63
+ return cs.typed_dict_schema(
64
+ {
65
+ "ok": cs.typed_dict_field(cs.literal_schema([ok])),
66
+ field: cs.typed_dict_field(inner),
67
+ }
68
+ )
69
+
70
+ # {"ok": ..., "value"/"error": ...} -> Ok/Err, validating the payload.
71
+ from_dict = cs.no_info_after_validator_function(lambda d: cls(d[field]), shape(payload))
72
+ # An existing Ok/Err is re-checked the same way, so Ok("not a User") is rejected.
73
+ from_instance = cs.chain_schema(
74
+ [
75
+ cs.is_instance_schema(cls),
76
+ cs.no_info_plain_validator_function(lambda r: {"ok": ok, field: getattr(r, field)}),
77
+ from_dict,
78
+ ]
79
+ )
80
+
81
+ def serialize(result: Any, handler: cs.SerializerFunctionWrapHandler) -> Any:
82
+ if not isinstance(result, cls):
83
+ # In Ok | Err, both serializers are tried; this tells Pydantic to use the other.
84
+ raise PydanticSerializationUnexpectedValue(f"not an {cls.__name__}")
85
+ # Hand the dict back to Pydantic, so include/exclude reach the payload's fields
86
+ # (e.g. exclude={"result": {"value": {"secret"}}}).
87
+ return handler({"ok": ok, field: to_payload(getattr(result, field))})
88
+
89
+ def document_written_shape(schema: cs.CoreSchema, handler: GetJsonSchemaHandler) -> Any:
90
+ # An exception is validated by instance check (no JSON Schema) but written as
91
+ # {type, message}; document what is written. (return_schema would do this too, but
92
+ # it serializes a second time, which breaks include/exclude.)
93
+ if handler.mode == "serialization":
94
+ return handler(shape(payload_out))
95
+ return handler(schema)
96
+
97
+ return cs.json_or_python_schema(
98
+ json_schema=from_dict,
99
+ python_schema=cs.union_schema([from_instance, from_dict]),
100
+ serialization=cs.wrap_serializer_function_ser_schema(serialize, schema=shape(payload_out)),
101
+ metadata={"pydantic_js_functions": [document_written_shape]} if exceptions else None,
102
+ )