-
Notifications
You must be signed in to change notification settings - Fork 170
Add detail to traced values where str() is ambiguous #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| Traced values now gain detail where ``str()`` is ambiguous: an empty string or a string | ||
| carrying whitespace is quoted, an enum member shows its name, and a path shows its type, | ||
| so that two arguments pointing at the same place are distinguishable. Values that read | ||
| unambiguously as themselves are unchanged, and a value spanning several lines is drawn | ||
| as a block attached to its key instead of running into the surrounding trace. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1030,6 +1030,25 @@ undo function to disable the behaviour. | |
| pm.trace.root.setwriter(print) | ||
| undo = pm.enable_tracing() | ||
|
|
||
| Each hook call is traced with its keyword arguments, followed by a ``finish`` | ||
| line carrying the result:: | ||
|
|
||
| he_method1 [hook] | ||
| plugin_name: example | ||
| path: PosixPath('/tmp') | ||
| reason: 'needs a network connection' | ||
| status: <ExitCode.TESTS_FAILED: 1> | ||
| explanation: | ||
| | first line | ||
| \ second line | ||
| finish he_method1 --> ['value'] [hook] | ||
|
|
||
| Values are rendered with :func:`str` wherever that reads unambiguously, and with | ||
| :func:`repr` where it does not: an empty string, a string carrying whitespace, | ||
| an enum member, or a path, whose type is otherwise easy to lose. A value | ||
| spanning several lines is drawn as a block so that it stays attached to its key | ||
| instead of running into the surrounding trace. | ||
|
Comment on lines
+1046
to
+1050
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example seems useful but I think this paragraph is unnecessary, would just remove it. |
||
|
|
||
|
|
||
| Call monitoring | ||
| --------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,20 +6,22 @@ | |
|
|
||
| from collections.abc import Callable | ||
| from collections.abc import Sequence | ||
| import enum | ||
| import os | ||
| from typing import Any | ||
|
|
||
|
|
||
| _Writer = Callable[[str], object] | ||
| _Processor = Callable[[tuple[str, ...], tuple[Any, ...]], object] | ||
|
|
||
|
|
||
| def _describe_str_failure(exc: Exception, obj: object) -> str: | ||
| def _describe_failure(exc: Exception, obj: object, func: str) -> str: | ||
| try: | ||
| exc_info = repr(exc) | ||
| except Exception: | ||
| exc_info = f"unpresentable {type(exc).__name__}" | ||
| name = type(obj).__name__ | ||
| return f"<[{exc_info} raised in str()] {name} object at 0x{id(obj):x}>" | ||
| return f"<[{exc_info} raised in {func}()] {name} object at 0x{id(obj):x}>" | ||
|
|
||
|
|
||
| def _escape_surrogates(text: str) -> str: | ||
|
|
@@ -42,7 +44,55 @@ def _safe_str(obj: object) -> str: | |
| try: | ||
| text = str(obj) | ||
| except Exception as exc: | ||
| text = _describe_str_failure(exc, obj) | ||
| text = _describe_failure(exc, obj, "str") | ||
| return _escape_surrogates(text) | ||
|
|
||
|
|
||
| def _is_plain_token(text: str) -> bool: | ||
| """Whether ``text`` can be shown bare, without quotes around it.""" | ||
| return bool(text) and text.isprintable() and " " not in text | ||
|
|
||
|
|
||
| def _format_block(indent: str, text: str) -> list[str]: | ||
| """Draw a multi line value as a box, so it reads as one value. | ||
|
|
||
| The left edge marks every line as continuation, and the final ``\\`` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In plaintext the |
||
| closes it, which keeps a block distinguishable from the trace lines | ||
| around it. | ||
| """ | ||
| body = text.split("\n") | ||
| edges = ["|"] * (len(body) - 1) + ["\\"] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems a bit inefficient to create the entire list just for this, maybe replace with an if? But it's OK if you prefer it |
||
| return [f"{indent} {edge} {line}\n" for edge, line in zip(edges, body)] | ||
|
|
||
|
|
||
| def _render_value(obj: object) -> str: | ||
| """Render a traced value, adding detail only where ``str`` is ambiguous. | ||
|
|
||
| Most values keep their plain ``str`` rendering, which is what makes a trace | ||
| readable. ``repr`` is used only where ``str`` hides something the reader | ||
| needs: the type of a path, the name of an enum member, or the boundaries of | ||
| a string that is empty or carries whitespace. | ||
|
Comment on lines
+71
to
+74
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would remove this paragraph as too verbose. The code itself is clear enough I'd say. |
||
| """ | ||
| if isinstance(obj, str): | ||
| if "\n" in obj or "\r" in obj: | ||
| return _safe_str(obj) | ||
| if _is_plain_token(obj): | ||
| return _safe_str(obj) | ||
| return _safe_repr(obj) | ||
| if isinstance(obj, (enum.Enum, os.PathLike)): | ||
| return _safe_repr(obj) | ||
| return _safe_str(obj) | ||
|
|
||
|
|
||
| def _safe_repr(obj: object) -> str: | ||
| """``repr(obj)`` for tracing, with a failing ``__repr__`` rendered, not raised. | ||
|
|
||
| The result has lone surrogates escaped, so any text writer accepts it. | ||
| """ | ||
| try: | ||
| text = repr(obj) | ||
| except Exception as exc: | ||
| text = _describe_failure(exc, obj, "repr") | ||
| return _escape_surrogates(text) | ||
|
|
||
|
|
||
|
|
@@ -68,7 +118,12 @@ def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: | |
| lines = [f"{indent}{content} [{':'.join(tags)}]\n"] | ||
|
|
||
| for name, value in extra.items(): | ||
| lines.append(f"{indent} {name}: {_safe_str(value)}\n") | ||
| rendered = _render_value(value) | ||
| if "\n" in rendered: | ||
| lines.append(f"{indent} {name}:\n") | ||
| lines.extend(_format_block(indent, rendered)) | ||
| else: | ||
| lines.append(f"{indent} {name}: {rendered}\n") | ||
|
|
||
| return "".join(lines) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too verbose IMO. My suggestion to write it in human language: