hdl.ir: allow disabling UnusedElaboratable warning in file scope.
authorwhitequark <cz@m-labs.hk>
Sat, 26 Oct 2019 05:34:00 +0000 (05:34 +0000)
committerwhitequark <cz@m-labs.hk>
Sat, 26 Oct 2019 06:17:14 +0000 (06:17 +0000)
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.

nmigen/_utils.py
nmigen/hdl/ir.py

index 85c8cdb57f78a845f555becc40ebbabab4295439..6168ea1eae57b5f4f52862c3fec043badec9e4a2 100644 (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
index 44c3ecd9a412a93c1d929dea5f0b2e0cce51b8c4..c213943887ccf498254f83d6b2affb6e5b542c3a 100644 (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