Skip to content

Commit e54f3de

Browse files
committed
include doxygen url in __doc__, hint at missing cppinclude on AttributeErrors
1 parent a17dab2 commit e54f3de

File tree

3 files changed

+185
-4
lines changed

3 files changed

+185
-4
lines changed

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ classifiers = [
3030

3131
[tool.poetry.dependencies]
3232
python = "^3.6"
33-
cppyy = "^1.8.1"
33+
cppyy = "^1.8.1"
34+
#lxml = "^0"
35+
importlib_resources = "^1.4"
3436

3537
[build-system]
3638
requires = ["poetry>=0.12"]

src/ogdf_python/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import warnings
23

34
import cppyy.ll
45
from cppyy import include as cppinclude, cppdef, nullptr
@@ -7,14 +8,17 @@
78
cppyy.add_include_path(os.path.dirname(os.path.realpath(__file__)))
89

910
if "OGDF_INSTALL_DIR" in os.environ:
10-
INSTALL_DIR = os.path.expanduser(os.getenv("OGDF_INSTALL_DIR", "/usr/local"))
11+
INSTALL_DIR = os.path.expanduser(os.getenv("OGDF_INSTALL_DIR"))
1112
cppyy.add_include_path(os.path.join(INSTALL_DIR, "include"))
1213
cppyy.add_library_path(os.path.join(INSTALL_DIR, "lib"))
1314
elif "OGDF_BUILD_DIR" in os.environ:
14-
BUILD_DIR = os.path.expanduser(os.getenv("OGDF_BUILD_DIR", "~/ogdf/build-debug"))
15+
BUILD_DIR = os.path.expanduser(os.getenv("OGDF_BUILD_DIR"))
1516
cppyy.add_include_path(os.path.join(BUILD_DIR, "include"))
1617
cppyy.add_include_path(os.path.join(os.path.dirname(BUILD_DIR), "include"))
1718
cppyy.add_library_path(BUILD_DIR)
19+
else:
20+
warnings.warn("ogdf-python couldn't find OGDF. "
21+
"Please set environment variables OGDF_INSTALL_DIR or OGDF_BUILD_DIR.")
1822

1923
cppyy.cppdef("#undef NDEBUG")
2024
cppyy.include("ogdf/basic/internal/config_autogen.h")
@@ -25,7 +29,11 @@
2529
cppyy.load_library("libOGDF.so")
2630

2731
import ogdf_python.pythonize
32+
import ogdf_python.doxygen
2833
from cppyy.gbl import ogdf
2934

3035
__all__ = ["ogdf", "cppinclude", "cppdef", "nullptr"]
31-
__keep_imports = [cppyy, ogdf_python.pythonize, ogdf, cppinclude, cppdef, nullptr]
36+
__keep_imports = [cppyy,
37+
ogdf_python.doxygen,
38+
ogdf_python.pythonize,
39+
ogdf, cppinclude, cppdef, nullptr]

src/ogdf_python/doxygen.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import functools
2+
import os
3+
from collections import defaultdict
4+
5+
6+
def parse_index_xml():
7+
root = etree.parse(os.path.join(DOXYGEN_XML_DIR, 'index.xml'))
8+
compounds = defaultdict(dict)
9+
for compound in root.iter('compound'):
10+
kind = compound.attrib['kind']
11+
name = compound.find('name').text.strip()
12+
if name in compounds[kind]:
13+
print("duplicate compound", kind, name)
14+
continue
15+
compound_data = compounds[kind][name] = {
16+
"kind": kind,
17+
"name": name,
18+
"refid": compound.attrib['refid'],
19+
"members": defaultdict(dict)
20+
}
21+
22+
for member in compound.iter('member'):
23+
kind = member.attrib['kind']
24+
name = member.find('name').text.strip()
25+
refid = member.attrib['refid']
26+
if refid in compound_data["members"][name]:
27+
print("duplicate member", kind, name, refid)
28+
continue
29+
compound_data["members"][name][refid] = {
30+
"kind": kind,
31+
"name": name,
32+
"refid": refid
33+
}
34+
35+
return compounds
36+
37+
38+
def pythonize_docstrings(klass, name):
39+
data = DOXYGEN_DATA["class"][klass.__cpp_name__]
40+
klass.__doc__ += "\n" + DOXYGEN_URL % (data["refid"], "")
41+
for mem, val in klass.__dict__.items():
42+
if mem not in data["members"]:
43+
print(klass.__cpp_name__, "has no member", mem)
44+
continue
45+
try:
46+
for override in data["members"][mem].values():
47+
val.__doc__ += "\n" + DOXYGEN_URL % (data["refid"], override["refid"][len(data["refid"]) + 2:])
48+
except:
49+
import traceback # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
50+
traceback.print_exc()
51+
print(val.__doc__)
52+
pass
53+
54+
55+
def find_all_includes():
56+
for ctype in ("class", "struct", "namespace"):
57+
for compound in DOXYGEN_DATA[ctype].values():
58+
compound_xml = etree.parse(os.path.join(DOXYGEN_XML_DIR, compound["refid"] + '.xml'))
59+
for location in compound_xml.findall(".//*[@id]/location"):
60+
parent = location.getparent()
61+
if parent.get("id") == compound["refid"]:
62+
member = compound
63+
else:
64+
name = parent.find("name")
65+
if name.text not in compound["members"]:
66+
print("got location for unknown object", compound["refid"], parent.get("id"), name.text)
67+
continue
68+
else:
69+
member = compound["members"][name.text][parent.get("id")]
70+
member["file"] = location.get("declfile", location.get("file"))
71+
72+
73+
def find_include(*names):
74+
if len(names) == 1:
75+
if isinstance(names[0], str):
76+
names = names[0].split("::")
77+
else:
78+
names = names[0]
79+
80+
name = names[-1]
81+
qualname = "::".join(names)
82+
parentname = "::".join(names[:-1])
83+
84+
data = DOXYGEN_DATA["class"].get(qualname, None)
85+
filename = None
86+
if not data:
87+
data = DOXYGEN_DATA["struct"].get(qualname, None)
88+
if not data:
89+
namespace_data = DOXYGEN_DATA["namespace"].get(parentname, None)
90+
if namespace_data and name in namespace_data["members"]:
91+
data = next(iter(namespace_data["members"][name].values()))
92+
filename = namespace_data["refid"]
93+
if not data:
94+
return None
95+
96+
if "file" in data:
97+
return data["file"]
98+
99+
if not filename:
100+
filename = data["refid"]
101+
namespace_xml = etree.parse(os.path.join(DOXYGEN_XML_DIR, filename + '.xml'))
102+
location = namespace_xml.find(".//*[@id='%s']/location" % data["refid"])
103+
return location.get("declfile", location.get("file"))
104+
105+
106+
def wrap_getattribute(ns):
107+
getattrib = type(ns).__getattribute__
108+
if hasattr(getattrib, "__wrapped__"): return
109+
110+
@functools.wraps(getattrib)
111+
def helpful_getattribute(ns, name):
112+
try:
113+
val = getattrib(ns, name)
114+
if isinstance(type(val), type(type(ns))):
115+
wrap_getattribute(val)
116+
return val
117+
except AttributeError as e:
118+
if hasattr(e, "__helpful__"):
119+
raise e
120+
msg = e.args[0]
121+
file = find_include(*ns.__cpp_name__.split("::"), name)
122+
if file:
123+
prefix = "include/"
124+
msg += "\nDid you forget to include file %s? Try running\ncppinclude(\"%s\")" % \
125+
(file, file[len(prefix):] if file.startswith(prefix) else file)
126+
else:
127+
msg += "\nThe name %s::%s couldn't be found in the docs." % (ns.__cpp_name__, name)
128+
e.args = (msg, *e.args[1:])
129+
e.__helpful__ = True
130+
raise e
131+
132+
type(ns).__getattribute__ = helpful_getattribute
133+
134+
135+
if "OGDF_DOC_DIR" in os.environ:
136+
from lxml import etree
137+
138+
DOXYGEN_XML_DIR = os.path.join(os.environ["OGDF_DOC_DIR"], "xml")
139+
DOXYGEN_DATA = parse_index_xml()
140+
141+
if __name__ == "__main__":
142+
import json, sys
143+
144+
find_all_includes()
145+
with open("doxygen.json", "wt") as f:
146+
json.dump(DOXYGEN_DATA, f)
147+
sys.exit(0)
148+
149+
else:
150+
import json
151+
152+
DOXYGEN_XML_DIR = None
153+
154+
try:
155+
with open("doxygen.json", "rt") as f:
156+
DOXYGEN_DATA = json.load(f)
157+
except FileNotFoundError:
158+
import importlib_resources
159+
160+
with importlib_resources.files(__name__).joinpath("doxygen.json").open("rt") as f:
161+
DOXYGEN_DATA = json.load(f)
162+
163+
if "OGDF_DOC_URL" in os.environ:
164+
DOXYGEN_URL = os.environ["OGDF_DOC_URL"]
165+
else:
166+
DOXYGEN_URL = "https://ogdf.github.io/doc/ogdf/%s.html#%s"
167+
168+
import cppyy
169+
170+
cppyy.py.add_pythonization(pythonize_docstrings, "ogdf")
171+
wrap_getattribute(cppyy.gbl.ogdf)

0 commit comments

Comments
 (0)