Source code for mmodel.metadata

import inspect
from textwrap import TextWrapper, shorten
from dataclasses import dataclass, field


[docs]@dataclass(frozen=True) class MetaDataFormatter: """Metadata Formatter.""" format_dict: dict meta_order: list text_wrapper: callable shorten_list: list = field(default_factory=list) def __call__(self, obj): """Convert metadata dictionary to string. The process returns a list of metadata by lines. If the formatter is not found in the formatter dictionary, the default string output is "key: value". :param dict format_dict: format function dictionary :param list meta_order: metadata key order, entry is None if linebreak needed. Defaults to dictionary key order. """ metadata_list = [] for key in self.meta_order: if key == "_": # linebreak metadata_list.append("") continue if key == "self": # allow reference self value = obj else: value = getattr(obj, key, None) # the format functions return a list, for potential multi-liners strings if key in self.format_dict: entry = self.format_dict[key](key, value) elif value: entry = [f"{key}: {value}"] else: entry = [] if key in self.shorten_list: # replace the original list entry = [shorten(ele, width=self.text_wrapper.width) for ele in entry] metadata_list.extend(entry) metadata_wrapped = [] for line in metadata_list: if line: metadata_wrapped.extend(self.text_wrapper.wrap(line)) else: metadata_wrapped.append("") return "\n".join(metadata_wrapped).strip()
[docs]def format_func(key, value): """Format the metadata value that has a function. The key name is not shown in the string output. The result is func(args1, args2, ...).""" if not value: return [] return [f"{value.__name__}{inspect.signature(value)}"]
[docs]def format_list(key, value): """Format the metadata value that is a list.""" if not value: # return [f"{key}: []"] return [] elements = [f"\t- {v}" for v in value] return [f"{key}:"] + elements
[docs]def format_dictargs(key, value): """Format the metadata value that is a dictionary.""" if not value: # return [f"{key}: []"] return [] elements = [f"\t- {k}: {v}" for k, v in value.items()] return [f"{key}:"] + elements
[docs]def modifier_metadata(closure): """Extract metadata from closure, including the name and the arguments. The order of extraction: 1. If the object has the "metadata" attribute defined. 2. If the closure takes no arguments, the name is the function name. 3. If the closure takes arguments, the "metadata" attribute is not defined. Note:: inspect.getclosurevars(closure).nonlocals can only parse values if the value is used in the closure. """ if hasattr(closure, "metadata"): return closure.metadata elif not inspect.getclosurevars(closure).nonlocals: return closure.__name__ else: # closure takes arguments # In some rare cases, the closure is a nested function. # For example, in the tests, the nested closure reflects the # path of the parent function. Here we remove the nested # parent function name. name = closure.__qualname__.rsplit(".<locals>.")[-2] kwargs = inspect.getclosurevars(closure).nonlocals kwargs_str = ", ".join(f"{k}={repr(v)}" for k, v in kwargs.items()) return f"{name}({kwargs_str})"
[docs]def format_modifierlist(key, value): """Format the metadata that is a list of modifiers. The metadata of the modifier is extracted by the modifier_metadata function. The resulting list is formatted by the format_list function. """ modifier_str_list = [modifier_metadata(modifier) for modifier in value] return format_list(key, modifier_str_list)
[docs]def format_shortdocstring(key, value): """Format function docstring. Only the short docstring is parsed. The built-in and ufunc type docstring location is not consistent some module/function has the repr at the first line, and some don't. Here we try to grab the first line that starts with an upper case and ends with a period. """ if not value: return [] for line in value.splitlines(): line = line.strip() if line and line[0].isupper() and line.endswith("."): doc = line break return [f"{doc}"]
[docs]def format_returns(key, value): """Format the metadata value that has a list of returns. The formatter is for the returns metadata. If the "returns" value is empty, the output is None. If the returns only have one value, return the value; otherwise , return the values separated by commas in a tuple representation. """ return_len = len(value) if not return_len: returns_str = "None" elif return_len == 1: returns_str = value[0] else: returns_str = f"({', '.join(value)})" return [f"{key}: {returns_str}"]
[docs]def format_value(key, value): """Format the metadata without displaying the key.""" if not value: return [] return [f"{value}"]
[docs]def format_obj_name(key, value): """Format the metadata value that is an object. Only show the name of the object. This is used for graph and handler objects. The object needs to have __name__ or name attribute defined. If neither is defined, display the string representation. """ if not value: return [] name = getattr(value, "__name__", getattr(value, "name", str(value))) return [f"{key}: {name}"]
# customized textwrapper wrapper80 = TextWrapper( width=80, subsequent_indent="", replace_whitespace=False, expand_tabs=True, tabsize=0, ) modelformatter = MetaDataFormatter( { "self": format_func, "returns": format_returns, "graph": format_obj_name, "handler": format_obj_name, "handler_kwargs": format_dictargs, "modifiers": format_modifierlist, "doc": format_value, }, [ "self", "returns", "graph", "handler", "handler_kwargs", "modifiers", "_", "doc", ], wrapper80, ["handler_kwargs", "modifiers"], ) nodeformatter = MetaDataFormatter( { "name": format_value, "node_func": format_func, "output": lambda key, value: [f"return: {value}"], "modifiers": format_modifierlist, "doc": format_shortdocstring, }, ["name", "_", "node_func", "output", "functype", "modifiers", "_", "doc"], wrapper80, )