Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions amaranth/_unused.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import sys
from typing import Any
import warnings
import traceback
import re
import os

from ._utils import get_linter_option

Expand All @@ -14,15 +18,57 @@ class UnusedMustUse(Warning):
class MustUse:
_MustUse__silence = False
_MustUse__warning = UnusedMustUse
_MustUse__should_trace = os.environ.get("AMARANTH_TRACE_UNUSED", "").lower() in [
"1",
"true",
"yes",
"enable",
"full",
]
_MustUse__should_trace_full = (
os.environ.get("AMARANTH_TRACE_UNUSED", "").lower() == "full"
)
# Ignore stack frames from code in files that are likely to be unhelpful
_MustUse__ignored_paths_re = re.compile(
r"(?:^<frozen runpy>$|python[^/]*/(?:unittest|_pyrepl)/)"
)

_MustUse__used: bool
_MustUse__context: dict[str, Any]
_MustUse__stack_summary: traceback.StackSummary

@classmethod
def __filter_stack(
cls, stack_summary: traceback.StackSummary
) -> traceback.StackSummary:
return traceback.StackSummary(
filter(
lambda f: cls._MustUse__ignored_paths_re.search(f.filename) is None,
stack_summary,
)
)

def __new__(cls, *_args: list[Any], src_loc_at: int = 0, **_kwargs: dict[str, Any]):
# capture and ignore arbitrary args/kwargs to prevent errors with mixins

def __new__(cls, *args, src_loc_at=0, **kwargs):
frame = sys._getframe(1 + src_loc_at)

self = super().__new__(cls)
self._MustUse__used = False
self._MustUse__used = False
self._MustUse__context = dict(
filename=frame.f_code.co_filename,
lineno=frame.f_lineno,
source=self)
source=self,
)

if cls._MustUse__should_trace:
self._MustUse__stack_summary = traceback.extract_stack(f=frame)

if not cls._MustUse__should_trace_full:
self._MustUse__stack_summary = cls.__filter_stack(
self._MustUse__stack_summary
)

return self

def __del__(self):
Expand All @@ -31,11 +77,35 @@ def __del__(self):
if getattr(self._MustUse__warning, "_MustUse__silence", False):
return
if hasattr(self, "_MustUse__used") and not self._MustUse__used:
if get_linter_option(self._MustUse__context["filename"],
self._MustUse__warning.__qualname__, bool, True):
# allow suppression via amaranth file level linter option
if get_linter_option(
self._MustUse__context["filename"],
self._MustUse__warning.__qualname__,
type=bool,
default=True,
):
trace: str
if self._MustUse__should_trace:
if self._MustUse__should_trace_full:
trace = f"Full trace of {type(self).__qualname__} (MustUse) creation:\n"
else:
trace = (
f"Filtered trace of {type(self).__qualname__} (MustUse) creation "
"(set AMARANTH_TRACE_UNUSED=full for unfiltered):\n"
)

trace += "\n".join(traceback.format_list(self._MustUse__stack_summary))
else:
trace = (
f"Trace of {type(self).__qualname__} (MustUse) creation not available, "
"set AMARANTH_TRACE_UNUSED=1 (or =full for unfiltered) and rerun"
)

warnings.warn_explicit(
f"{self!r} created but never used", self._MustUse__warning,
**self._MustUse__context)
f"{self!r} created but never used\n{trace}",
self._MustUse__warning,
**self._MustUse__context,
)


_old_excepthook = sys.excepthook
Expand Down
Loading