From 542362bc306d5fa5dc574631e98fae4bc3d846ac Mon Sep 17 00:00:00 2001 From: Eero Vaher Date: Thu, 21 May 2026 15:56:25 +0200 Subject: [PATCH 1/2] Raise a better error if an ERFA test is not found Among other things `erfa_generator` translates the tests in an ERFA C file into a `pytest` test suite that checks `pyerfa` ufuncs. The source code for each test is found with a regex, but so far `erfa_generator` has simply assumed a match is always found. This assumption is true with the current version of ERFA, but it might not be true with all future versions. From now on if the test for an ERFA function cannot be found the error message will contain the function's name. --- erfa_generator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erfa_generator.py b/erfa_generator.py index 94bc80a..5de6d59 100644 --- a/erfa_generator.py +++ b/erfa_generator.py @@ -356,6 +356,8 @@ def __init__(self, func: Function, t_erfa_c: str) -> None: # and the first line starting with '}' or ' }'. pattern = rf"^static void t_{func.pyname}\(" + r".+?(^\{.+?^\s?\})" search = re.search(pattern, t_erfa_c, flags=re.DOTALL | re.MULTILINE) + if search is None: + raise RuntimeError(f"cannot find the test for {func.name}") self.lines = search.group(1).split('\n') self.dt_pv_vars: Final = frozenset( re.findall(r"(\w+)\[2\]\[3\]", search.group(1)) From 5d54e5893006e6b116ad8d66fdad75db8d2656d7 Mon Sep 17 00:00:00 2001 From: Eero Vaher Date: Thu, 21 May 2026 16:27:49 +0200 Subject: [PATCH 2/2] Remove `TestFunction.pre_process_lines()` The method was meant to adjust the source code of an ERFA test written in C into a more convenient format, but using Python's regex functionality allows conversion into an even more convenient format with much less code. --- erfa_generator.py | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/erfa_generator.py b/erfa_generator.py index 5de6d59..3c1315d 100644 --- a/erfa_generator.py +++ b/erfa_generator.py @@ -354,31 +354,16 @@ def __init__(self, func: Function, t_erfa_c: str) -> None: # Get lines that test the given erfa function: capture everything # between a line starting with '{' after the test function definition # and the first line starting with '}' or ' }'. - pattern = rf"^static void t_{func.pyname}\(" + r".+?(^\{.+?^\s?\})" - search = re.search(pattern, t_erfa_c, flags=re.DOTALL | re.MULTILINE) + search = re.search( + rf"^static void t_{func.pyname}\(" + r".+?^\{(.+?)^\s?\}", + t_erfa_c, + re.DOTALL | re.MULTILINE, + ) if search is None: raise RuntimeError(f"cannot find the test for {func.name}") - self.lines = search.group(1).split('\n') - self.dt_pv_vars: Final = frozenset( - re.findall(r"(\w+)\[2\]\[3\]", search.group(1)) - ) - - def pre_process_lines(self): - """Basic pre-processing. - - Combine multi-part lines, strip braces, semi-colons, empty lines. - """ - lines = [] - line = '' - for part in self.lines: - part = part.strip() - if part in ('', '{', '}'): - continue - line += part + ' ' - if part.endswith(';'): - lines.append(line.strip()[:-1]) - line = '' - return lines + source = re.sub(r"\s\s+", " ", search.group(1)) + self.lines: Final = re.findall(r"\s(.*?);", source, re.DOTALL) + self.dt_pv_vars: Final = frozenset(re.findall(r"(\w+)\[2\]\[3\]", source)) def define_arrays(self, line): """Check variable definition line for items also needed in python. @@ -419,10 +404,8 @@ def to_python(self): # TODO: this is quite hacky right now! Would be good to let function # calls be understood by the Function class. - # Collect actual code lines, without ";", braces, etc. - lines = self.pre_process_lines() out = [] - for line in lines: + for line in self.lines: # In ldn ufunc, the number of bodies is inferred from the array size, # so no need to keep the definition. if line == "n = 3" and self.func.pyname == "ldn": @@ -455,11 +438,11 @@ def to_python(self): .replace("s, '+'", "s[0], b'+'") # Rather hacky... .strip()) - if m := re.match(r"viv ?\( ?([\w\[\]]+), +(.+?),", line): + if m := re.match(r"viv ?\( ?([\w\[\]]+), (.+?),", line): line = f"assert {m.group(1)} == {m.group(2)}" elif m := re.match( - r"vvd\( ?(.+) ?, +([\d\.e-]+), *([\d\.e-]+), .+?, .+?, +status\)", line + r"vvd\( ?(.+) ?, ([\d\.e-]+), ?([\d\.e-]+), .+?, .+?, status\)", line ): expr = m.group(1).replace( self.func.name, f"erfa_ufunc.{self.func.pyname}" @@ -511,8 +494,6 @@ def to_python(self): # Input number setting. elif '=' in line: - # Small clean-up. - line = line.replace('= ', '= ') # Hack to make astrom element assignment work. if line.startswith('astrom'): out.append('astrom = np.zeros((), erfa_ufunc.dt_eraASTROM).view(np.recarray)')