hdl.ir: allow disabling UnusedElaboratable warning in file scope.

This warning is usually quite handy, but is problematic in tests:
although it can be suppressed by using Fragment.get on elaboratable,
that is not always possible, in particular when writing tests for
exceptions raised by __init__, e.g.:

    def test_wrong_csr_bus(self):
        with self.assertRaisesRegex(ValueError, r"blah blah"):
            WishboneCSRBridge(csr_bus=object())

In theory, it should be possible to suppress warnings per-module
and even per-line using code such as:

    import re, warnings
    from nmigen.hdl.ir import UnusedElaboratable
    warnings.filterwarnings("ignore", category=UnusedElaboratable,
                            module=re.escape(__name__))

Unfortunately, not only is this code quite convoluted, but it also
does not actually work; we are using warnings.warn_explicit() because
we collect source locations on our own, but it requires the caller
to extract the __warningregistry__ dictionary from module globals,
or warning suppression would not work. Not only is this not feasible
in most diagnostic sites in nMigen, but also I never got it to work
anyway, even when passing all of module, registry, and module_globals
to warn_explicit().

Instead, use a magic comment at the start of a file to do this job,
which might not be elegant but is simple and practical. For now,
only UnusedElaboratable can be suppressed with it, but in future,
other linter parameters may become tweakable this way.
This commit is contained in:
whitequark 2019-10-26 05:34:00 +00:00
parent 8b05b28f5a
commit 9786d0c0e3
2 changed files with 43 additions and 6 deletions

View file

@ -1,6 +1,8 @@
import contextlib
import functools
import warnings
import linecache
import re
from collections import OrderedDict
from collections.abc import Iterable
from contextlib import contextmanager
@ -8,7 +10,8 @@ from contextlib import contextmanager
from .utils import *
__all__ = ["flatten", "union" , "log2_int", "bits_for", "memoize", "final", "deprecated"]
__all__ = ["flatten", "union" , "log2_int", "bits_for", "memoize", "final", "deprecated",
"get_linter_options", "get_linter_option"]
def flatten(i):
@ -82,3 +85,32 @@ def extend(cls):
name = f.__name__
setattr(cls, name, f)
return decorator
def get_linter_options(filename):
first_line = linecache.getline(filename, 1)
if first_line:
match = re.match(r"^#\s*nmigen:\s*((?:\w+=\w+\s*)(?:,\s*\w+=\w+\s*)*)\n$", first_line)
if match:
return dict(map(lambda s: s.strip().split("=", 2), match.group(1).split(",")))
return dict()
def get_linter_option(filename, name, type, default):
options = get_linter_options(filename)
if name not in options:
return default
option = options[name]
if type is bool:
if option in ("1", "yes", "enable"):
return True
if option in ("0", "no", "disable"):
return False
return default
if type is int:
try:
return int(option, 0)
except ValueError:
return default
assert False

View file

@ -21,19 +21,24 @@ class Elaboratable(metaclass=ABCMeta):
_Elaboratable__silence = False
def __new__(cls, *args, src_loc_at=0, **kwargs):
frame = sys._getframe(1 + src_loc_at)
self = super().__new__(cls)
self._Elaboratable__src_loc = traceback.extract_stack(limit=2 + src_loc_at)[0]
self._Elaboratable__used = False
self._Elaboratable__context = dict(
filename=frame.f_code.co_filename,
lineno=frame.f_lineno,
source=self)
return self
def __del__(self):
if self._Elaboratable__silence:
return
if hasattr(self, "_Elaboratable__used") and not self._Elaboratable__used:
warnings.warn_explicit("{!r} created but never used".format(self), UnusedElaboratable,
filename=self._Elaboratable__src_loc.filename,
lineno=self._Elaboratable__src_loc.lineno,
source=self)
if get_linter_option(self._Elaboratable__context["filename"],
"UnusedElaboratable", bool, True):
warnings.warn_explicit(
"{!r} created but never used".format(self), UnusedElaboratable,
**self._Elaboratable__context)
_old_excepthook = sys.excepthook