Skip to content

Commit 0947ff0

Browse files
author
y-p
committed
BLD: added caching to setup.py - speeds up testing dramatically.
Cythoning results and compiled extension are cached. This is disabled by default, to enable it uncomment the line specifying the cache directory at the top of setup.py. you need to occasionally empty the cache manually, to keep it from from growing too much. BLD: added caching to setup.sys - speeds up testing dramatically. Cythoning results and compiled extension are cached. This is disabled by default, to enable it uncomment the line specifying the cache directory at the top of setup.py. you need to occasionally empty the cache manually, to keep it from from growing too much.
1 parent b5956fd commit 0947ff0

File tree

1 file changed

+225
-28
lines changed

1 file changed

+225
-28
lines changed

setup.py

+225-28
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
import shutil
1212
import warnings
1313

14+
try:
15+
BUILD_CACHE_DIR=None
16+
17+
# uncomment to activate the build cache
18+
#BUILD_CACHE_DIR="/tmp/.pandas_build_cache/"
19+
20+
if os.isdir(BUILD_CACHE_DIR):
21+
print("--------------------------------------------------------")
22+
print("BUILD CACHE ACTIVATED. be careful, this is experimental.")
23+
print("--------------------------------------------------------")
24+
else:
25+
BUILD_CACHE_DIR=None
26+
except :
27+
pass
28+
1429
# may need to work around setuptools bug by providing a fake Pyrex
1530
try:
1631
import Cython
@@ -87,8 +102,15 @@
87102

88103
from distutils.extension import Extension
89104
from distutils.command.build import build
90-
from distutils.command.build_ext import build_ext
91105
from distutils.command.sdist import sdist
106+
from distutils.command.build_ext import build_ext
107+
108+
try:
109+
from Cython.Distutils import build_ext
110+
#from Cython.Distutils import Extension # to get pyrex debugging symbols
111+
cython=True
112+
except ImportError:
113+
cython=False
92114

93115
from os.path import splitext, basename, join as pjoin
94116

@@ -314,38 +336,213 @@ def build_extensions(self):
314336
for ext in self.extensions:
315337
self.build_extension(ext)
316338

339+
class CompilationCacheMixin(object):
340+
def __init__(self,*args,**kwds):
341+
cache_dir=kwds.pop("cache_dir",BUILD_CACHE_DIR)
342+
self.cache_dir=cache_dir
343+
if not os.path.isdir(cache_dir):
344+
raise Exception("Error: path to Cache directory [%s] is not a dir");
345+
346+
def _copy_from_cache(self,hash,target):
347+
src=os.path.join(self.cache_dir,hash)
348+
if os.path.exists(src):
349+
# print("Cache HIT: asked to copy file %s in %s" % (src,os.path.abspath(target)))
350+
s="."
351+
for d in target.split(os.path.sep)[:-1]:
352+
s=os.path.join(s,d)
353+
if not os.path.exists(s):
354+
os.mkdir(s)
355+
shutil.copyfile(src,target)
356+
357+
return True
358+
359+
return False
360+
361+
def _put_to_cache(self,hash,src):
362+
target=os.path.join(self.cache_dir,hash)
363+
# print( "Cache miss: asked to copy file from %s to %s" % (src,target))
364+
s="."
365+
for d in target.split(os.path.sep)[:-1]:
366+
s=os.path.join(s,d)
367+
if not os.path.exists(s):
368+
os.mkdir(s)
369+
shutil.copyfile(src,target)
370+
371+
def _hash_obj(self,obj):
372+
"""
373+
you should override this method to provide a sensible
374+
implementation of hashing functions for your intended objects
375+
"""
376+
try:
377+
return hash(obj)
378+
except:
379+
raise NotImplementedError("You must override this method")
380+
381+
# this is missing in 2.5, mro will do the right thing
382+
def get_ext_fullpath(self, ext_name):
383+
"""Returns the path of the filename for a given extension.
384+
385+
The file is located in `build_lib` or directly in the package
386+
(inplace option).
387+
"""
388+
import string
389+
# makes sure the extension name is only using dots
390+
all_dots = string.maketrans('/'+os.sep, '..')
391+
ext_name = ext_name.translate(all_dots)
392+
393+
fullname = self.get_ext_fullname(ext_name)
394+
modpath = fullname.split('.')
395+
filename = self.get_ext_filename(ext_name)
396+
filename = os.path.split(filename)[-1]
397+
398+
if not self.inplace:
399+
# no further work needed
400+
# returning :
401+
# build_dir/package/path/filename
402+
filename = os.path.join(*modpath[:-1]+[filename])
403+
return os.path.join(self.build_lib, filename)
404+
405+
# the inplace option requires to find the package directory
406+
# using the build_py command for that
407+
package = '.'.join(modpath[0:-1])
408+
build_py = self.get_finalized_command('build_py')
409+
package_dir = os.path.abspath(build_py.get_package_dir(package))
410+
411+
# returning
412+
# package_dir/filename
413+
return os.path.join(package_dir, filename)
414+
415+
class CompilationCacheExtMixin(CompilationCacheMixin):
416+
def __init__(self,*args,**kwds):
417+
CompilationCacheMixin.__init__(self,*args,**kwds)
418+
419+
def _hash_file(self,fname):
420+
from hashlib import sha1
421+
try:
422+
hash=sha1()
423+
hash.update(self.build_lib.encode('utf-8'))
424+
try:
425+
if sys.version_info[0] >= 3:
426+
import io
427+
f=io.open(fname,"rb")
428+
else:
429+
f=open(fname)
430+
431+
first_line=f.readline()
432+
# ignore cython generation timestamp header
433+
if "Generated by Cython" not in first_line.decode('utf-8'):
434+
hash.update(first_line)
435+
hash.update(f.read())
436+
return hash.hexdigest()
437+
438+
except:
439+
raise
440+
return None
441+
finally:
442+
f.close()
443+
444+
except IOError:
445+
return None
446+
447+
def _hash_obj(self,ext):
448+
from hashlib import sha1
449+
450+
sources = ext.sources
451+
if sources is None or \
452+
(not hasattr(sources,'__iter__') ) or \
453+
isinstance(sources,str) or \
454+
sys.version[0]==2 and isinstance(sources,unicode): #argh
455+
return False
456+
457+
sources = list(sources) + ext.depends
458+
hash=sha1()
459+
try:
460+
for fname in sources:
461+
fhash=self._hash_file(fname)
462+
if fhash:
463+
hash.update(fhash.encode('utf-8'))
464+
except:
465+
return None
466+
467+
return hash.hexdigest()
468+
469+
class CachingBuildExt(build_ext,CompilationCacheExtMixin):
470+
def __init__(self,*args,**kwds):
471+
CompilationCacheExtMixin.__init__(self,*args,**kwds)
472+
kwds.pop("cache_dir",None)
473+
build_ext.__init__(self,*args,**kwds)
474+
475+
def build_extension(self, ext,*args,**kwds):
476+
ext_path = self.get_ext_fullpath(ext.name)
477+
build_path = os.path.join(self.build_lib,os.path.basename(ext_path))
478+
479+
hash=self._hash_obj(ext)
480+
if hash and self._copy_from_cache(hash,ext_path):
481+
return
482+
483+
build_ext.build_extension(self,ext,*args,**kwds)
484+
485+
hash=self._hash_obj(ext)
486+
if os.path.exists(build_path):
487+
self._put_to_cache(hash,build_path) # build_ext
488+
if os.path.exists(ext_path):
489+
self._put_to_cache(hash,ext_path) # develop
490+
491+
492+
def cython_sources(self, sources, extension):
493+
import re
494+
cplus = self.cython_cplus or getattr(extension, 'cython_cplus', 0) or \
495+
(extension.language and extension.language.lower() == 'c++')
496+
target_ext = '.c'
497+
if cplus:
498+
target_ext = '.cpp'
499+
500+
for i,s in enumerate(sources):
501+
if not re.search("\.(pyx|pxi|pxd)$",s):
502+
continue
503+
ext_dir=os.path.dirname(s)
504+
ext_basename=re.sub("\.[^\.]+$","",os.path.basename(s))
505+
ext_basename += target_ext
506+
target= os.path.join(ext_dir,ext_basename)
507+
hash=self._hash_file(s)
508+
sources[i]=target
509+
if hash and self._copy_from_cache(hash,target):
510+
continue
511+
build_ext.cython_sources(self,[s],extension)
512+
self._put_to_cache(hash,target)
513+
514+
return sources
515+
516+
class CythonCommand(build_ext):
517+
"""Custom distutils command subclassed from Cython.Distutils.build_ext
518+
to compile pyx->c, and stop there. All this does is override the
519+
C-compile method build_extension() with a no-op."""
520+
def build_extension(self, ext):
521+
pass
522+
523+
class DummyBuildSrc(Command):
524+
""" numpy's build_src command interferes with Cython's build_ext.
525+
"""
526+
user_options = []
527+
def initialize_options(self):
528+
self.py_modules_dict = {}
529+
def finalize_options(self):
530+
pass
531+
def run(self):
532+
pass
533+
317534
cmdclass = {'clean': CleanCommand,
318535
'build': build}
319-
320-
try:
321-
from Cython.Distutils import build_ext
322-
#from Cython.Distutils import Extension # to get pyrex debugging symbols
323-
cython=True
324-
except ImportError:
325-
cython=False
326-
suffix = '.c'
327-
cmdclass['build_ext'] = CheckingBuildExt
328-
else:
536+
if cython:
329537
suffix = '.pyx'
330-
class CythonCommand(build_ext):
331-
"""Custom distutils command subclassed from Cython.Distutils.build_ext
332-
to compile pyx->c, and stop there. All this does is override the
333-
C-compile method build_extension() with a no-op."""
334-
def build_extension(self, ext):
335-
pass
336-
337-
class DummyBuildSrc(Command):
338-
""" numpy's build_src command interferes with Cython's build_ext.
339-
"""
340-
user_options = []
341-
def initialize_options(self):
342-
self.py_modules_dict = {}
343-
def finalize_options(self):
344-
pass
345-
def run(self):
346-
pass
538+
cmdclass['build_ext'] = build_ext
539+
if BUILD_CACHE_DIR: # use the cache
540+
cmdclass['build_ext'] = CachingBuildExt
541+
else:
347542

543+
suffix = '.c'
348544
cmdclass['build_src'] = DummyBuildSrc
545+
349546
cmdclass['cython'] = CythonCommand
350547
cmdclass['build_ext'] = build_ext
351548
cmdclass['sdist'] = CheckSDist

0 commit comments

Comments
 (0)