Skip to content

Commit 0a5543b

Browse files
for possible cython extensions
1 parent d9f0c87 commit 0a5543b

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

cythexts.py

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import os
2+
from os.path import splitext, sep as filesep, join as pjoin, relpath
3+
from hashlib import sha1
4+
5+
from distutils.command.build_ext import build_ext
6+
from distutils.command.sdist import sdist
7+
from distutils.version import LooseVersion
8+
9+
10+
def derror_maker(klass, msg):
11+
""" Decorate distutils class to make run method raise error """
12+
class K(klass):
13+
def run(self):
14+
raise RuntimeError(msg)
15+
return K
16+
17+
18+
def stamped_pyx_ok(exts, hash_stamp_fname):
19+
""" Check for match of recorded hashes for pyx, corresponding c files
20+
21+
Parameters
22+
----------
23+
exts : sequence of ``Extension``
24+
distutils ``Extension`` instances, in fact only need to contain a
25+
``sources`` sequence field.
26+
hash_stamp_fname : str
27+
filename of text file containing hash stamps
28+
29+
Returns
30+
-------
31+
tf : bool
32+
True if there is a corresponding c file for each pyx or py file in
33+
`exts` sources, and the hash for both the (pyx, py) file *and* the c
34+
file match those recorded in the file named in `hash_stamp_fname`.
35+
"""
36+
# Calculate hashes for pyx and c files. Check for presence of c files.
37+
stamps = {}
38+
for mod in exts:
39+
for source in mod.sources:
40+
base, ext = splitext(source)
41+
if not ext in ('.pyx', '.py'):
42+
continue
43+
source_hash = sha1(open(source, 'rb').read()).hexdigest()
44+
c_fname = base + '.c'
45+
try:
46+
c_file = open(c_fname, 'rb')
47+
except IOError:
48+
return False
49+
c_hash = sha1(c_file.read()).hexdigest()
50+
stamps[source_hash] = source
51+
stamps[c_hash] = c_fname
52+
# Read stamps from hash_stamp_fname; check in stamps dictionary
53+
try:
54+
stamp_file = open(hash_stamp_fname, 'rt')
55+
except IOError:
56+
return False
57+
for line in stamp_file:
58+
if line.startswith('#'):
59+
continue
60+
fname, hash = [e.strip() for e in line.split(',')]
61+
if not hash in stamps:
62+
return False
63+
# Compare path made canonical for \/
64+
fname = fname.replace(filesep, '/')
65+
if not stamps[hash].replace(filesep, '/') == fname:
66+
return False
67+
stamps.pop(hash)
68+
# All good if we found all hashes we need
69+
return len(stamps) == 0
70+
71+
72+
def cyproc_exts(exts, cython_min_version,
73+
hash_stamps_fname = 'pyx-stamps',
74+
build_ext=build_ext):
75+
""" Process sequence of `exts` to check if we need Cython. Return builder
76+
77+
Parameters
78+
----------
79+
exts : sequence of distutils ``Extension``
80+
If we already have good c files for any pyx or py sources, we replace
81+
the pyx or py files with their compiled up c versions inplace.
82+
cython_min_version : str
83+
Minimum cython version neede for compile
84+
hash_stamps_fname : str, optional
85+
filename with hashes for pyx/py and c files known to be in sync. Default
86+
is 'pyx-stamps'
87+
build_ext : distutils command
88+
default build_ext to return if not cythonizing. Default is distutils
89+
``build_ext`` class
90+
91+
Returns
92+
-------
93+
builder : ``distutils`` ``build_ext`` class or similar
94+
Can be ``build_ext`` input (if we have good c files) or cython
95+
``build_ext`` if we have a good cython, or a class raising an informative
96+
error on ``run()``
97+
need_cython : bool
98+
True if we need Cython to build extensions, False otherwise.
99+
"""
100+
if stamped_pyx_ok(exts, hash_stamps_fname):
101+
# Replace pyx with c files, use standard builder
102+
for mod in exts:
103+
sources = []
104+
for source in mod.sources:
105+
base, ext = splitext(source)
106+
if ext in ('.pyx', '.py'):
107+
sources.append(base + '.c')
108+
else:
109+
sources.append(source)
110+
mod.sources = sources
111+
return build_ext, False
112+
# We need cython
113+
try:
114+
from Cython.Compiler.Version import version as cyversion
115+
except ImportError:
116+
return derror_maker(build_ext,
117+
'Need cython>={0} to build extensions '
118+
'but cannot import "Cython"'.format(
119+
cython_min_version)), True
120+
if LooseVersion(cyversion) >= cython_min_version:
121+
from Cython.Distutils import build_ext as extbuilder
122+
return extbuilder, True
123+
return derror_maker(build_ext,
124+
'Need cython>={0} to build extensions'
125+
'but found cython version {1}'.format(
126+
cython_min_version, cyversion)), True
127+
128+
129+
def build_stamp(pyxes, include_dirs=()):
130+
""" Cythonize files in `pyxes`, return pyx, C filenames, hashes
131+
132+
Parameters
133+
----------
134+
pyxes : sequence
135+
sequence of filenames of files on which to run Cython
136+
include_dirs : sequence
137+
Any extra include directories in which to find Cython files.
138+
139+
Returns
140+
-------
141+
pyx_defs : dict
142+
dict has key, value pairs of <pyx_filename>, <pyx_info>, where
143+
<pyx_info> is a dict with key, value pairs of "pyx_hash", <pyx file SHA1
144+
hash>; "c_filename", <c filemane>; "c_hash", <c file SHA1 hash>.
145+
"""
146+
pyx_defs = {}
147+
from Cython.Compiler.Main import compile
148+
from Cython.Compiler.CmdLine import parse_command_line
149+
includes = sum([['--include-dir', d] for d in include_dirs], [])
150+
for source in pyxes:
151+
base, ext = splitext(source)
152+
pyx_hash = sha1(open(source, 'rt').read().encode('utf-8')).hexdigest()
153+
c_filename = base + '.c'
154+
options, sources = parse_command_line(includes + [source])
155+
result = compile(sources, options)
156+
if result.num_errors > 0:
157+
raise RuntimeError('Cython failed to compile ' + source)
158+
c_hash = sha1(open(c_filename, 'rt').read().encode('utf-8')).hexdigest()
159+
pyx_defs[source] = dict(pyx_hash=pyx_hash,
160+
c_filename=c_filename,
161+
c_hash=c_hash)
162+
return pyx_defs
163+
164+
165+
def write_stamps(pyx_defs, stamp_fname='pyx-stamps'):
166+
""" Write stamp information in `pyx_defs` to filename `stamp_fname`
167+
168+
Parameters
169+
----------
170+
pyx_defs : dict
171+
dict has key, value pairs of <pyx_filename>, <pyx_info>, where
172+
<pyx_info> is a dict with key, value pairs of "pyx_hash", <pyx file SHA1
173+
hash>; "c_filename", <c filemane>; "c_hash", <c file SHA1 hash>.
174+
stamp_fname : str
175+
filename to which to write stamp information
176+
"""
177+
with open(stamp_fname, 'wt') as stamp_file:
178+
stamp_file.write('# SHA1 hashes for pyx files and generated c files\n')
179+
stamp_file.write('# Auto-generated file, do not edit\n')
180+
for pyx_fname, pyx_info in pyx_defs.items():
181+
stamp_file.write('%s, %s\n' % (pyx_fname,
182+
pyx_info['pyx_hash']))
183+
stamp_file.write('%s, %s\n' % (pyx_info['c_filename'],
184+
pyx_info['c_hash']))
185+
186+
187+
def find_pyx(root_dir):
188+
""" Recursively find files with extension '.pyx' starting at `root_dir`
189+
190+
Parameters
191+
----------
192+
root_dir : str
193+
Directory from which to search for pyx files.
194+
195+
Returns
196+
-------
197+
pyxes : list
198+
list of filenames relative to `root_dir`
199+
"""
200+
pyxes = []
201+
for dirpath, dirnames, filenames in os.walk(root_dir):
202+
for filename in filenames:
203+
if not filename.endswith('.pyx'):
204+
continue
205+
base = relpath(dirpath, root_dir)
206+
pyxes.append(pjoin(base, filename))
207+
return pyxes
208+
209+
210+
def get_pyx_sdist(sdist_like=sdist, hash_stamps_fname='pyx-stamps',
211+
include_dirs=()):
212+
""" Add pyx->c conversion, hash recording to sdist command `sdist_like`
213+
214+
Parameters
215+
----------
216+
sdist_like : sdist command class, optional
217+
command that will do work of ``distutils.command.sdist.sdist``. By
218+
default we use the distutils version
219+
hash_stamps_fname : str, optional
220+
filename to which to write hashes of pyx / py and c files. Default is
221+
``pyx-stamps``
222+
include_dirs : sequence
223+
Any extra include directories in which to find Cython files.
224+
225+
Returns
226+
-------
227+
modified_sdist : sdist-like command class
228+
decorated `sdist_like` class, for compiling pyx / py files to c, putting
229+
the .c files in the the source archive, and writing hashes for these
230+
into the file named from `hash_stamps_fname`
231+
"""
232+
class PyxSDist(sdist_like):
233+
""" Custom distutils sdist command to generate .c files from pyx files.
234+
235+
Running the command object ``obj.run()`` will compile the pyx / py files
236+
in any extensions, into c files, and add them to the list of files to
237+
put into the source archive, as well as the usual behavior of distutils
238+
``sdist``. It will also take the sha1 hashes of the pyx / py and c
239+
files, and store them in a file ``pyx-stamps``, and put this file in the
240+
release tree. This allows someone who has the archive to know that the
241+
pyx and c files that they have are the ones packed into the archive, and
242+
therefore they may not need Cython at install time. See
243+
``cython_process_exts`` for the build-time command.
244+
"""
245+
246+
def make_distribution(self):
247+
""" Compile pyx to c files, add to sources, stamp sha1s """
248+
pyxes = []
249+
for mod in self.distribution.ext_modules:
250+
for source in mod.sources:
251+
base, ext = splitext(source)
252+
if ext in ('.pyx', '.py'):
253+
pyxes.append(source)
254+
self.pyx_defs = build_stamp(pyxes, include_dirs)
255+
for pyx_fname, pyx_info in self.pyx_defs.items():
256+
self.filelist.append(pyx_info['c_filename'])
257+
sdist_like.make_distribution(self)
258+
259+
def make_release_tree(self, base_dir, files):
260+
""" Put pyx stamps file into release tree """
261+
sdist_like.make_release_tree(self, base_dir, files)
262+
stamp_fname = pjoin(base_dir, hash_stamps_fname)
263+
write_stamps(self.pyx_defs, stamp_fname)
264+
265+
return PyxSDist
266+
267+
268+
def build_stamp_source(root_dir=None, stamp_fname='pyx-stamps',
269+
include_dirs=None):
270+
""" Build cython c files, make stamp file in source tree `root_dir`
271+
272+
Parameters
273+
----------
274+
root_dir : None or str, optional
275+
Directory from which to find ``.pyx`` files. If None, use current
276+
working directory.
277+
stamp_fname : str, optional
278+
Filename for stamp file we will write
279+
include_dirs : None or sequence
280+
Any extra Cython include directories
281+
"""
282+
if root_dir is None:
283+
root_dir = os.getcwd()
284+
if include_dirs is None:
285+
include_dirs = [pjoin(root_dir, 'src')]
286+
pyxes = find_pyx(root_dir)
287+
pyx_defs = build_stamp(pyxes, include_dirs=include_dirs)
288+
write_stamps(pyx_defs, stamp_fname)

0 commit comments

Comments
 (0)