Skip to content

Commit ac39870

Browse files
committed
restructure pythonizations and add further goodies
- fixed G.nodes(.byid) - add node/edge/adjEntry to string - fix BUILD_DIR ending with a slash - enable doxygen URLs - add jupyter cell magics
1 parent b9f9200 commit ac39870

File tree

9 files changed

+973
-112
lines changed

9 files changed

+973
-112
lines changed

docs/examples/cpp-interaction.ipynb

Lines changed: 738 additions & 0 deletions
Large diffs are not rendered by default.

src/ogdf_python/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import warnings
33

44
import cppyy.ll
5-
from cppyy import include as cppinclude, cppdef, nullptr
5+
from cppyy import include as cppinclude, cppdef, cppexec, nullptr
66

77
cppyy.ll.set_signals_as_exception(True)
88
cppyy.add_include_path(os.path.dirname(os.path.realpath(__file__)))
@@ -14,7 +14,7 @@
1414
elif "OGDF_BUILD_DIR" in os.environ:
1515
BUILD_DIR = os.path.expanduser(os.getenv("OGDF_BUILD_DIR"))
1616
cppyy.add_include_path(os.path.join(BUILD_DIR, "include"))
17-
cppyy.add_include_path(os.path.join(os.path.dirname(BUILD_DIR), "include"))
17+
cppyy.add_include_path(os.path.join(BUILD_DIR, "..", "include"))
1818
cppyy.add_library_path(BUILD_DIR)
1919
else:
2020
warnings.warn("ogdf-python couldn't find OGDF. "
@@ -30,10 +30,12 @@
3030

3131
import ogdf_python.doxygen
3232
import ogdf_python.pythonize
33+
import ogdf_python.jupyter
3334
from cppyy.gbl import ogdf
3435

35-
__all__ = ["ogdf", "cppinclude", "cppdef", "nullptr"]
36+
__all__ = ["ogdf", "cppinclude", "cppdef", "cppexec", "nullptr"]
3637
__keep_imports = [cppyy,
3738
ogdf_python.doxygen,
3839
ogdf_python.pythonize,
39-
ogdf, cppinclude, cppdef, nullptr]
40+
ogdf_python.jupyter,
41+
ogdf, cppinclude, cppdef, cppexec, nullptr]

src/ogdf_python/doxygen.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,26 @@ def find_all_includes():
5959
# doc strings / help messages##########################################################################################
6060

6161
def pythonize_docstrings(klass, name):
62-
data = DOXYGEN_DATA["class"][klass.__cpp_name__] # TODO do the same for namespace members
62+
data = DOXYGEN_DATA["class"][klass.__cpp_name__.partition("<")[0]] # TODO do the same for namespace members
6363
url = DOXYGEN_URL % (data["refid"], "")
6464
try:
6565
if klass.__doc__:
6666
klass.__doc__ = "%s\n%s" % (klass.__doc__, url)
6767
else:
6868
klass.__doc__ = url
6969
except AttributeError as e:
70-
print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
70+
pass
71+
# print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
7172

7273
for mem, val in klass.__dict__.items():
7374
if mem not in data["members"]:
74-
print(klass.__cpp_name__, "has no member", mem)
75+
# print(klass.__cpp_name__, "has no member", mem)
7576
continue
7677
try:
7778
for override in data["members"][mem].values():
7879
val.__doc__ += "\n" + DOXYGEN_URL % (data["refid"], override["refid"][len(data["refid"]) + 2:])
7980
except AttributeError as e:
80-
print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
81+
# print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
8182
# import traceback
8283
# traceback.print_exc()
8384
# print(val.__doc__)
@@ -178,12 +179,4 @@ def helpful_getattribute(ns, name):
178179
with importlib_resources.files("ogdf_python").joinpath("doxygen.json").open("rt") as f:
179180
DOXYGEN_DATA = json.load(f)
180181

181-
if "OGDF_DOC_URL" in os.environ:
182-
DOXYGEN_URL = os.environ["OGDF_DOC_URL"]
183-
else:
184-
DOXYGEN_URL = "https://ogdf.github.io/doc/ogdf/%s.html#%s"
185-
186-
import cppyy
187-
188-
# cppyy.py.add_pythonization(pythonize_docstrings, "ogdf") # TODO needs to be added *before* any classes are loaded
189-
wrap_getattribute(cppyy.gbl.ogdf)
182+
DOXYGEN_URL = os.environ.get("OGDF_DOC_URL", "https://ogdf.github.io/doc/ogdf/%s.html#%s")

src/ogdf_python/jupyter.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import sys
2+
3+
import cppyy
4+
5+
try:
6+
from IPython.core.magic import register_cell_magic
7+
except ImportError:
8+
pass
9+
else:
10+
cppyy.cppdef("""
11+
#include <sstream>
12+
#include <streambuf>
13+
#include <iostream>
14+
15+
namespace ogdf_pythonization {
16+
std::ostringstream gCapturedStdout;
17+
std::streambuf* gOldStdoutBuffer = nullptr;
18+
19+
static void BeginCaptureStdout() {
20+
gOldStdoutBuffer = std::cout.rdbuf();
21+
std::cout.rdbuf(gCapturedStdout.rdbuf());
22+
}
23+
24+
static std::string EndCaptureStdout() {
25+
std::cout.rdbuf(gOldStdoutBuffer);
26+
gOldStdoutBuffer = nullptr;
27+
28+
std::string capturedStdout = std::move(gCapturedStdout).str();
29+
30+
gCapturedStdout.str("");
31+
gCapturedStdout.clear();
32+
33+
return capturedStdout;
34+
}
35+
36+
std::ostringstream gCapturedStderr;
37+
std::streambuf* gOldStderrBuffer = nullptr;
38+
39+
static void BeginCaptureStderr() {
40+
gOldStderrBuffer = std::cerr.rdbuf();
41+
std::cerr.rdbuf(gCapturedStderr.rdbuf());
42+
}
43+
44+
static std::string EndCaptureStderr() {
45+
std::cerr.rdbuf(gOldStderrBuffer);
46+
gOldStderrBuffer = nullptr;
47+
48+
std::string capturedStderr = std::move(gCapturedStderr).str();
49+
50+
gCapturedStderr.str("");
51+
gCapturedStderr.clear();
52+
53+
return capturedStderr;
54+
}
55+
}
56+
""")
57+
58+
59+
@register_cell_magic
60+
def cpp(line, cell):
61+
"""
62+
might yield "function definition is not allowed here" for some multi-part definitions
63+
use `cppdef` instead if required
64+
https://github.com/jupyter-xeus/xeus-cling/issues/40
65+
"""
66+
cppyy.gbl.ogdf_pythonization.BeginCaptureStdout()
67+
cppyy.gbl.ogdf_pythonization.BeginCaptureStderr()
68+
try:
69+
cppyy.cppexec(cell)
70+
finally:
71+
print(cppyy.gbl.ogdf_pythonization.EndCaptureStdout(), end="")
72+
print(cppyy.gbl.ogdf_pythonization.EndCaptureStderr(), file=sys.stderr, end="")
73+
74+
75+
@register_cell_magic
76+
def cppdef(line, cell):
77+
cppyy.cppdef(cell)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import re
2+
3+
from ogdf_python.doxygen import pythonize_docstrings, wrap_getattribute
4+
from ogdf_python.pythonize.container import *
5+
from ogdf_python.pythonize.graph_attributes import *
6+
from ogdf_python.pythonize.render import *
7+
from ogdf_python.pythonize.str import *
8+
9+
10+
def pythonize_ogdf(klass, name):
11+
# print(name, klass)
12+
pythonize_docstrings(klass, name)
13+
14+
if name in ("Graph", "ClusterGraph"):
15+
klass._repr_html_ = GraphAttributes_to_html
16+
elif name in ("GraphAttributes", "ClusterGraphAttributes"):
17+
replace_GraphAttributes(klass, name)
18+
klass._repr_html_ = GraphAttributes_to_html
19+
20+
# TODO setitem?
21+
# TODO array slicing
22+
elif name.startswith("GraphObjectContainer"):
23+
klass.byid = GraphObjectContainer_byindex
24+
klass.__getitem__ = iterable_getitem
25+
elif re.match("S?List(Pure)?", name):
26+
klass.__getitem__ = iterable_getitem
27+
elif re.match("List(Const)?(Reverse)?Iterator(Base)?(<.+>)?", name):
28+
klass.__next__ = advance_iterator
29+
elif re.match("(Node|Edge|AdjEntry|Cluster|Face)Array", name):
30+
klass.__iter__ = cpp_iterator
31+
# klass.__str__ = grapharray_str # TODO there is no generic way to get the key list (yet)
32+
33+
elif name == "NodeElement":
34+
klass.__str__ = node_str
35+
klass.__repr__ = node_repr
36+
elif name == "EdgeElement":
37+
klass.__str__ = edge_str
38+
klass.__repr__ = edge_repr
39+
elif name == "AdjElement":
40+
klass.__str__ = adjEntry_str
41+
klass.__repr__ = adjEntry_repr
42+
43+
44+
cppyy.py.add_pythonization(pythonize_ogdf, "ogdf")
45+
cppyy.py.add_pythonization(pythonize_ogdf, "ogdf::internal")
46+
generate_GA_setters()
47+
wrap_getattribute(cppyy.gbl.ogdf)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
def GraphObjectContainer_byindex(self, idx):
2+
for e in self:
3+
if e.index() == idx:
4+
return e
5+
raise IndexError("Container has no element with index %s." % idx)
6+
7+
8+
def advance_iterator(self):
9+
if not self.valid():
10+
raise StopIteration()
11+
val = self.__deref__()
12+
self.__preinc__()
13+
return val
14+
15+
16+
def cpp_iterator(self):
17+
it = self.begin()
18+
while it != self.end():
19+
yield it.__deref__()
20+
it.__preinc__()
21+
22+
23+
def iterable_getitem(self, key):
24+
if isinstance(key, slice):
25+
indices = range(*key.indices(len(self)))
26+
elems = []
27+
try:
28+
next_ind = next(indices)
29+
for i, e in enumerate(self):
30+
if i == next_ind:
31+
elems.append(e)
32+
next_ind = next(indices)
33+
except StopIteration:
34+
pass
35+
return elems
36+
elif isinstance(key, int):
37+
if key < 0:
38+
key += len(self)
39+
if key < 0 or key >= len(self):
40+
raise IndexError("The index (%d) is out of range." % key)
41+
for i, e in enumerate(self):
42+
if i == key:
43+
return e
44+
else:
45+
raise TypeError("Invalid argument type %s." % type(key))
Lines changed: 3 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import functools
2-
import re
3-
import tempfile
4-
51
import cppyy
62

73

@@ -17,8 +13,8 @@ def __setitem__(self, key, item):
1713
def __getitem__(self, key):
1814
return self.getter(self.GA, key)
1915

20-
def __call__(self, key):
21-
return self.getter(self.GA, key)
16+
def __call__(self, *args, **kwargs):
17+
return self.getter(self.GA, *args, **kwargs)
2218

2319

2420
class GraphAttributesDescriptor(object):
@@ -96,98 +92,10 @@ def generate_GA_setters():
9692
cppyy.cppdef(DEFS + "};")
9793

9894

99-
SVGConf = None
100-
101-
102-
def GraphAttributes_to_html(self):
103-
global SVGConf
104-
if SVGConf == None:
105-
SVGConf = cppyy.gbl.ogdf.GraphIO.SVGSettings()
106-
SVGConf.margin(50)
107-
SVGConf.bezierInterpolation(True)
108-
SVGConf.curviness(0.3)
109-
with tempfile.NamedTemporaryFile("w+t", suffix=".svg", prefix="ogdf-python-") as f:
110-
# os = cppyy.gbl.std.ofstream(f.name)
111-
# cppyy.bind_object(cppyy.addressof(os), "std::basic_ostream<char>")
112-
cppyy.gbl.ogdf.GraphIO.drawSVG(self, f.name, SVGConf)
113-
# os.close()
114-
return f.read()
115-
116-
11795
def replace_GraphAttributes(klass, name):
118-
if not name.endswith("GraphAttributes"): return
11996
klass.directed = property(klass.directed, cppyy.gbl.ogdf_pythonization.GraphAttributes_set_directed)
12097
for field in (CGA_FIELD_NAMES if name.startswith("Cluster") else GA_FIELD_NAMES):
12198
setattr(klass, field, GraphAttributesDescriptor(
12299
getattr(klass, field),
123100
getattr(cppyy.gbl.ogdf_pythonization, "GraphAttributes_set_%s" % field)
124-
))
125-
klass._repr_html_ = GraphAttributes_to_html
126-
127-
128-
generate_GA_setters()
129-
# TODO use pythonization for this so that classes are loaded lazily
130-
replace_GraphAttributes(cppyy.gbl.ogdf.GraphAttributes, "GraphAttributes")
131-
replace_GraphAttributes(cppyy.gbl.ogdf.ClusterGraphAttributes, "ClusterGraphAttributes")
132-
cppyy.gbl.ogdf.Graph._repr_html_ = GraphAttributes_to_html # TODO layout
133-
cppyy.gbl.ogdf.ClusterGraph._repr_html_ = GraphAttributes_to_html
134-
135-
136-
def GraphObjectContainer_getitem(self, idx):
137-
for e in self:
138-
if e.index() == idx:
139-
return e
140-
raise IndexError()
141-
142-
143-
def pythonize_ogdf_internal(klass, name):
144-
if name.startswith("GraphObjectContainer"):
145-
klass.__getitem__ = GraphObjectContainer_getitem
146-
147-
148-
cppyy.py.add_pythonization(pythonize_ogdf_internal, "ogdf::internal")
149-
150-
151-
class StreamToStr(object):
152-
def __init__(self, klass):
153-
self.klass = klass
154-
self.old_str = klass.__str__
155-
156-
def __get__(self, obj, type=None):
157-
@functools.wraps(self.old_str)
158-
def to_str():
159-
try:
160-
return cppyy.gbl.ogdf_pythonization.to_string(obj)
161-
except TypeError as e:
162-
print(e)
163-
return self.old_str(obj)
164-
165-
return to_str
166-
167-
168-
def generic_getitem(self, idx):
169-
# TODO more efficient implementation for random-access, reverse iteration
170-
for i, e in enumerate(self):
171-
if i == idx:
172-
return e
173-
raise IndexError()
174-
175-
176-
def pythonize_ogdf(klass, name):
177-
if not isinstance(klass.__str__, StreamToStr):
178-
klass.__str__ = StreamToStr(klass)
179-
if re.match("List(Const)?(Reverse)?Iterator(Base)?(<.+>)?", name):
180-
def advance(self):
181-
if not self.valid():
182-
raise StopIteration()
183-
val = self.__deref__()
184-
self.__preinc__()
185-
return val
186-
187-
klass.__next__ = advance
188-
if re.match("S?List(Pure)?", name):
189-
klass.__getitem__ = generic_getitem
190-
# TODO setitem?
191-
192-
193-
cppyy.py.add_pythonization(pythonize_ogdf, "ogdf")
101+
))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import tempfile
2+
3+
import cppyy
4+
5+
SVGConf = None
6+
7+
8+
def GraphAttributes_to_html(self):
9+
global SVGConf
10+
if SVGConf is None:
11+
SVGConf = cppyy.gbl.ogdf.GraphIO.SVGSettings()
12+
SVGConf.margin(50)
13+
SVGConf.bezierInterpolation(True)
14+
SVGConf.curviness(0.3)
15+
with tempfile.NamedTemporaryFile("w+t", suffix=".svg", prefix="ogdf-python-") as f:
16+
# os = cppyy.gbl.std.ofstream(f.name)
17+
# cppyy.bind_object(cppyy.addressof(os), "std::basic_ostream<char>")
18+
cppyy.gbl.ogdf.GraphIO.drawSVG(self, f.name, SVGConf)
19+
# os.close()
20+
return f.read()

0 commit comments

Comments
 (0)