man nm-setting-*: add "expand enumvals" capability to property-infos

In some cases, properties documentation might require to provide an
explanation of each of the possible values that the property accepts.
If the possible values are the variants of an enum, we can use the
introspection data to get all the possible values for that enum and
their descriptions. With that info, we can automatically generate the
documentation with an always up to date list of accepted values.

Add a new "expand enumvals" feature: it will convert a token with the
format `#EnumName:*` to a list of all the possible values. For the
docbook (description-docbook field in the XML), it is expanded to a
bulleted list of all the values and their respective documentations.

This feature is limited to the "property-infos" comments (those like
---nmcli---, ---dbus---, etc). This comments are used only to generate
the nm-settings-* manual pages. For the documentation under the doc/
folder this is not needed: it's not supported by gtkdoc and, anyway,
it's better to use just `#EnumName` that will generate an HTML link.

Additionally, expansion of `%ENUM_VALUE` is now supported in the
property-infos comments. Instead of expanding them in the same style
than gtkdoc "ENUM_VALUE (num)", it is expanded in a format more suitable
for the nm-setting-* manual pages:
- for nmcli: value_nick (num)
- others: num (value_nick)

Also, fix typo in meson build file propery -> property.
This commit is contained in:
Íñigo Huguet
2023-08-22 12:39:42 +02:00
parent 5c6ae44e00
commit c9ced304d2
3 changed files with 123 additions and 13 deletions

View File

@@ -2088,7 +2088,7 @@ endif
libnm_docs_sources = $(src_libnm_core_impl_lib_c_settings_real)
src/libnm-client-impl/nm-property-infos-%.xml: tools/generate-docs-nm-property-infos.py src/libnm-core-impl/libnm-core-impl.la $(libnm_docs_sources)
src/libnm-client-impl/nm-property-infos-%.xml: tools/generate-docs-nm-property-infos.py src/libnm-core-impl/libnm-core-impl.la src/libnm-client-impl/NM-1.0.gir $(libnm_docs_sources)
$(AM_V_GEN) \
"$(PYTHON)" \
$(srcdir)/tools/generate-docs-nm-property-infos.py \

View File

@@ -169,9 +169,9 @@ if enable_introspection
endif
foreach info: infos
t = custom_target(
'nm-propery-infos-' + info + '.xml',
input: libnm_core_settings_sources,
output: 'nm-propery-infos-' + info + '.xml',
'nm-property-infos-' + info + '.xml',
input: [libnm_gir[0]] + libnm_core_settings_sources,
output: 'nm-property-infos-' + info + '.xml',
command: [
python.path(),
join_paths(meson.source_root(), 'tools', 'generate-docs-nm-property-infos.py'),

View File

@@ -14,6 +14,9 @@ class LineError(Exception):
self.line_no = line_no
enums = {}
enumvals = {}
_dbg_level = 0
try:
_dbg_level = int(os.getenv("NM_DEBUG_GENERATE_DOCS", 0))
@@ -52,6 +55,45 @@ def xnode_get_or_create(root_node, node_name, name):
return node, created
def init_enumvals(girxml):
ns_map = {
"c": "http://www.gtk.org/introspection/c/1.0",
"gi": "http://www.gtk.org/introspection/core/1.0",
"glib": "http://www.gtk.org/introspection/glib/1.0",
}
type_key = "{%s}type" % ns_map["c"]
identifier_key = "{%s}identifier" % ns_map["c"]
nick_key = "{%s}nick" % ns_map["glib"]
for enum in girxml.findall("./gi:namespace/gi:enumeration", ns_map):
enum_cname = enum.attrib[type_key]
enums[enum_cname] = []
for enumval in enum.findall("./gi:member", ns_map):
cname = enumval.attrib[identifier_key]
doc = enumval.find("./gi:doc", ns_map)
enums[enum_cname].append(cname)
enumvals[cname] = {
"value": enumval.attrib["value"],
"nick": enumval.attrib.get(nick_key, cname),
"doc": doc.text if doc is not None else None,
}
for enum in girxml.findall("./gi:namespace/gi:bitfield", ns_map):
enum_cname = enum.attrib[type_key]
enums[enum_cname] = []
for enumval in enum.findall("./gi:member", ns_map):
cname = enumval.attrib[identifier_key]
doc = enumval.find("./gi:doc", ns_map)
enums[enum_cname].append(cname)
enumvals[cname] = {
"value": "0x%x" % int(enumval.attrib["value"]),
"nick": enumval.attrib.get(nick_key, cname),
"doc": doc.text if doc is not None else None,
}
def get_setting_names(source_file):
m = re.match(r"^(.*)/libnm-core-impl/(nm-setting-[^/]*)\.c$", source_file)
assert m
@@ -172,15 +214,50 @@ def write_data(tag, setting_node, line_no, parsed_data):
else:
assert False
def expand_enumval(enumval_name, nick_first):
if enumval_name not in enumvals:
return enumval_name
enumval = enumvals[enumval_name]
if nick_first:
return "%s (%s)" % (enumval["nick"], enumval["value"])
else:
return "%s (%s)" % (enumval["value"], enumval["nick"])
def expand_all_enumvals(enum, nick_first):
assert enum in enums, "Enum name not found: %s" % enum
return ", ".join(expand_enumval(val_name, nick_first) for val_name in enums[enum])
def expand_all_enumvals_with_docs(enum, nick_first):
assert enum in enums, "Enum name not found: %s" % enum
out_str = "<itemizedlist>"
for enumval_name in enums[enum]:
assert enumval_name in enumvals
enumval = enumvals[enumval_name]
out_str += "<listitem><para><literal>%s</literal>%s</para></listitem>" % (
expand_enumval(enumval_name, nick_first),
" - " + enumval["doc"] if enumval["doc"] else "",
)
out_str += "</itemizedlist>"
return out_str
def format_descriptions(tag, parsed_data):
if (
parsed_data.get("description", None) is not None
and parsed_data.get("description-docbook", None) is None
):
# we have a description, but no docbook. Generate one.
node = ET.SubElement(property_node, "description-docbook")
for l in re.split("\n", parsed_data["description"]):
paragraph = ET.SubElement(node, "para")
paragraph.text = l
parsed_data["description-docbook"] = ""
for line in parsed_data["description"].split("\n"):
para = ET.Element("para")
para.text = line
parsed_data["description-docbook"] += ET.tostring(para, encoding="unicode")
elif (
parsed_data.get("description-docbook", None) is not None
and parsed_data.get("description", None) is None
@@ -188,10 +265,35 @@ def write_data(tag, setting_node, line_no, parsed_data):
raise Exception(
'Invalid configuration. When specifying "description-docbook:" there MUST be also a "description:"'
)
elif parsed_data.get("description", None) is None:
return
# Expand enumvals expressions (%ENUM_VALUE and #EnumName:*)
nick_first = tag == "nmcli"
kwd_first_line_re = re.compile(r"^ *\* ([-a-z0-9]+): (.*)$")
kwd_more_line_re = re.compile(r"^ *\*( *)(.*?)\s*$")
parsed_data["description"] = re.sub(
r"#([A-Za-z0-9_]*):\*",
lambda match: expand_all_enumvals(match.group(1), nick_first),
parsed_data["description"],
)
parsed_data["description"] = re.sub(
r"%([^%]\w*)",
lambda match: expand_enumval(match.group(1), nick_first),
parsed_data["description"],
)
parsed_data["description-docbook"] = re.sub(
r"#([A-Za-z0-9_]*):\*",
lambda match: expand_all_enumvals_with_docs(match.group(1), nick_first),
parsed_data["description-docbook"],
)
parsed_data["description-docbook"] = re.sub(
r"%([^%]\w*)",
lambda match: expand_enumval(match.group(1), nick_first),
parsed_data["description-docbook"],
)
def parse_data(tag, line_no, lines):
@@ -326,6 +428,7 @@ def process_setting(tag, root_node, source_file, setting_name):
parsed_data = parse_data(tag, line_no_start, lines)
if not parsed_data:
raise Exception('invalid data: line %s, "%s"' % (line_no, lines))
format_descriptions(tag, parsed_data)
dbg("> > > property: %s" % (parsed_data["property"],))
if _dbg_level > 1:
for keyword in sorted(parsed_data.keys()):
@@ -344,12 +447,14 @@ def process_setting(tag, root_node, source_file, setting_name):
raise LineError(line_no_start, "Unterminated start tag")
def process_settings_docs(tag, output, source_files):
def process_settings_docs(tag, output, gir_file, source_files):
dbg("> tag:%s, output:%s" % (tag, output))
root_node = ET.Element("nm-setting-docs")
init_enumvals(ET.parse(gir_file).getroot())
for setting_name, source_file in get_file_infos(source_files):
try:
process_setting(tag, root_node, source_file, setting_name)
@@ -369,11 +474,16 @@ def process_settings_docs(tag, output, source_files):
def main():
if len(sys.argv) < 4:
print("Usage: %s [tag] [output-xml-file] [srcfiles...]" % (sys.argv[0]))
print(
"Usage: %s [tag] [output-xml-file] [gir-file] [srcfiles...]" % (sys.argv[0])
)
exit(1)
process_settings_docs(
tag=sys.argv[1], output=sys.argv[2], source_files=sys.argv[3:]
tag=sys.argv[1],
output=sys.argv[2],
gir_file=sys.argv[3],
source_files=sys.argv[4:],
)