From 739aa5d9e7163e147b731af2fab01493f0fc95cf Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Fri, 3 Jan 2025 18:41:19 -0700 Subject: [PATCH] [sourcegen] Improve type hinting --- .../sourcegen/sourcegen/_HeaderFileParser.py | 34 ++++-- .../sourcegen/sourcegen/_SourceGenerator.py | 4 +- .../sourcegen/sourcegen/_TagFileParser.py | 81 ++++++------- .../sourcegen/sourcegen/_dataclasses.py | 53 +++++---- .../sourcegen/sourcegen/_orchestrate.py | 15 ++- .../sourcegen/clib/_CLibSourceGenerator.py | 107 +++++++++--------- .../sourcegen/sourcegen/clib/_Config.py | 9 +- .../csharp/_CSharpSourceGenerator.py | 15 +-- .../sourcegen/sourcegen/csharp/_Config.py | 11 +- .../sourcegen/yaml/_YamlSourceGenerator.py | 22 ++-- 10 files changed, 191 insertions(+), 160 deletions(-) diff --git a/interfaces/sourcegen/sourcegen/_HeaderFileParser.py b/interfaces/sourcegen/sourcegen/_HeaderFileParser.py index 696a2c925f..64658856d2 100644 --- a/interfaces/sourcegen/sourcegen/_HeaderFileParser.py +++ b/interfaces/sourcegen/sourcegen/_HeaderFileParser.py @@ -6,6 +6,8 @@ from pathlib import Path import logging import re +from typing import Iterable +from typing_extensions import Self from ._dataclasses import HeaderFile, Func, Recipe from ._helpers import read_config @@ -26,15 +28,17 @@ class HeaderFileParser: themselves are used for subsequent code scaffolding. """ - def __init__(self, path: Path, ignore_funcs: list[str] = None): + def __init__(self, path: Path, ignore_funcs: Iterable[str] = None) -> None: self._path = path self._ignore_funcs = ignore_funcs @classmethod - def headers_from_yaml(cls, ignore_files, ignore_funcs) -> list[HeaderFile]: + def headers_from_yaml( + cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str] + ) -> list[HeaderFile]: """Parse header file YAML configuration.""" - files = [ff for ff in _DATA_PATH.glob("*.yaml") if ff.name not in ignore_files] - files.sort() + files = sorted( + ff for ff in _DATA_PATH.glob("*.yaml") if ff.name not in ignore_files) return [cls(ff, ignore_funcs.get(ff.name, []))._parse_yaml() for ff in files] def _parse_yaml(self) -> HeaderFile: @@ -44,27 +48,33 @@ def read_docstring(): while True: line = fid.readline() if line.startswith("#"): - doc.append(line.lstrip("#").strip()) + doc.append(line.removeprefix("#").strip()) else: break if doc and doc[0].startswith("This file is part of "): return [] return doc + msg = f" parsing {self._path.name!r}" + _LOGGER.info(msg) config = read_config(self._path) + if self._ignore_funcs: + msg = f" ignoring {self._ignore_funcs!r}" + _LOGGER.info(msg) + recipes = [] prefix = config["prefix"] base = config["base"] parents = config.get("parents", []) derived = config.get("derived", []) for recipe in config["recipes"]: - if recipe['name'] in self._ignore_funcs: + if recipe["name"] in self._ignore_funcs: continue uses = recipe.get("uses", []) if not isinstance(uses, list): uses = [uses] recipes.append( - Recipe(recipe['name'], + Recipe(recipe["name"], recipe.get("implements", ""), uses, recipe.get("what", ""), @@ -79,7 +89,9 @@ def read_docstring(): read_docstring()) @classmethod - def headers_from_h(cls, ignore_files, ignore_funcs) -> list[HeaderFile]: + def headers_from_h( + cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str] + ) -> list[HeaderFile]: """Parse existing header file.""" files = [ff for ff in _CLIB_PATH.glob("*.h") if ff.name not in ignore_files + _CLIB_IGNORE] @@ -98,9 +110,11 @@ def _parse_h(self) -> HeaderFile: parsed = map(Func.from_str, c_functions) - _LOGGER.info(f" parsing {self._path.name!r}") + msg = f" parsing {self._path.name!r}" + _LOGGER.info(msg) if self._ignore_funcs: - _LOGGER.info(f" ignoring {self._ignore_funcs!r}") + msg = f" ignoring {self._ignore_funcs!r}" + _LOGGER.info(msg) parsed = [f for f in parsed if f.name not in self._ignore_funcs] diff --git a/interfaces/sourcegen/sourcegen/_SourceGenerator.py b/interfaces/sourcegen/sourcegen/_SourceGenerator.py index 33c47b18f6..cf79f12bcf 100644 --- a/interfaces/sourcegen/sourcegen/_SourceGenerator.py +++ b/interfaces/sourcegen/sourcegen/_SourceGenerator.py @@ -13,9 +13,9 @@ class SourceGenerator(metaclass=ABCMeta): """Specifies the interface of a language-specific SourceGenerator""" @abstractmethod - def __init__(self, out_dir: Path, config: dict, templates: dict): + def __init__(self, out_dir: Path, config: dict, templates: dict) -> None: pass @abstractmethod - def generate_source(self, headers_files: list[HeaderFile]): + def generate_source(self, headers_files: list[HeaderFile]) -> None: pass diff --git a/interfaces/sourcegen/sourcegen/_TagFileParser.py b/interfaces/sourcegen/sourcegen/_TagFileParser.py index 25c35150fb..1a0fb32c8a 100644 --- a/interfaces/sourcegen/sourcegen/_TagFileParser.py +++ b/interfaces/sourcegen/sourcegen/_TagFileParser.py @@ -6,6 +6,8 @@ import sys from pathlib import Path import re +from typing import Sequence, Iterable +from typing_extensions import Self import logging from dataclasses import dataclass import xml.etree.ElementTree as ET @@ -33,7 +35,7 @@ class TagInfo: anchor: str = "" #: doxygen anchor @classmethod - def from_xml(cls, qualified_name, xml): + def from_xml(cls: Self, qualified_name: str, xml: str) -> Self: """Create tag information based on XML data.""" base = "" if "::" in qualified_name: @@ -41,27 +43,27 @@ def from_xml(cls, qualified_name, xml): xml_tree = ET.fromstring(xml) return cls(base, - xml_tree.find('type').text, - xml_tree.find('name').text, - xml_tree.find('arglist').text, - xml_tree.find('anchorfile').text.replace(".html", ".xml"), - xml_tree.find('anchor').text) + xml_tree.find("type").text, + xml_tree.find("name").text, + xml_tree.find("arglist").text, + xml_tree.find("anchorfile").text.replace(".html", ".xml"), + xml_tree.find("anchor").text) - def __bool__(self): + def __bool__(self) -> bool: return all([self.type, self.name, self.arglist, self.anchorfile, self.anchor]) @property - def signature(self): + def signature(self) -> str: """Generate function signature based on tag information.""" return f"{self.type} {self.name}{self.arglist}" @property - def id(self): + def id(self) -> str: """Generate doxygen id.""" return f"{self.anchorfile.replace('.xml', '')}_1{self.anchor}" @property - def qualified_name(self): + def qualified_name(self) -> str: """Return qualified name.""" if self.base: return f"{self.base}::{self.name}" @@ -75,7 +77,7 @@ class TagDetails(TagInfo): location: str = "" #: File containing doxygen description briefdescription: str = "" #: Brief doxygen description - parameterlist: list[Param] = None #: Annotated doxygen parameter list + parameterlist: list[Param] | None = None #: Annotated doxygen parameter list class TagFileParser: @@ -91,16 +93,14 @@ def __init__(self, bases: dict[str, str]) -> None: _LOGGER.critical(msg) sys.exit(1) - with tag_file.open() as fid: - doxygen_tags = fid.read() - logging.info("Parsing doxygen tags...") + doxygen_tags = tag_file.read_text() self._parse_doxyfile(doxygen_tags, bases) - def _parse_doxyfile(self, doxygen_tags: str, bases: list[str]) -> None: + def _parse_doxyfile(self, doxygen_tags: str, bases: Sequence[str]) -> None: """Retrieve class and function information from Cantera namespace.""" - def xml_compounds(kind: str, names: list[str]) -> dict[str,str]: + def xml_compounds(kind: str, names: Sequence[str]) -> dict[str, str]: regex = re.compile(rf'') found = [] compounds = {} @@ -110,7 +110,7 @@ def xml_compounds(kind: str, names: list[str]) -> dict[str,str]: if compound_name in names: found.append(compound_name) compounds[compound_name] = compound - if not set(names) - set(found): + if not (set(names) - set(found)): return compounds missing = '", "'.join(set(names) - set(found)) msg = f"Missing {kind!r} compound(s):\n {missing!r}\nusing regex " @@ -130,15 +130,16 @@ def xml_compounds(kind: str, names: list[str]) -> dict[str,str]: unknown = set(bases) - set(class_names) if "', '".join(unknown): unknown = "', '".join(unknown) - _LOGGER.critical("Class(es) in configuration file are missing " - f"from tag file: {unknown!r}") + msg = ("Class(es) in configuration file are missing " + f"from tag file: {unknown!r}") + _LOGGER.critical(msg) exit(1) # Parse content of classes that are specified by the configuration file class_names = set(bases) & set(class_names) classes = xml_compounds("class", class_names) - def xml_members(kind: str, text: str, prefix="") -> dict[str, str]: + def xml_members(kind: str, text: str, prefix: str = "") -> dict[str, str]: regex = re.compile(rf'') functions = {} for func in re.findall(regex, text): @@ -161,7 +162,7 @@ def exists(self, cxx_func: str) -> bool: """Check whether doxygen tag exists.""" return cxx_func in self._known - def detect(self, name, bases, permissive=True): + def detect(self, name: str, bases: Iterable[str], permissive: bool = True) -> str: """Detect qualified method name.""" for base in bases: name_ = f"{base}::{name}" @@ -170,33 +171,35 @@ def detect(self, name, bases, permissive=True): if self.exists(name): return name if permissive: - return None - _LOGGER.critical(f"Unable to detect {name!r} in doxygen tags.") + return "" + msg = f"Unable to detect {name!r} in doxygen tags." + _LOGGER.critical(msg) exit(1) def tag_info(self, func_string: str) -> TagInfo: """Look up tag information based on (partial) function signature.""" cxx_func = func_string.split("(")[0].split(" ")[-1] if cxx_func not in self._known: - _LOGGER.critical(f"Could not find {cxx_func!r} in doxygen tag file.") + msg = f"Could not find {cxx_func!r} in doxygen tag file." + _LOGGER.critical(msg) sys.exit(1) ix = 0 if len(self._known[cxx_func]) > 1: # Disambiguate functions with same name # TODO: current approach does not use information on default arguments - known_args = [ET.fromstring(xml).find('arglist').text + known_args = [ET.fromstring(xml).find("arglist").text for xml in self._known[cxx_func]] known_args = [ArgList.from_xml(al).short_str() for al in known_args] - args = re.findall(re.compile(r'(?<=\().*(?=\))'), func_string) + args = re.findall(re.compile(r"(?<=\().*(?=\))"), func_string) if not args and "()" in known_args: # Candidate function without arguments exists ix = known_args.index("()") elif not args: # Function does not use arguments - known = '\n - '.join([""] + known_args) - _LOGGER.critical( - f"Need argument list to disambiguate {func_string!r}. " - f"possible matches are:{known}") + known = "\n - ".join([""] + known_args) + msg = (f"Need argument list to disambiguate {func_string!r}. " + f"possible matches are:{known}") + _LOGGER.critical(msg) sys.exit(1) else: args = f"({args[0]}" @@ -207,8 +210,8 @@ def tag_info(self, func_string: str) -> TagInfo: ix = i break if ix < 0: - _LOGGER.critical( - f"Unable to match {func_string!r} to known functions.") + msg = f"Unable to match {func_string!r} to known functions." + _LOGGER.critical(msg) sys.exit(1) return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix]) @@ -241,28 +244,28 @@ def tag_lookup(tag_info: TagInfo) -> TagDetails: """Retrieve tag details from doxygen tree.""" xml_file = _XML_PATH / tag_info.anchorfile if not xml_file.exists(): - msg = (f"XML file does not exist at expected location: {xml_file}") + msg = f"Tag file does not exist at expected location:\n {xml_file}" _LOGGER.error(msg) return TagDetails() - with xml_file.open() as fid: - xml_details = fid.read() - + xml_details = xml_file.read_text() id_ = tag_info.id regex = re.compile(rf'') matches = re.findall(regex, xml_details) if not matches: - _LOGGER.error(f"No XML matches found for {tag_info.qualified_name!r}") + msg = f"No XML matches found for {tag_info.qualified_name!r}" + _LOGGER.error(msg) return TagDetails() if len(matches) != 1: - _LOGGER.warning(f"Inconclusive XML matches found for {tag_info.qualified_name!r}") + msg = f"Inconclusive XML matches found for {tag_info.qualified_name!r}" + _LOGGER.warning(msg) matches = matches[:1] def no_refs(entry: str) -> str: # Remove stray XML markup that causes problems with xml.etree if "') + regex = re.compile(r"") for ref in re.findall(regex, entry): entry = entry.replace(ref, "") entry = entry.replace("", "").replace("", "") diff --git a/interfaces/sourcegen/sourcegen/_dataclasses.py b/interfaces/sourcegen/sourcegen/_dataclasses.py index 132e164d8e..7d02d9ab8b 100644 --- a/interfaces/sourcegen/sourcegen/_dataclasses.py +++ b/interfaces/sourcegen/sourcegen/_dataclasses.py @@ -7,6 +7,7 @@ import re from pathlib import Path from typing import Any, Iterator +from typing_extensions import Self from ._helpers import with_unpack_iter @@ -24,13 +25,12 @@ class Param: default: Any = None #: Default value (optional) @classmethod - def from_str(cls, param: str, doc: str="") -> 'Param': + def from_str(cls: Self, param: str, doc: str = "") -> Self: """Generate Param from parameter string.""" param = param.strip() default = None if "=" in param: - default = param[param.rfind("=")+1:] - param = param[:param.rfind("=")] + param, _, default = param.partition("=") parts = param.strip().rsplit(" ", 1) if len(parts) == 2 and parts[0] not in ["const", "virtual", "static"]: if "@param" not in doc: @@ -44,7 +44,7 @@ def from_str(cls, param: str, doc: str="") -> 'Param': return cls(param) @classmethod - def from_xml(cls, param: str) -> 'Param': + def from_xml(cls: Self, param: str) -> Self: """ Generate Param from XML string. @@ -80,12 +80,12 @@ def _split_arglist(arglist: str) -> tuple[str, str]: return "", "" spec = arglist[arglist.rfind(")") + 1:] # match text within parentheses - regex = re.compile(r'(?<=\().*(?=\))', flags=re.DOTALL) + regex = re.compile(r"(?<=\().*(?=\))", flags=re.DOTALL) arglist = re.findall(regex, arglist)[0] return arglist, spec @classmethod - def from_str(cls, arglist: str) -> 'ArgList': + def from_str(cls: Self, arglist: str) -> Self: """Generate ArgList from string argument list.""" arglist, spec = cls._split_arglist(arglist) if not arglist: @@ -93,30 +93,30 @@ def from_str(cls, arglist: str) -> 'ArgList': return cls([Param.from_str(arg) for arg in arglist.split(",")], spec) @classmethod - def from_xml(cls, arglist: str) -> 'ArgList': + def from_xml(cls: Self, arglist: str) -> Self: """Generate ArgList from XML string argument list.""" arglist, spec = cls._split_arglist(arglist) if not arglist: return cls([], spec) return cls([Param.from_xml(arg) for arg in arglist.split(",")], spec) - def __len__(self): + def __len__(self) -> int: return len(self.params) - def __getitem__(self, k): + def __getitem__(self, k: int) -> Param: return self.params[k] - def __iter__(self) -> "Iterator[Param]": + def __iter__(self) -> Iterator[Param]: return iter(self.params) def short_str(self) -> str: """String representation of the argument list without parameter names.""" - args = ', '.join([par.short_str() for par in self.params]) + args = ", ".join(par.short_str() for par in self.params) return f"({args}) {self.spec}".strip() def long_str(self) -> str: """String representation of the argument list with parameter names.""" - args = ', '.join([par.long_str() for par in self.params]) + args = ", ".join([par.long_str() for par in self.params]) return f"({args}) {self.spec}".strip() @@ -130,11 +130,11 @@ class Func: arglist: ArgList #: Argument list @classmethod - def from_str(cls, func: str) -> 'Func': + def from_str(cls: Self, func: str) -> Self: """Generate Func from declaration string of a function.""" func = func.rstrip(";").strip() - # match all characters before an opening parenthesis '(' or end of line - name = re.findall(r'.*?(?=\(|$)', func)[0] + # match all characters before an opening parenthesis "(" or end of line + name = re.findall(r".*?(?=\(|$)", func)[0] arglist = ArgList.from_str(func.replace(name, "").strip()) r_type = "" if " " in name: @@ -152,19 +152,18 @@ class CFunc(Func): """Represents an annotated function declaration in a C/C++ header file.""" brief: str = "" #: Brief description (optional) - implements: 'CFunc' = None #: Implemented C++ function/method (optional) + implements: Self = None #: Implemented C++ function/method (optional) returns: str = "" #: Description of returned value (optional) base: str = "" #: Qualified scope of function/method (optional) - uses: list['CFunc'] = None #: List of auxiliary C++ methods (optional) + uses: list[Self] | None = None #: List of auxiliary C++ methods (optional) @classmethod - def from_str(cls, func: str, brief="") -> 'CFunc': + def from_str(cls: Self, func: str, brief: str = "") -> Self: """Generate annotated CFunc from header block of a function.""" lines = func.split("\n") - func = super().from_str(lines[-1]) + func = Func.from_str(lines[-1]) if len(lines) == 1: - return func - brief = "" + return cls(*func, brief, None, "", "", []) returns = "" args = [] for ix, line in enumerate(lines[:-1]): @@ -188,7 +187,7 @@ def short_declaration(self) -> str: return f"{self.ret_type} {ret}" @property - def ret_param(self): + def ret_param(self) -> Param: """Assemble return parameter.""" return Param(self.ret_type, "", self.returns) @@ -224,12 +223,12 @@ class HeaderFile: prefix: str = "" #: prefix used for CLib function names base: str = "" #: base class of C++ methods (if applicable) - parents: list[str] = None #: list of C++ parent class(es) - derived: list[str] = None #: list of C++ specialization(s) - recipes: list[Recipe] = None #: list of header recipes read from YAML - docstring: list[str] = None #: lines representing docstring of YAML file + parents: list[str] | None = None #: list of C++ parent class(es) + derived: list[str] | None = None #: list of C++ specialization(s) + recipes: list[Recipe] | None = None #: list of header recipes read from YAML + docstring: list[str] | None = None #: lines representing docstring of YAML file - def output_name(self, auto="3", suffix=""): + def output_name(self, auto: str = "3", suffix: str = "") -> Path: """Return updated path.""" ret = self.path.parent / self.path.name.replace("_auto", auto) return ret.with_suffix(suffix) diff --git a/interfaces/sourcegen/sourcegen/_orchestrate.py b/interfaces/sourcegen/sourcegen/_orchestrate.py index 622c597e38..4919eab037 100644 --- a/interfaces/sourcegen/sourcegen/_orchestrate.py +++ b/interfaces/sourcegen/sourcegen/_orchestrate.py @@ -23,7 +23,7 @@ def format(self, record): return formatter.format(record) -def generate_source(lang: str, out_dir: str=None, verbose=False): +def generate_source(lang: str, out_dir: str = "", verbose = False) -> None: """Main entry point of sourcegen.""" loghandler = logging.StreamHandler(sys.stdout) loghandler.setFormatter(CustomFormatter()) @@ -42,13 +42,17 @@ def generate_source(lang: str, out_dir: str=None, verbose=False): ignore_files: list[str] = config.pop("ignore_files", []) ignore_funcs: dict[str, list[str]] = config.pop("ignore_funcs", {}) - if lang == 'clib': + msg = f"Starting sourcegen for {lang!r} API" + _LOGGER.info(msg) + + if lang == "clib": + # prepare for generation of CLib headers in main processing step files = HeaderFileParser.headers_from_yaml(ignore_files, ignore_funcs) - elif lang == 'csharp': + elif lang == "csharp": # csharp parses existing (traditional) CLib header files files = HeaderFileParser.headers_from_h(ignore_files, ignore_funcs) else: - # generate CLib headers from YAML specifications + # generate CLib headers from YAML specifications as a preprocessing step files = HeaderFileParser.headers_from_yaml(ignore_files, ignore_funcs) clib_root = Path(__file__).parent / "clib" clib_config = read_config(clib_root / "config.yaml") @@ -59,7 +63,8 @@ def generate_source(lang: str, out_dir: str=None, verbose=False): clib_scaffolder.resolve_tags(files) # find and instantiate the language-specific SourceGenerator - _LOGGER.info(f"Generating {lang!r} source files...") + msg = f"Generating {lang!r} source files..." + _LOGGER.info(msg) _, scaffolder_type = inspect.getmembers(module, lambda m: inspect.isclass(m) and issubclass(m, SourceGenerator))[0] scaffolder: SourceGenerator = scaffolder_type(out_dir, config, templates) diff --git a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py index 0f62de860c..05717d7ca7 100644 --- a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py +++ b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py @@ -23,21 +23,21 @@ class CLibSourceGenerator(SourceGenerator): _clib_bases: list[str] = None #: list of bases provided via YAML configurations - def __init__(self, out_dir: str, config: dict, templates: dict): + def __init__(self, out_dir: str, config: dict, templates: dict) -> None: self._out_dir = out_dir or None if self._out_dir is not None: self._out_dir = Path(out_dir) self._out_dir.mkdir(parents=True, exist_ok=True) - self._config = Config.from_parsed(**config) # typed config + self._config = Config.from_parsed(**config) self._templates = templates self._doxygen_tags = None @staticmethod - def _javadoc_comment(block): + def _javadoc_comment(block: str) -> str: """Build deblanked JavaDoc-style (C-style) comment block.""" block = ["/**"] + block.strip().split("\n") block = "\n * ".join(block).strip() + "\n */" - return "\n".join([line.rstrip() for line in block.split('\n')]) + return "\n".join([line.rstrip() for line in block.split("\n")]) def _scaffold_annotation(self, c_func: CFunc, what: str) -> str: """Build annotation block via Jinja.""" @@ -45,7 +45,7 @@ def _scaffold_annotation(self, c_func: CFunc, what: str) -> str: par_template = loader.from_string(self._templates["clib-param"]) template = loader.from_string(self._templates["clib-comment"]) - def param(item: Param): + def param(item: Param) -> str: ret = par_template.render(par=item) return f"{ret:<19} {item.description}" @@ -73,7 +73,8 @@ def _handle_crosswalk(self, what: str, crosswalk: dict, derived: list[str]) -> s # successful crosswalk with cabinet object return cabinet - _LOGGER.critical(f"Failed crosswalk for handle type {what!r} using {classes}.") + msg = f"Failed crosswalk for handle type {what!r} using {classes}." + _LOGGER.critical(msg) sys.exit(1) def _ret_crosswalk( @@ -117,7 +118,8 @@ def _ret_crosswalk( f"Handle to stored {handle} object or -1 for exception handling.") return returns, [] - _LOGGER.critical(f"Failed crosswalk for return type {what!r}.") + msg = f"Failed crosswalk for return type {what!r}." + _LOGGER.critical(msg) sys.exit(1) def _prop_crosswalk(self, par_list: list[Param]) -> list[Param]: @@ -155,7 +157,8 @@ def _prop_crosswalk(self, par_list: list[Param]) -> list[Param]: params.append( Param("int", par.name, description.strip(), par.direction)) else: - _LOGGER.critical(f"Failed crosswalk for argument type {what!r}.") + msg = f"Failed crosswalk for argument type {what!r}." + _LOGGER.critical(msg) sys.exit(1) return params @@ -168,7 +171,7 @@ def _reverse_crosswalk(c_func: CFunc, base: str) -> tuple[dict[str, str], set[st buffer = [] bases = set() - def shared_object(cxx_type): + def shared_object(cxx_type) -> str: """Extract object type from shared_ptr.""" if "shared_ptr<" not in cxx_type: return None @@ -200,9 +203,9 @@ def shared_object(cxx_type): f"{c_args[c_ix].name});", "int(out.size()) + 1"] # include \0 else: - _LOGGER.critical(f"Scaffolding failed for {c_func.name!r}: reverse " - f"crosswalk not implemented for {cxx_type!r}:\n" - f"{c_func.declaration()}") + msg = (f"Scaffolding failed for {c_func.name!r}: reverse crosswalk " + f"not implemented for {cxx_type!r}:\n{c_func.declaration()}") + _LOGGER.critical(msg) exit(1) break @@ -214,8 +217,9 @@ def shared_object(cxx_type): elif c_name.endswith("Len"): check_array = True else: - _LOGGER.critical(f"Scaffolding failed for {c_func.name!r}: " - f"unexpected behavior for {c_name!r}.") + msg = (f"Scaffolding failed for {c_func.name!r}: " + f"unexpected behavior for {c_name!r}.") + _LOGGER.critical(msg) exit(1) continue @@ -243,8 +247,9 @@ def shared_object(cxx_type): # Can be passed directly; example: double *const args.append(c_name) else: - _LOGGER.critical(f"Scaffolding failed for {c_func.name!r}: reverse " - f"crosswalk not implemented for {cxx_type!r}.") + msg = (f"Scaffolding failed for {c_func.name!r}: reverse " + f"crosswalk not implemented for {cxx_type!r}.") + _LOGGER.critical(msg) exit(1) check_array = False elif "shared_ptr" in cxx_type: @@ -330,14 +335,15 @@ def _scaffold_body(self, c_func: CFunc, recipe: Recipe) -> tuple[str, set[str]]: self._templates[f"clib-reserved-{recipe.name}-cpp"]) else: - _LOGGER.critical(f"{recipe.what!r} not implemented: {c_func.name!r}.") + msg = f"{recipe.what!r} not implemented: {c_func.name!r}." + _LOGGER.critical(msg) exit(1) return template.render(**args), bases - def _resolve_recipe(self, recipe: Recipe, quiet: bool=True) -> CFunc: + def _resolve_recipe(self, recipe: Recipe) -> CFunc: """Build CLib header from recipe and doxygen annotations.""" - def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: + def merge_params(implements: str, cxx_func: CFunc) -> tuple[list[Param], int]: """Create preliminary CLib argument list.""" obj_handle = [] if "::" in implements: @@ -364,8 +370,8 @@ def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: if recipe.name in reserved: recipe.what = "reserved" loader = Environment(loader=BaseLoader) - if not quiet: - _LOGGER.debug(f" generating {func_name!r} -> {recipe.what}") + msg = f" generating {func_name!r} -> {recipe.what}" + _LOGGER.debug(msg) header = loader.from_string( self._templates[f"clib-reserved-{recipe.name}-h"] ).render(base=recipe.base, prefix=recipe.prefix) @@ -388,8 +394,8 @@ def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: "continuing with auto-generated code.") if recipe.implements: - if not quiet: - _LOGGER.debug(f" generating {func_name!r} -> {recipe.implements}") + msg = f" generating {func_name!r} -> {recipe.implements}" + _LOGGER.debug(msg) cxx_func = self._doxygen_tags.cxx_func(recipe.implements) # Convert C++ return type to format suitable for crosswalk: @@ -405,9 +411,9 @@ def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: # Autodetection of CLib function purpose ("what") cxx_arglen = len(cxx_func.arglist) if not cxx_func.base: - if cxx_func.name.startswith("new") and \ + if (cxx_func.name.startswith("new") and any(base in cxx_func.ret_type - for base in [recipe.base] + recipe.derived): + for base in [recipe.base] + recipe.derived)): recipe.what = "constructor" else: recipe.what = "function" @@ -426,7 +432,6 @@ def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: recipe.what = "method" else: _LOGGER.critical("Unable to auto-detect function type.") - # recipe.what = "function" exit(1) elif recipe.name == "del" and not recipe.what: @@ -434,8 +439,8 @@ def merge_params(implements, cxx_func: CFunc) -> tuple[list[Param], int]: if recipe.what in ["destructor", "noop"]: # these function types don't have direct C++ equivalents - if not quiet: - _LOGGER.debug(f" generating {func_name!r} -> {recipe.what}") + msg = f" generating {func_name!r} -> {recipe.what}" + _LOGGER.debug(msg) if recipe.what == "noop": args = [] brief= "No operation." @@ -459,12 +464,14 @@ def _write_header(self, headers: HeaderFile) -> None: loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True) filename = headers.output_name(suffix=".h", auto="3") - _LOGGER.info(f" scaffolding {filename.name!r}") + msg = f" scaffolding {filename.name!r}" + _LOGGER.info(msg) template = loader.from_string(self._templates["clib-definition"]) declarations = [] for c_func, recipe in zip(headers.funcs, headers.recipes): - _LOGGER.debug(f" scaffolding {c_func.name!r} header") + msg = f" scaffolding {c_func.name!r} header" + _LOGGER.debug(msg) declarations.append( template.render( declaration=c_func.declaration(), @@ -479,25 +486,25 @@ def _write_header(self, headers: HeaderFile) -> None: out = (Path(self._out_dir) / "include" / "cantera" / "clib_experimental" / filename.name) - _LOGGER.info(f" writing {filename.name!r}") - if not out.parent.exists(): - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "wt", encoding="utf-8") as stream: - stream.write(output) - stream.write("\n") + msg = f" writing {filename.name!r}" + _LOGGER.info(msg) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(output + "\n") def _write_implementation(self, headers: HeaderFile) -> None: """Parse header specification and generate implementation file.""" loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True) filename = headers.output_name(suffix=".cpp", auto="3") - _LOGGER.info(f" scaffolding {filename.name!r}") + msg = f" scaffolding {filename.name!r}" + _LOGGER.info(msg) template = loader.from_string(self._templates["clib-implementation"]) implementations = [] other = set() for c_func, recipe in zip(headers.funcs, headers.recipes): - _LOGGER.debug(f" scaffolding {c_func.name!r} implementation") + msg = f" scaffolding {c_func.name!r} implementation" + _LOGGER.debug(msg) body, bases = self._scaffold_body(c_func, recipe) implementations.append( template.render(declaration=c_func.declaration(),body=body)) @@ -519,14 +526,12 @@ def _write_implementation(self, headers: HeaderFile) -> None: includes=includes, other=other, str_utils=str_utils) out = Path(self._out_dir) / "src" / "clib_experimental" / filename.name - _LOGGER.info(f" writing {filename.name!r}") - if not out.parent.exists(): - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "wt", encoding="utf-8") as stream: - stream.write(output) - stream.write("\n") - - def resolve_tags(self, headers_files: list[HeaderFile], quiet: bool=True): + msg = f" writing {filename.name!r}" + _LOGGER.info(msg) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(output + "\n") + + def resolve_tags(self, headers_files: list[HeaderFile]) -> None: """Resolve doxygen tags.""" def get_bases() -> tuple[list[str], list[str]]: bases = set() @@ -541,16 +546,16 @@ def get_bases() -> tuple[list[str], list[str]]: self._doxygen_tags = TagFileParser(classes) for headers in headers_files: - if not quiet: - _LOGGER.info(f" resolving recipes in {headers.path.name!r}:") + msg = f" resolving recipes in {headers.path.name!r}:" + _LOGGER.info(msg) c_funcs = [] for recipe in headers.recipes: - c_funcs.append(self._resolve_recipe(recipe, quiet=quiet)) + c_funcs.append(self._resolve_recipe(recipe)) headers.funcs = c_funcs - def generate_source(self, headers_files: list[HeaderFile]): + def generate_source(self, headers_files: list[HeaderFile]) -> None: """Generate output.""" - self.resolve_tags(headers_files, quiet=False) + self.resolve_tags(headers_files) for headers in headers_files: self._write_header(headers) diff --git a/interfaces/sourcegen/sourcegen/clib/_Config.py b/interfaces/sourcegen/sourcegen/clib/_Config.py index 10173dfcdf..1f70e69402 100644 --- a/interfaces/sourcegen/sourcegen/clib/_Config.py +++ b/interfaces/sourcegen/sourcegen/clib/_Config.py @@ -4,6 +4,7 @@ # at https://cantera.org/license.txt for license and copyright information. from dataclasses import dataclass +from typing_extensions import Self @dataclass(frozen=True) @@ -34,13 +35,13 @@ class Config: "const string&": "const char*", "shared_ptr": "int", "const shared_ptr": "int", - "const vector&": 'const double*', - "const vector>&": 'int[]', + "const vector&": "const double*", + "const vector>&": "int[]", } - includes: dict[str,list[str]] + includes: dict[str, list[str]] @classmethod - def from_parsed(cls, *, includes=None) -> 'Config': + def from_parsed(cls: Self, *, includes = None) -> Self: """Ensure that configurations are correct.""" return cls(includes or {}) diff --git a/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py b/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py index ba67cc289b..fd875b6faa 100644 --- a/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py +++ b/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py @@ -20,7 +20,7 @@ class CSharpSourceGenerator(SourceGenerator): """The SourceGenerator for scaffolding C# files for the .NET interface""" - def __init__(self, out_dir: str, config: dict, templates: dict): + def __init__(self, out_dir: str, config: dict, templates: dict) -> None: if not out_dir: _logger.critical("Non-empty string identifying output path required.") sys.exit(1) @@ -159,7 +159,7 @@ def _write_file(self, file_name: str, template_name: str, **kwargs) -> None: self._out_dir.joinpath(file_name).write_text(contents, encoding="utf-8") - def _scaffold_interop(self, header_file_path: Path, cs_funcs: list[CsFunc]): + def _scaffold_interop(self, header_file_path: Path, cs_funcs: list[CsFunc]) -> None: template = _loader.from_string(self._templates["csharp-interop-func"]) function_list = [ template.render(unsafe=func.unsafe(), declaration=func.declaration()) @@ -169,7 +169,8 @@ def _scaffold_interop(self, header_file_path: Path, cs_funcs: list[CsFunc]): self._write_file( file_name, "csharp-scaffold-interop", cs_functions=function_list) - def _scaffold_handles(self, header_file_path: Path, handles: dict[str, str]): + def _scaffold_handles( + self, header_file_path: Path, handles: dict[str, str]) -> None: template = _loader.from_string(self._templates["csharp-base-handle"]) handle_list = [ template.render(class_name=key, release_func_name=val) @@ -179,7 +180,7 @@ def _scaffold_handles(self, header_file_path: Path, handles: dict[str, str]): self._write_file( file_name, "csharp-scaffold-handles", cs_handles=handle_list) - def _scaffold_derived_handles(self): + def _scaffold_derived_handles(self) -> None: template = _loader.from_string(self._templates["csharp-derived-handle"]) handle_list = [ template.render(derived_class_name=key, base_class_name=val) @@ -190,7 +191,7 @@ def _scaffold_derived_handles(self): file_name, "csharp-scaffold-handles", cs_handles=handle_list) def _scaffold_wrapper_class(self, clib_area: str, props: dict[str, str], - known_funcs: dict[str, CsFunc]): + known_funcs: dict[str, CsFunc]) -> None: property_list = [ self._get_property_text(clib_area, c_name, cs_name, known_funcs) for c_name, cs_name in props.items()] @@ -203,7 +204,7 @@ def _scaffold_wrapper_class(self, clib_area: str, props: dict[str, str], wrapper_class_name=wrapper_class_name, handle_class_name=handle_class_name, cs_properties=property_list) - def generate_source(self, headers_files: list[HeaderFile]): + def generate_source(self, headers_files: list[HeaderFile]) -> None: self._out_dir.mkdir(parents=True, exist_ok=True) known_funcs: dict[str, list[CsFunc]] = {} @@ -224,5 +225,5 @@ def generate_source(self, headers_files: list[HeaderFile]): self._scaffold_derived_handles() - for (clib_area, props) in self._config.wrapper_classes.items(): + for clib_area, props in self._config.wrapper_classes.items(): self._scaffold_wrapper_class(clib_area, props, known_funcs) diff --git a/interfaces/sourcegen/sourcegen/csharp/_Config.py b/interfaces/sourcegen/sourcegen/csharp/_Config.py index d0249fc7fc..f1f1db1001 100644 --- a/interfaces/sourcegen/sourcegen/csharp/_Config.py +++ b/interfaces/sourcegen/sourcegen/csharp/_Config.py @@ -2,11 +2,12 @@ # at https://cantera.org/license.txt for license and copyright information. from dataclasses import dataclass +from typing_extensions import Self @dataclass(frozen=True) class Config: - """Provides configuration info for the CSharpSourceGenerator class""" + """Provides configuration info for the CSharpSourceGenerator class.""" ret_type_crosswalk = { @@ -32,8 +33,10 @@ class Config: wrapper_classes: dict[str, dict[str, str]] @classmethod - def from_parsed(cls, *, - class_crosswalk=None, class_accessors=None, - derived_handles=None, wrapper_classes=None): + def from_parsed(cls: Self, *, + class_crosswalk: dict[str, str] | None = None, + class_accessors: dict[str, str] | None = None, + derived_handles: dict[str, str] | None = None, + wrapper_classes: dict[str, str] | None = None): return cls(class_crosswalk or {}, class_accessors or {}, derived_handles or {}, wrapper_classes or {}) diff --git a/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py b/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py index 74eeb43e14..ab0aa981da 100644 --- a/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py +++ b/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py @@ -23,7 +23,7 @@ class YamlSourceGenerator(SourceGenerator): """The SourceGenerator for generating CLib.""" - def __init__(self, out_dir: str, config: dict, templates: dict): + def __init__(self, out_dir: str, config: dict, templates: dict) -> None: self._out_dir = out_dir or None if self._out_dir is not None: self._out_dir = Path(out_dir) @@ -31,14 +31,15 @@ def __init__(self, out_dir: str, config: dict, templates: dict): self._config = config self._templates = templates - def _write_yaml(self, headers: HeaderFile): + def _write_yaml(self, headers: HeaderFile) -> None: """Parse header file and generate YAML output.""" loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True) definition = loader.from_string(self._templates["yaml-definition"]) declarations = [] for c_func, recipe in zip(headers.funcs, headers.recipes): - _LOGGER.debug(f" scaffolding {c_func.name!r} implementation") + msg = f" scaffolding {c_func.name!r} implementation" + _LOGGER.debug(msg) implements = "" if isinstance(c_func.implements, CFunc): implements = c_func.implements.short_declaration() @@ -52,15 +53,14 @@ def _write_yaml(self, headers: HeaderFile): output = template.render(filename=filename.name, header_entries=declarations) out = Path(self._out_dir) / "yaml" / filename.name - _LOGGER.info(f" writing {filename.name!r}") - if not out.parent.exists(): - out.parent.mkdir(parents=True, exist_ok=True) - with open(out, "wt", encoding="utf-8") as stream: - stream.write(output) - stream.write("\n") + msg = f" writing {filename.name!r}" + _LOGGER.info(msg) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(output + "\n") - def generate_source(self, headers_files: list[HeaderFile]): + def generate_source(self, headers_files: list[HeaderFile]) -> None: """Generate output.""" for headers in headers_files: - _LOGGER.info(f" parsing functions in {headers.path.name!r}") + msg = f" parsing functions in {headers.path.name!r}" + _LOGGER.info(msg) self._write_yaml(headers)