# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
from __future__ import annotations
import subprocess
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from pathlib import Path
import xml.sax
from xml.sax.handler import ContentHandler
DESC = """Print a list of module short names ordered by typesystem dependencies
for which documentation can be built by intersecting the PySide6 modules with
the modules built in Qt."""
ROOT_DIR = Path(__file__).parents[1].resolve()
SOURCE_DIR = ROOT_DIR / "sources" / "pyside6" / "PySide6"
qt_version = None
qt_include_dir = None
class TypeSystemContentHandler(ContentHandler):
"""XML SAX content handler that extracts required modules from the
"load-typesystem" elements of the typesystem_file. Nodes that start
with Qt and are marked as generate == "no" are considered required."""
def __init__(self):
self.required_modules = []
def startElement(self, name, attrs):
if name == "load-typesystem":
generate = attrs.get("generate", "").lower()
if generate == "no" or generate == "false":
load_file_name = attrs.get("name") # "QtGui/typesystem_gui.xml"
if load_file_name.startswith("Qt"):
slash = load_file_name.find("/")
if slash > 0:
self.required_modules.append(load_file_name[:slash])
def required_typesystems(module):
"""Determine the required Qt modules by looking at the "load-typesystem"
elements of the typesystem_file."""
name = module[2:].lower()
typesystem_file = SOURCE_DIR / module / f"typesystem_{name}.xml"
# Use a SAX parser since that works despite undefined entity
# errors for typesystem entities.
handler = TypeSystemContentHandler()
try:
parser = xml.sax.make_parser()
parser.setContentHandler(handler)
parser.parse(typesystem_file)
except Exception as e:
print(f"Warning: XML error parsing {typesystem_file}: {e}", file=sys.stderr)
return handler.required_modules
def query_qtpaths(keyword):
query_cmd = ["qtpaths", "-query", keyword]
output = subprocess.check_output(query_cmd, stderr=subprocess.STDOUT,
universal_newlines=True)
return output.strip()
def sort_modules(dependency_dict):
"""Sort the modules by dependencies using brute force: Keep adding
modules all of whose requirements are present to the result list
until done."""
result = []
while True:
found = False
for module, dependencies in dependency_dict.items():
if module not in result:
if all(dependency in result for dependency in dependencies):
result.append(module)
found = True
if not found:
break
if len(result) < len(dependency_dict) and verbose:
for desired_module in dependency_dict.keys():
if desired_module not in result:
print(f"Not documenting {desired_module} (missing dependency)",
file=sys.stderr)
return result
def _write_type_system(modules, file):
"""Helper to write the type system for shiboken. It needs to be in
dependency order to prevent shiboken from loading the included
typesystems with generate="no", which causes those modules to be
missing."""
for module in modules:
name = module[2:].lower()
filename = f"{module}/typesystem_{name}.xml"
print(f' ',
file=file)
print("", file=file)
def write_type_system(modules, filename):
"""Write the type system for shiboken in dependency order."""
if filename == "-":
_write_type_system(modules, sys.stdout)
else:
path = Path(filename)
exists = path.exists()
with path.open(mode="a") as f:
if not exists:
print('', file=f)
_write_type_system(modules, f)
def _write_global_header(modules, file):
"""Helper to write the global header for shiboken."""
for module in modules:
print(f"#include <{module}/{module}>", file=file)
def write_global_header(modules, filename):
"""Write the global header for shiboken."""
if filename == "-":
_write_global_header(modules, sys.stdout)
else:
with Path(filename).open(mode="a") as f:
_write_global_header(modules, f)
def _write_docconf(modules, file):
"""Helper to write the include paths for the .qdocconf file."""
# @TODO fix this for macOS frameworks.
for module in modules:
root = f" -I/{qt_include_dir}/{module}"
print(f"{root} \\", file=file)
print(f"{root}/{qt_version} \\", file=file)
print(f"{root}/{qt_version}/{module} \\", file=file)
def write_docconf(modules, filename):
"""Write the include paths for the .qdocconf file."""
if filename == "-":
_write_docconf(modules, sys.stdout)
else:
with Path(filename).open(mode="a") as f:
_write_docconf(modules, f)
if __name__ == "__main__":
argument_parser = ArgumentParser(description=DESC,
formatter_class=RawTextHelpFormatter)
argument_parser.add_argument("--verbose", "-v", action="store_true",
help="Verbose")
argument_parser.add_argument("qt_include_dir", help="Qt Include dir",
nargs='?', type=str)
argument_parser.add_argument("qt_version", help="Qt version string",
nargs='?', type=str)
argument_parser.add_argument("--typesystem", "-t", help="Typesystem file to write",
action="store", type=str)
argument_parser.add_argument("--global-header", "-g", help="Global header to write",
action="store", type=str)
argument_parser.add_argument("--docconf", "-d", help="docconf file to write",
action="store", type=str)
options = argument_parser.parse_args()
verbose = options.verbose
if options.qt_include_dir:
qt_include_dir = Path(options.qt_include_dir)
if not qt_include_dir.is_dir():
print(f"Invalid include directory passed: {options.qt_include_dir}",
file=sys.stderr)
sys.exit(-1)
else:
verbose = True # Called by hand to find out about available modules
query_cmd = ["qtpaths", "-query", "QT_INSTALL_HEADERS"]
qt_include_dir = Path(query_qtpaths("QT_INSTALL_HEADERS"))
if not qt_include_dir.is_dir():
print("Cannot determine include directory", file=sys.stderr)
sys.exit(-1)
qt_version = options.qt_version if options.qt_version else query_qtpaths("QT_VERSION")
# Build a typesystem dependency dict of the available modules in order
# to be able to sort_modules by dependencies. This is required as
# otherwise shiboken will read the required typesystems with
# generate == "no" and thus omit modules.
module_dependency_dict = {}
for m in SOURCE_DIR.glob("Qt*"):
module = m.name
# QtGraphs duplicates symbols from QtDataVisualization/QtCharts causing shiboken errors
if module == "QtDataVisualization" or module == "QtCharts":
continue
qt_include_path = qt_include_dir / module
if qt_include_path.is_dir():
module_dependency_dict[module] = required_typesystems(module)
elif verbose:
print(f"Not documenting {module} (not built)", file=sys.stderr)
modules = sort_modules(module_dependency_dict)
print(" ".join([m[2:] for m in modules]))
if options.typesystem:
write_type_system(modules, options.typesystem)
if options.global_header:
write_global_header(modules, options.global_header)
if options.docconf:
write_docconf(modules, options.docconf)