diff --git a/interfaces/sourcegen/sourcegen/_TagFileParser.py b/interfaces/sourcegen/sourcegen/_TagFileParser.py index 46b2614e9c..d3e5c890d8 100644 --- a/interfaces/sourcegen/sourcegen/_TagFileParser.py +++ b/interfaces/sourcegen/sourcegen/_TagFileParser.py @@ -69,6 +69,16 @@ def qualified_name(self): return self.name +@dataclass(frozen=True) +@with_unpack_iter +class TagDetails(TagInfo): + """Create tag information based on XML data.""" + + location: str = "" #: file containing doxygen description + briefdescription: str = "" #: brief doxygen description + parameterlist: list[Param] = None #: annotated doxygen parameter list + + class TagFileParser: """Class handling contents of doxygen tag file.""" @@ -101,9 +111,8 @@ def xml_compounds(kind: str, names: list[str]) -> dict[str,str]: unknown = set(bases) - set(class_names) if unknown: unknown = "', '".join(unknown) - _logger.warning( - "Class(es) in configuration file are missing from tag file: '%s'", - unknown) + _logger.warning("Class(es) in configuration file are missing " + f"from tag file: {unknown!r}") # Parse content of classes that are specified by the configuration file class_names = set(bases) & set(class_names) @@ -144,7 +153,7 @@ 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"Did not find {cxx_func!r} in doxygen tag file.") + _logger.critical(f"Could not find {cxx_func!r} in doxygen tag file.") sys.exit(1) ix = 0 if len(self._known[cxx_func]) > 1: @@ -174,28 +183,97 @@ def tag_info(self, func_string: str) -> TagInfo: return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix]) -def xml_tag(tag: str, text: str, suffix: str="", index=0) -> str: - """Extract content enclosed between XML tags, optionally skipping matches.""" - if suffix: - suffix = f" {suffix.strip()}" - regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)', flags=re.DOTALL) - match = re.findall(regex, text) - if index >= len(match): - return "" # not enough matches found - return match[index].strip() - - -def xml_tags(tag: str, text: str, suffix: str="") -> list[str]: +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}") + _logger.error(msg) + return TagDetails() + + with xml_file.open() as fid: + xml_details = fid.read() + + id_ = tag_info.id + regex = re.compile(rf'<memberdef kind="function" id="{id_}"[\s\S]*?</memberdef>') + matches = re.findall(regex, xml_details) + + if not matches: + _logger.error(f"No XML matches found for {tag_info.qualified_name!r}") + return TagDetails() + if len(matches) != 1: + _logger.error(f"Inconclusive XML matches found for {tag_info.qualified_name!r}") + return TagDetails() + + def cleanup(entry: str) -> str: + # Remove stray XML markup + if entry.startswith("<para>"): + entry = xml_tag("para", entry) + if "<ref" in entry: + regex = re.compile(r'<ref [\s\S]*?>') + for ref in re.findall(regex, entry): + entry = entry.replace(ref, "<ref>") + entry = entry.replace("<ref>", "").replace("</ref>", "") + return entry + + def resolve_parameteritem(par_map: str) -> list[Param]: + # Resolve/flatten parameter list + name_lines = xml_tag("parameternamelist", par_map).split("\n") + regex = re.compile(r'(?<=<parametername)(.*?)(?=>)') + names = [] + directions = [] + for name_line in name_lines: + direction = re.findall(regex, name_line)[0] + if "=" in direction: + name_line = name_line.replace(direction, "") + direction = direction.split("=")[1].strip('"') + directions.append(direction) + names.append(xml_tag("parametername", name_line)) + description = cleanup(xml_tag("parameterdescription", par_map)) + return [Param("", n, description, d) for n, d in zip(names, directions)] + + xml = matches[0] + par_list = [] + par_block = xml_tag("parameterlist", xml, suffix='kind="param"') + if par_block: + for par_map in xml_tags("parameteritem", par_block): + par_list.extend(resolve_parameteritem(par_map)) + + def xml_attribute(attr: str, text: str, *, entry: str) -> str: + """Extract XML attribute.""" + regex = re.compile(rf'(?<=<{attr} )(.*?)(?=/>)', flags=re.DOTALL) + match = re.findall(regex, text) + if not match: + return "" # not enough matches found + entries = dict([tuple(_.split("=")) for _ in match[0].strip().split(" ")]) + return entries.get(entry, "").strip('"') + + return TagDetails(*tag_info, + xml_attribute("location", xml, entry="file"), + cleanup(xml_tag("briefdescription", xml)), + par_list) + + +def xml_tag(tag: str, text: str, suffix: str="") -> str: + """Extract first match of content enclosed between XML tags.""" + match = xml_tags(tag, text, suffix, True) + if match: + return match[0].strip() + return "" # not enough matches found + + +def xml_tags(tag: str, text: str, suffix: str="", permissive: bool=False) -> list[str]: """Extract list of content enclosed by XML tags.""" if suffix: suffix = f" {suffix.strip()}" regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)', flags=re.DOTALL|re.MULTILINE) matched = re.findall(regex, text) - if not matched: - blanks = text.split("\n")[-1].split("<")[0] - msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n" - msg += f"using regex: {regex}" - _logger.error(msg) - return [] - return matched + if matched or permissive: + return matched + + blanks = text.split("\n")[-1].split("<")[0] + msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n" + msg += f"using regex: {regex}" + _logger.error(msg) + return [] diff --git a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py index 8deec3f1c3..559709ec54 100644 --- a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py +++ b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py @@ -11,9 +11,9 @@ from ._Config import Config -from .._dataclasses import HeaderFile, Recipe #ArgList, Param, Func +from .._dataclasses import HeaderFile, Recipe, Param #ArgList, Param, Func from .._SourceGenerator import SourceGenerator -from .._TagFileParser import TagFileParser #, TagDetails, tag_lookup +from .._TagFileParser import TagFileParser, TagDetails, tag_lookup _logger = logging.getLogger() @@ -57,11 +57,11 @@ def build_declaration(self, recipe: Recipe) -> tuple[str, str, list[str]]: if recipe.implements: tag_info = self._doxygen_tags.tag_info(recipe.implements) - _logger.info(f" recipe for {recipe.name!r}: {tag_info.anchorfile}") - # details = tag_lookup(tag_info) + details = tag_lookup(tag_info) - # # convert XML return type to format suitable for crosswalk - # ret_type = Param.from_xml(details.type).p_type + # convert XML return type to format suitable for crosswalk + ret_type = Param.from_xml(details.type).p_type + _logger.info(f" recipe for {recipe.name!r}: {details.qualified_name}") # ret_param, buffer_params, cabinets = self._ret_crosswalk(ret_type) # par_list = merge_params(recipe.implements, details) # prop_params, prop_cabinets = self._prop_crosswalk(par_list) @@ -69,10 +69,10 @@ def build_declaration(self, recipe: Recipe) -> tuple[str, str, list[str]]: # args = prop_params + buffer_params elif recipe.what == "destructor": - _logger.info(f" recipe for {recipe.name!r}: destructor") - # args = [Param("int", "handle", f"Handle to {recipe.base} object.")] - # details = TagDetails( - # "", "", "", "", "", "", f"Delete {recipe.base} object.", "", args) + args = [Param("int", "handle", f"Handle to {recipe.base} object.")] + details = TagDetails( + "", "", "", "", "", "", f"Delete {recipe.base} object.", "", args) + _logger.info(f" recipe for {recipe.name!r}: void") # ret_param = Param( # "int", "", "Zero for success and -1 for exception handling.") # annotations = f"//! {details.briefdescription}"