]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-113317: Rework Argument Clinic cpp.py error handling (#113525)
authorErlend E. Aasland <erlend@python.org>
Wed, 27 Dec 2023 21:43:19 +0000 (22:43 +0100)
committerGitHub <noreply@github.com>
Wed, 27 Dec 2023 21:43:19 +0000 (21:43 +0000)
Rework error handling in the C preprocessor helper. Instead of monkey-
patching the cpp.Monitor.fail() method from within clinic.py, rewrite
cpp.py to use a subclass of the ClinicError exception. As a side-effect,
ClinicError is moved into Tools/clinic/libclinic/errors.py.

Yak-shaving in preparation for putting cpp.py into libclinic.

Lib/test/test_clinic.py
Tools/clinic/clinic.py
Tools/clinic/cpp.py
Tools/clinic/libclinic/__init__.py
Tools/clinic/libclinic/errors.py [new file with mode: 0644]

index 21f56fe0195e690ababde1aeeb4dc266c876c857..3d6816d73d45bc5b21ddf30f4ea0466b83668dac 100644 (file)
@@ -22,7 +22,7 @@ with test_tools.imports_under_tool('clinic'):
 
 
 def _make_clinic(*, filename='clinic_tests'):
-    clang = clinic.CLanguage(None)
+    clang = clinic.CLanguage(filename)
     c = clinic.Clinic(clang, filename=filename, limited_capi=False)
     c.block_parser = clinic.BlockParser('', clang)
     return c
@@ -3920,7 +3920,7 @@ class ClinicReprTests(unittest.TestCase):
         self.assertEqual(repr(parameter), "<clinic.Parameter 'bar'>")
 
     def test_Monitor_repr(self):
-        monitor = clinic.cpp.Monitor()
+        monitor = clinic.cpp.Monitor("test.c")
         self.assertRegex(repr(monitor), r"<clinic.Monitor \d+ line=0 condition=''>")
 
         monitor.line_number = 42
index f004bec3cce8f64baf4920c9d8c129fe00f4fa58..82efff56eda756fa2cd6be39f5ec03c6b5dd583f 100755 (executable)
@@ -53,6 +53,7 @@ from typing import (
 
 # Local imports.
 import libclinic
+from libclinic import ClinicError
 
 
 # TODO:
@@ -94,27 +95,6 @@ NULL = Null()
 TemplateDict = dict[str, str]
 
 
-@dc.dataclass
-class ClinicError(Exception):
-    message: str
-    _: dc.KW_ONLY
-    lineno: int | None = None
-    filename: str | None = None
-
-    def __post_init__(self) -> None:
-        super().__init__(self.message)
-
-    def report(self, *, warn_only: bool = False) -> str:
-        msg = "Warning" if warn_only else "Error"
-        if self.filename is not None:
-            msg += f" in file {self.filename!r}"
-        if self.lineno is not None:
-            msg += f" on line {self.lineno}"
-        msg += ":\n"
-        msg += f"{self.message}\n"
-        return msg
-
-
 @overload
 def warn_or_fail(
     *args: object,
@@ -669,7 +649,6 @@ class CLanguage(Language):
     def __init__(self, filename: str) -> None:
         super().__init__(filename)
         self.cpp = cpp.Monitor(filename)
-        self.cpp.fail = fail  # type: ignore[method-assign]
 
     def parse_line(self, line: str) -> None:
         self.cpp.writeline(line)
index 16eee6fc399491466f7c3bbb4e56b96201cdfe1d..659099056cd46cda97bec8cb49dd0b5221ffbcdd 100644 (file)
@@ -3,6 +3,8 @@ import re
 import sys
 from typing import NoReturn
 
+from libclinic.errors import ParseError
+
 
 TokenAndCondition = tuple[str, str]
 TokenStack = list[TokenAndCondition]
@@ -32,7 +34,7 @@ class Monitor:
 
     Anyway this implementation seems to work well enough for the CPython sources.
     """
-    filename: str | None = None
+    filename: str
     _: dc.KW_ONLY
     verbose: bool = False
 
@@ -59,14 +61,8 @@ class Monitor:
         """
         return " && ".join(condition for token, condition in self.stack)
 
-    def fail(self, *a: object) -> NoReturn:
-        if self.filename:
-            filename = " " + self.filename
-        else:
-            filename = ''
-        print("Error at" + filename, "line", self.line_number, ":")
-        print("   ", ' '.join(str(x) for x in a))
-        sys.exit(-1)
+    def fail(self, msg: str) -> NoReturn:
+        raise ParseError(msg, filename=self.filename, lineno=self.line_number)
 
     def writeline(self, line: str) -> None:
         self.line_number += 1
@@ -74,7 +70,7 @@ class Monitor:
 
         def pop_stack() -> TokenAndCondition:
             if not self.stack:
-                self.fail("#" + token + " without matching #if / #ifdef / #ifndef!")
+                self.fail(f"#{token} without matching #if / #ifdef / #ifndef!")
             return self.stack.pop()
 
         if self.continuation:
@@ -145,7 +141,7 @@ class Monitor:
 
         if token in {'if', 'ifdef', 'ifndef', 'elif'}:
             if not condition:
-                self.fail("Invalid format for #" + token + " line: no argument!")
+                self.fail(f"Invalid format for #{token} line: no argument!")
             if token in {'if', 'elif'}:
                 if not is_a_simple_defined(condition):
                     condition = "(" + condition + ")"
@@ -155,7 +151,8 @@ class Monitor:
             else:
                 fields = condition.split()
                 if len(fields) != 1:
-                    self.fail("Invalid format for #" + token + " line: should be exactly one argument!")
+                    self.fail(f"Invalid format for #{token} line: "
+                              "should be exactly one argument!")
                 symbol = fields[0]
                 condition = 'defined(' + symbol + ')'
                 if token == 'ifndef':
index 0c3c6840901a42cfaca063306a8dd8ef63152a7b..d4e7a0c5cf7b7656a50ce246c573ef2e6aa7b1e0 100644 (file)
@@ -1,5 +1,8 @@
 from typing import Final
 
+from .errors import (
+    ClinicError,
+)
 from .formatting import (
     SIG_END_MARKER,
     c_repr,
@@ -15,6 +18,9 @@ from .formatting import (
 
 
 __all__ = [
+    # Error handling
+    "ClinicError",
+
     # Formatting helpers
     "SIG_END_MARKER",
     "c_repr",
diff --git a/Tools/clinic/libclinic/errors.py b/Tools/clinic/libclinic/errors.py
new file mode 100644 (file)
index 0000000..afb21b0
--- /dev/null
@@ -0,0 +1,26 @@
+import dataclasses as dc
+
+
+@dc.dataclass
+class ClinicError(Exception):
+    message: str
+    _: dc.KW_ONLY
+    lineno: int | None = None
+    filename: str | None = None
+
+    def __post_init__(self) -> None:
+        super().__init__(self.message)
+
+    def report(self, *, warn_only: bool = False) -> str:
+        msg = "Warning" if warn_only else "Error"
+        if self.filename is not None:
+            msg += f" in file {self.filename!r}"
+        if self.lineno is not None:
+            msg += f" on line {self.lineno}"
+        msg += ":\n"
+        msg += f"{self.message}\n"
+        return msg
+
+
+class ParseError(ClinicError):
+    pass