Skip to content

Commit 601cbf0

Browse files
committed
feat: generate enums from igraph include headers
1 parent dd50a3c commit 601cbf0

File tree

7 files changed

+727
-62
lines changed

7 files changed

+727
-62
lines changed

src/codegen/functions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ igraph_eigen_matrix:
2525
igraph_eigen_matrix_symmetric:
2626
IGNORE: PythonCTypes
2727

28+
igraph_get_shortest_path_astar:
29+
# igraph_astar_heuristic_func_t not handled yet
30+
IGNORE: PythonCTypes, PythonCTypesTypedWrapper
31+
2832
igraph_empty_attrs:
2933
# Not needed at all
3034
IGNORE: PythonCTypes, PythonCTypesTypedWrapper
@@ -72,3 +76,7 @@ igraph_layout_star:
7276
igraph_maximum_bipartite_matching:
7377
# .Machine$double.eps as default value for 'eps' argument
7478
IGNORE: PythonCTypesTypedWrapper
79+
80+
igraph_erdos_renyi_game:
81+
# Decided not to generate code for it because we have separate functions for gnm and gnp
82+
IGNORE: PythonCTypes, PythonCTypesTypedWrapper

src/codegen/internal_enums.py.in

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from enum import IntEnum
2+
3+
4+
class Loops(IntEnum):
5+
"""Python counterpart of an ``igraph_loops_t`` enum."""
6+
7+
IGNORE = 0
8+
TWICE = 1
9+
ONCE = 2
10+
11+
12+
# fmt: off
13+
# The rest of this file is generated

src/codegen/internal_functions.py.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import numpy as np
21
import numpy.typing as npt
32

43
from typing import Any, Iterable, Optional, Tuple

src/codegen/run.py

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,187 @@
11
from os.path import expanduser
2+
from pathlib import Path
3+
from typing import Dict, Iterable, List, Optional, Sequence, TextIO, Tuple
24

5+
import re
36
import subprocess
47
import sys
58

69

10+
IGRAPH_SOURCE_FOLDER = Path.home() / "dev" / "igraph" / "igraph"
11+
12+
13+
def longest_common_prefix_length(items: Sequence[str]) -> int:
14+
if not items:
15+
return 0
16+
17+
best = 0
18+
min_length = len(min(items, key=len, default=""))
19+
for i in range(1, min_length):
20+
prefixes = [item[:i] for item in items]
21+
if len(set(prefixes)) > 1:
22+
break
23+
24+
if prefixes[0][-1] != "_":
25+
continue
26+
27+
best = i
28+
29+
return best
30+
31+
32+
def generate_enums(template: Path, output: Path, headers: Iterable[Path]):
33+
"""Generates the contents of ``enums.py`` in the source tree by parsing
34+
the given include files from igraph's source tree.
35+
36+
Parsing is done with crude string operations and not with a real C parser
37+
so the formatting of the input file matters.
38+
"""
39+
40+
IGNORED_ENUMS = set(
41+
(
42+
"igraph_cached_property_t",
43+
"igraph_attribute_type_t",
44+
"igraph_attribute_elemtype_t",
45+
"igraph_lapack_dsyev_which_t",
46+
)
47+
)
48+
ENUM_NAME_REMAPPING = {
49+
"Adjacency": "AdjacencyMode",
50+
"BlissSh": "BLISSSplittingHeuristics",
51+
"EdgeorderType": "EdgeOrder",
52+
"EitType": "EdgeIteratorType",
53+
"EsType": "EdgeSequenceType",
54+
"FasAlgorithm": "FASAlgorithm",
55+
"FileformatType": "FileFormat",
56+
"LayoutDrlDefault": "DRLLayoutPreset",
57+
"Loops": None,
58+
"Neimode": "NeighborMode",
59+
"PagerankAlgo": "PagerankAlgorithm",
60+
"SparsematType": "SparseMatrixType",
61+
"SparsematSolve": "SparseMatrixSolver",
62+
"VitType": "VertexIteratorType",
63+
"VsType": "VertexSequenceType",
64+
}
65+
EXTRA_ENUM_MEMBERS: Dict[str, Sequence[Tuple[str, int]]] = {
66+
"Loops": [("IGNORE", 0)]
67+
}
68+
69+
def process_enum(fp: TextIO, spec) -> Optional[str]:
70+
spec = re.sub(r"\s*/\*[^/]*\*/\s*", " ", spec)
71+
spec = spec.replace("IGRAPH_DEPRECATED_ENUMVAL", "")
72+
spec = re.sub(r"\s+", " ", spec)
73+
74+
spec, sep, name = spec.rpartition("}")
75+
if not sep:
76+
raise ValueError("invalid enum, needs braces")
77+
_, sep, spec = spec.partition("{")
78+
if not sep:
79+
raise ValueError("invalid enum, needs braces")
80+
81+
name = name.replace(";", "").strip().lower()
82+
orig_name = name
83+
if orig_name in IGNORED_ENUMS:
84+
return None
85+
if not name.startswith("igraph_") or name.startswith("igraph_i_"):
86+
return None
87+
88+
name = name[7:]
89+
if name.endswith("_t"):
90+
name = name[:-2]
91+
name = "".join(part.capitalize() for part in name.split("_"))
92+
93+
entries = [entry.strip() for entry in spec.split(",")]
94+
entries = [entry for entry in entries if entry]
95+
plen = longest_common_prefix_length(entries)
96+
entries = [entry[plen:] for entry in entries]
97+
98+
remapped_name = ENUM_NAME_REMAPPING.get(name, name)
99+
if remapped_name is None:
100+
return name # it is already written by hand
101+
else:
102+
name = remapped_name
103+
104+
fp.write(f"class {name}(IntEnum):\n")
105+
fp.write(f' """Python counterpart of an ``{orig_name}`` enum."""\n\n')
106+
107+
last_value = -1
108+
for entry in entries:
109+
key, sep, value = entry.replace(" ", "").partition("=")
110+
if key.startswith("UNUSED_"):
111+
continue
112+
113+
if sep:
114+
try:
115+
value_int = int(value)
116+
except ValueError:
117+
# this is an alias to another enum member, skip
118+
continue
119+
else:
120+
value_int = last_value + 1
121+
122+
try:
123+
key = int(key)
124+
except ValueError:
125+
# this is what we expected
126+
pass
127+
else:
128+
if key == 1:
129+
key = "ONE"
130+
else:
131+
raise ValueError(
132+
f"enum key is not a valid Python identifier: {key}"
133+
)
134+
135+
fp.write(f" {key} = {value_int}\n")
136+
last_value = value_int
137+
138+
for key, value_int in EXTRA_ENUM_MEMBERS.get(name, ()):
139+
fp.write(f" {key} = {value_int}\n")
140+
141+
fp.write("\n\n")
142+
return name
143+
144+
def process_file(outfp: TextIO, infp: TextIO) -> List[str]:
145+
all_names = []
146+
147+
current_enum, in_enum = [], False
148+
for line in infp:
149+
if "//" in line:
150+
line = line[: line.index("//")]
151+
152+
line = line.strip()
153+
154+
if line.startswith("typedef enum"):
155+
current_enum = [line]
156+
in_enum = "}" not in line
157+
elif in_enum:
158+
current_enum.append(line)
159+
in_enum = "}" not in line
160+
161+
if current_enum and not in_enum:
162+
name = process_enum(outfp, " ".join(current_enum))
163+
if name:
164+
all_names.append(name)
165+
166+
current_enum.clear()
167+
168+
return all_names
169+
170+
with output.open("w") as outfp:
171+
with template.open("r") as infp:
172+
outfp.write(infp.read())
173+
174+
exports = []
175+
for path in headers:
176+
with path.open("r") as infp:
177+
exports.extend(process_file(outfp, infp))
178+
179+
outfp.write("__all__ = (\n")
180+
for item in sorted(exports):
181+
outfp.write(f" {item!r},\n")
182+
outfp.write(")\n")
183+
184+
7185
def main():
8186
"""Executes the code generation steps that are needed to make the source
9187
code of the Python extension complete.
@@ -12,9 +190,9 @@ def main():
12190
"~/dev/igraph/stimulus/.venv/bin/python",
13191
"~/dev/igraph/stimulus/.venv/bin/stimulus",
14192
"-f",
15-
"~/dev/igraph/igraph/interfaces/functions.yaml",
193+
str(IGRAPH_SOURCE_FOLDER / "interfaces" / "functions.yaml"),
16194
"-t",
17-
"~/dev/igraph/igraph/interfaces/types.yaml",
195+
str(IGRAPH_SOURCE_FOLDER / "interfaces" / "types.yaml"),
18196
"-f",
19197
"src/codegen/functions.yaml",
20198
"-t",
@@ -49,6 +227,12 @@ def main():
49227
]
50228
subprocess.run(args, check=True)
51229

230+
generate_enums(
231+
Path("src/codegen/internal_enums.py.in"),
232+
Path("src/igraph_ctypes/_internal/enums.py"),
233+
(IGRAPH_SOURCE_FOLDER / "include").glob("*.h"),
234+
)
235+
52236

53237
if __name__ == "__main__":
54238
sys.exit(main())

0 commit comments

Comments
 (0)