11from os .path import expanduser
2+ from pathlib import Path
3+ from typing import Dict , Iterable , List , Optional , Sequence , TextIO , Tuple
24
5+ import re
36import subprocess
47import 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+
7185def 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
53237if __name__ == "__main__" :
54238 sys .exit (main ())
0 commit comments