Skip to content

Commit 9336c45

Browse files
authored
perf: build the StatusError message on first use instead of at construction (#180)
1 parent 6069883 commit 9336c45

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

‎src/httpware/errors.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from httpware._internal.redaction import redact_url
2424

2525

26+
_EXCEPTION_ARGS: types.GetSetDescriptorType = BaseException.__dict__["args"]
27+
28+
2629
def _restore_client_error(cls: "type[ClientError]", args: tuple[Any, ...]) -> "ClientError":
2730
return cls.__new__(cls, *args)
2831

@@ -71,7 +74,24 @@ class StatusError(ClientError):
7174

7275
def __init__(self, response: httpx2.Response) -> None:
7376
self.response = response
74-
super().__init__(self._summary())
77+
78+
@property
79+
def args(self) -> tuple[Any, ...]:
80+
"""The one-element ``(message,)`` tuple, the message built on first access."""
81+
args = _EXCEPTION_ARGS.__get__(self)
82+
# BaseException.__new__ stored the constructor's (response,) here.
83+
if len(args) == 1 and args[0] is vars(self).get("response"):
84+
args = (self._summary(),)
85+
_EXCEPTION_ARGS.__set__(self, args)
86+
return args
87+
88+
@args.setter
89+
def args(self, value: tuple[Any, ...]) -> None:
90+
_EXCEPTION_ARGS.__set__(self, value)
91+
92+
def __str__(self) -> str:
93+
args = self.args
94+
return str(args[0]) if len(args) == 1 else super().__str__()
7595

7696
def _summary(self) -> str:
7797
method = self.response.request.method

‎tests/test_errors.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,29 @@ def test_status_error_summary_message_includes_status_method_url() -> None:
9999
assert str(exc) == "404 GET https://example.test/missing"
100100

101101

102+
def test_status_error_args_hold_the_summary_message() -> None:
103+
exc = NotFoundError(_make_response(404, url="https://user:pass@example.test/x?token=s", method="GET"))
104+
assert exc.args == ("404 GET https://example.test/x?token=REDACTED",)
105+
106+
107+
def test_status_error_builds_its_message_on_first_use(monkeypatch: pytest.MonkeyPatch) -> None:
108+
redacted: list[str] = []
109+
monkeypatch.setattr(httpware.errors, "redact_url", redacted.append)
110+
NotFoundError(_make_response(404))
111+
assert redacted == []
112+
113+
114+
@pytest.mark.parametrize(
115+
("args", "message"),
116+
[(("custom",), "custom"), (("a", "b"), "('a', 'b')"), ((), "")],
117+
)
118+
def test_status_error_assigned_args_replace_the_message(args: tuple[str, ...], message: str) -> None:
119+
exc = NotFoundError(_make_response(404))
120+
exc.args = args
121+
assert exc.args == args
122+
assert str(exc) == message
123+
124+
102125
def test_status_error_strips_userinfo_in_summary_message() -> None:
103126
exc = NotFoundError(_make_response(404, url="https://user:pass@example.test/x"))
104127
assert "user" not in str(exc)

0 commit comments

Comments
 (0)