11
11
corresponding C files. If they have, then runs cython on these files to
12
12
recreate the C files.
13
13
14
- The script detects changes in the pyx files using checksums [or hashes] stored
15
- in a database file
14
+ The script detects changes in the pyx/pxd files using checksums
15
+ [or hashes] stored in a database file
16
16
17
17
Simple script to invoke Cython on all .pyx
18
18
files; while waiting for a proper build system. Uses file hashes to
27
27
We copied it for scikit-learn.
28
28
29
29
Note: this script does not check any of the dependent C libraries; it only
30
- operates on the Cython .pyx files.
30
+ operates on the Cython .pyx files or their corresponding Cython header (.pxd)
31
+ files.
31
32
"""
32
- # author: Arthur Mensch
33
- # license: BSD
33
+ # Author: Arthur Mensch <arthur.mensch@inria.fr>
34
+ # Author: Raghav R V <rvraghav93@gmail.com>
35
+ #
36
+ # License: BSD 3 clause
34
37
35
38
from __future__ import division , print_function , absolute_import
36
39
50
53
WindowsError = None
51
54
52
55
53
- #
54
- # Rules
55
- #
56
- def process_pyx (fromfile , tofile ):
56
+ def cythonize (cython_file , gen_file ):
57
57
try :
58
58
from Cython .Compiler .Version import version as cython_version
59
59
from distutils .version import LooseVersion
@@ -64,53 +64,53 @@ def process_pyx(fromfile, tofile):
64
64
pass
65
65
66
66
flags = ['--fast-fail' ]
67
- if tofile .endswith ('.cpp' ):
67
+ if gen_file .endswith ('.cpp' ):
68
68
flags += ['--cplus' ]
69
69
70
70
try :
71
71
try :
72
- r = subprocess .call (['cython' ] + flags + ["-o" , tofile , fromfile ])
73
- if r != 0 :
74
- raise Exception ('Cython failed' )
72
+ rc = subprocess .call (['cython' ] +
73
+ flags + ["-o" , gen_file , cython_file ])
74
+ if rc != 0 :
75
+ raise Exception ('Cythonizing %s failed' % cython_file )
75
76
except OSError :
76
77
# There are ways of installing Cython that don't result in a cython
77
78
# executable on the path, see scipy issue gh-2397.
78
- r = subprocess .call ([sys .executable , '-c' ,
79
- 'import sys; from Cython.Compiler.Main '
80
- 'import setuptools_main as main;'
81
- ' sys.exit(main())' ] + flags +
82
- ["-o" , tofile , fromfile ])
83
- if r != 0 :
84
- raise Exception ('Cython failed' )
79
+ rc = subprocess .call ([sys .executable , '-c' ,
80
+ 'import sys; from Cython.Compiler.Main '
81
+ 'import setuptools_main as main;'
82
+ ' sys.exit(main())' ] + flags +
83
+ ["-o" , gen_file , cython_file ])
84
+ if rc != 0 :
85
+ raise Exception ('Cythonizing %s failed' % cython_file )
85
86
except OSError :
86
87
raise OSError ('Cython needs to be installed' )
87
88
88
89
89
- rules = {
90
- '.pyx' : process_pyx ,
91
- }
92
-
93
-
94
- #
95
- # Hash db
96
- #
97
90
def load_hashes (filename ):
98
- # Return { filename : (sha1 of input, sha1 of output) }
99
- if os .path .isfile (filename ):
100
- hashes = {}
101
- with open (filename , 'r' ) as f :
102
- for line in f :
103
- filename , inhash , outhash = line .split ()
104
- hashes [filename ] = (inhash , outhash )
105
- else :
91
+ """Load the hashes dict from the hashfile"""
92
+ # { filename : (sha1 of header if available or 'NA',
93
+ # sha1 of input,
94
+ # sha1 of output) }
95
+
96
+ hashes = {}
97
+ try :
98
+ with open (filename , 'r' ) as cython_hash_file :
99
+ for hash_record in cython_hash_file :
100
+ (filename , header_hash ,
101
+ cython_hash , gen_file_hash ) = hash_record .split ()
102
+ hashes [filename ] = (header_hash , cython_hash , gen_file_hash )
103
+ except (KeyError , ValueError , AttributeError , IOError ):
106
104
hashes = {}
107
105
return hashes
108
106
109
107
110
- def save_hashes (hash_db , filename ):
111
- with open (filename , 'w' ) as f :
112
- for key , value in hash_db .items ():
113
- f .write ("%s %s %s\n " % (key , value [0 ], value [1 ]))
108
+ def save_hashes (hashes , filename ):
109
+ """Save the hashes dict to the hashfile"""
110
+ with open (filename , 'w' ) as cython_hash_file :
111
+ for key , value in hashes .items ():
112
+ cython_hash_file .write ("%s %s %s %s\n "
113
+ % (key , value [0 ], value [1 ], value [2 ]))
114
114
115
115
116
116
def sha1_of_file (filename ):
@@ -120,59 +120,74 @@ def sha1_of_file(filename):
120
120
return h .hexdigest ()
121
121
122
122
123
- #
124
- # Main program
125
- #
126
- def normpath (path ):
123
+ def clean_path (path ):
124
+ """Clean the path"""
127
125
path = path .replace (os .sep , '/' )
128
126
if path .startswith ('./' ):
129
127
path = path [2 :]
130
128
return path
131
129
132
130
133
- def get_hash (frompath , topath ):
134
- from_hash = sha1_of_file (frompath )
135
- to_hash = sha1_of_file (topath ) if os .path .exists (topath ) else None
136
- return from_hash , to_hash
131
+ def get_hash_tuple (header_path , cython_path , gen_file_path ):
132
+ """Get the hashes from the given files"""
133
+
134
+ header_hash = (sha1_of_file (header_path )
135
+ if os .path .exists (header_path ) else 'NA' )
136
+ from_hash = sha1_of_file (cython_path )
137
+ to_hash = (sha1_of_file (gen_file_path )
138
+ if os .path .exists (gen_file_path ) else 'NA' )
139
+
140
+ return header_hash , from_hash , to_hash
141
+
137
142
143
+ def cythonize_if_unchanged (path , cython_file , gen_file , hashes ):
144
+ full_cython_path = os .path .join (path , cython_file )
145
+ full_header_path = full_cython_path .replace ('.pyx' , '.pxd' )
146
+ full_gen_file_path = os .path .join (path , gen_file )
138
147
139
- def process (path , fromfile , tofile , processor_function , hash_db ):
140
- fullfrompath = os .path .join (path , fromfile )
141
- fulltopath = os .path .join (path , tofile )
142
- current_hash = get_hash (fullfrompath , fulltopath )
143
- if current_hash == hash_db .get (normpath (fullfrompath )):
144
- print ('%s has not changed' % fullfrompath )
148
+ current_hash = get_hash_tuple (full_header_path , full_cython_path ,
149
+ full_gen_file_path )
150
+
151
+ if current_hash == hashes .get (clean_path (full_cython_path )):
152
+ print ('%s has not changed' % full_cython_path )
145
153
return
146
154
147
- print ('Processing %s' % fullfrompath )
148
- processor_function (fullfrompath , fulltopath )
155
+ print ('Processing %s' % full_cython_path )
156
+ cythonize (full_cython_path , full_gen_file_path )
157
+
149
158
# changed target file, recompute hash
150
- current_hash = get_hash (fullfrompath , fulltopath )
151
- # store hash in db
152
- hash_db [normpath (fullfrompath )] = current_hash
159
+ current_hash = get_hash_tuple (full_header_path , full_cython_path ,
160
+ full_gen_file_path )
153
161
162
+ # Update the hashes dict with the new hash
163
+ hashes [clean_path (full_cython_path )] = current_hash
154
164
155
- def find_process_files (root_dir ):
165
+
166
+ def check_and_cythonize (root_dir ):
156
167
print (root_dir )
157
- hash_db = load_hashes (HASH_FILE )
168
+ hashes = load_hashes (HASH_FILE )
169
+
158
170
for cur_dir , dirs , files in os .walk (root_dir ):
159
171
for filename in files :
160
- for fromext , function in rules .items ():
161
- if filename .endswith (fromext ):
162
- toext = ".c"
163
- with open (os .path .join (cur_dir , filename ), 'rb' ) as f :
164
- data = f .read ()
165
- m = re .search (b"libcpp" , data , re .I | re .M )
166
- if m :
167
- toext = ".cpp"
168
- fromfile = filename
169
- tofile = filename [:- len (fromext )] + toext
170
- process (cur_dir , fromfile , tofile , function , hash_db )
171
- save_hashes (hash_db , HASH_FILE )
172
+ if filename .endswith ('.pyx' ):
173
+ gen_file_ext = '.c'
174
+ # Cython files with libcpp imports should be compiled to cpp
175
+ with open (os .path .join (cur_dir , filename ), 'rb' ) as f :
176
+ data = f .read ()
177
+ m = re .search (b"libcpp" , data , re .I | re .M )
178
+ if m :
179
+ gen_file_ext = ".cpp"
180
+ cython_file = filename
181
+ gen_file = filename .replace ('.pyx' , gen_file_ext )
182
+ cythonize_if_unchanged (cur_dir , cython_file , gen_file , hashes )
183
+
184
+ # Save hashes once per module. This prevents cythonizing prev.
185
+ # files again when debugging broken code in a single file
186
+ save_hashes (hashes , HASH_FILE )
172
187
173
188
174
189
def main (root_dir = DEFAULT_ROOT ):
175
- find_process_files (root_dir )
190
+ check_and_cythonize (root_dir )
176
191
177
192
178
193
if __name__ == '__main__' :
0 commit comments