2 from Cython
import __version__
4 import re
, os
, sys
, time
9 from glob
import glob
as iglob
27 from io
import open as io_open
29 from codecs
import open as io_open
32 from os
.path
import relpath
as _relpath
35 def _relpath(path
, start
=os
.path
.curdir
):
37 raise ValueError("no path specified")
38 start_list
= os
.path
.abspath(start
).split(os
.path
.sep
)
39 path_list
= os
.path
.abspath(path
).split(os
.path
.sep
)
40 i
= len(os
.path
.commonprefix([start_list
, path_list
]))
41 rel_list
= [os
.path
.pardir
] * (len(start_list
)-i
) + path_list
[i
:]
44 return os
.path
.join(*rel_list
)
47 from distutils
.extension
import Extension
49 from Cython
import Utils
50 from Cython
.Utils
import cached_function
, cached_method
, path_exists
, find_root_package_dir
51 from Cython
.Compiler
.Main
import Context
, CompilationOptions
, default_options
53 join_path
= cached_function(os
.path
.join
)
55 if sys
.version_info
[0] < 3:
56 # stupid Py2 distutils enforces str type in list of sources
57 _fs_encoding
= sys
.getfilesystemencoding()
58 if _fs_encoding
is None:
59 _fs_encoding
= sys
.getdefaultencoding()
60 def encode_filename_in_py2(filename
):
61 if isinstance(filename
, unicode):
62 return filename
.encode(_fs_encoding
)
65 def encode_filename_in_py2(filename
):
69 def extended_iglob(pattern
):
72 first
, rest
= pattern
.split('**/', 1)
74 first
= iglob(first
+'/')
78 for path
in extended_iglob(join_path(root
, rest
)):
82 for path
in extended_iglob(join_path(root
, '*', '**/' + rest
)):
87 for path
in iglob(pattern
):
91 def file_hash(filename
):
92 path
= os
.path
.normpath(filename
.encode("UTF-8"))
93 m
= hashlib
.md5(str(len(path
)) + ":")
95 f
= open(filename
, 'rb')
107 >>> parse_list("a b c")
109 >>> parse_list("[a, b, c]")
111 >>> parse_list('a " " b')
113 >>> parse_list('[a, ",a", "a,", ",", ]')
114 ['a', ',a', 'a,', ',']
116 if s
[0] == '[' and s
[-1] == ']':
121 s
, literals
= strip_string_literals(s
)
122 def unquote(literal
):
123 literal
= literal
.strip()
124 if literal
[0] in "'\"":
125 return literals
[literal
[1:-1]]
128 return [unquote(item
) for item
in s
.split(delimiter
) if item
.strip()]
130 transitive_str
= object()
131 transitive_list
= object()
133 distutils_settings
= {
136 'define_macros': list,
137 'undef_macros': list,
138 'libraries': transitive_list
,
139 'library_dirs': transitive_list
,
140 'runtime_library_dirs': transitive_list
,
141 'include_dirs': transitive_list
,
142 'extra_objects': list,
143 'extra_compile_args': transitive_list
,
144 'extra_link_args': transitive_list
,
145 'export_symbols': list,
146 'depends': transitive_list
,
147 'language': transitive_str
,
150 @cython.locals(start
=long, end
=long)
151 def line_iter(source
):
152 if isinstance(source
, basestring
):
155 end
= source
.find('\n', start
)
159 yield source
[start
:end
]
165 class DistutilsInfo(object):
167 def __init__(self
, source
=None, exn
=None):
169 if source
is not None:
170 for line
in line_iter(source
):
172 if line
!= '' and line
[0] != '#':
174 line
= line
[1:].strip()
175 if line
[:10] == 'distutils:':
178 key
= str(line
[:ix
].strip())
179 value
= line
[ix
+1:].strip()
180 type = distutils_settings
[key
]
181 if type in (list, transitive_list
):
182 value
= parse_list(value
)
183 if key
== 'define_macros':
184 value
= [tuple(macro
.split('=')) for macro
in value
]
185 self
.values
[key
] = value
186 elif exn
is not None:
187 for key
in distutils_settings
:
188 if key
in ('name', 'sources'):
190 value
= getattr(exn
, key
, None)
192 self
.values
[key
] = value
194 def merge(self
, other
):
197 for key
, value
in other
.values
.items():
198 type = distutils_settings
[key
]
199 if type is transitive_str
and key
not in self
.values
:
200 self
.values
[key
] = value
201 elif type is transitive_list
:
202 if key
in self
.values
:
203 all
= self
.values
[key
]
208 self
.values
[key
] = value
211 def subs(self
, aliases
):
214 resolved
= DistutilsInfo()
215 for key
, value
in self
.values
.items():
216 type = distutils_settings
[key
]
217 if type in [list, transitive_list
]:
222 if isinstance(v
, list):
225 new_value_list
.append(v
)
226 value
= new_value_list
229 value
= aliases
[value
]
230 resolved
.values
[key
] = value
233 def apply(self
, extension
):
234 for key
, value
in self
.values
.items():
235 type = distutils_settings
[key
]
236 if type in [list, transitive_list
]:
237 getattr(extension
, key
).extend(value
)
239 setattr(extension
, key
, value
)
241 @cython.locals(start
=long, q
=long, single_q
=long, double_q
=long, hash_mark
=long,
242 end
=long, k
=long, counter
=long, quote_len
=long)
243 def strip_string_literals(code
, prefix
='__Pyx_L'):
245 Normalizes every string literal to be of the form '__Pyx_Lxxx',
246 returning the normalized code and a mapping of labels to
254 hash_mark
= single_q
= double_q
= -1
259 hash_mark
= code
.find('#', q
)
261 single_q
= code
.find("'", q
)
263 double_q
= code
.find('"', q
)
264 q
= min(single_q
, double_q
)
265 if q
== -1: q
= max(single_q
, double_q
)
268 if q
== -1 and hash_mark
== -1:
269 new_code
.append(code
[start
:])
272 # Try to close the quote.
274 if code
[q
-1] == u
'\\':
276 while q
>= k
and code
[q
-k
] == u
'\\':
281 if code
[q
] == quote_type
and (quote_len
== 1 or (code_len
> q
+ 2 and quote_type
== code
[q
+1] == code
[q
+2])):
283 label
= "%s%s_" % (prefix
, counter
)
284 literals
[label
] = code
[start
+quote_len
:q
]
285 full_quote
= code
[q
:q
+quote_len
]
286 new_code
.append(full_quote
)
287 new_code
.append(label
)
288 new_code
.append(full_quote
)
296 elif -1 != hash_mark
and (hash_mark
< q
or q
== -1):
297 new_code
.append(code
[start
:hash_mark
+1])
298 end
= code
.find('\n', hash_mark
)
300 label
= "%s%s_" % (prefix
, counter
)
305 literals
[label
] = code
[hash_mark
+1:end_or_none
]
306 new_code
.append(label
)
313 if code_len
>= q
+3 and (code
[q
] == code
[q
+1] == code
[q
+2]):
319 new_code
.append(code
[start
:q
])
323 return "".join(new_code
), literals
326 dependancy_regex
= re
.compile(r
"(?:^from +([0-9a-zA-Z_.]+) +cimport)|"
327 r
"(?:^cimport +([0-9a-zA-Z_.]+)\b)|"
328 r
"(?:^cdef +extern +from +['\"]([^
'\"]+)['\"])|
"
329 r"(?
:^include
+['\"]([^'\"]+)['\"])", re.M)
331 def normalize_existing(base_path, rel_paths):
332 return normalize_existing0(os.path.dirname(base_path), tuple(set(rel_paths)))
335 def normalize_existing0(base_dir, rel_paths):
337 for rel in rel_paths:
338 path = join_path(base_dir, rel)
339 if path_exists(path):
340 normalized.append(os.path.normpath(path))
342 normalized.append(rel)
345 def resolve_depends(depends, include_dirs):
346 include_dirs = tuple(include_dirs)
348 for depend in depends:
349 path = resolve_depend(depend, include_dirs)
351 resolved.append(path)
355 def resolve_depend(depend, include_dirs):
356 if depend[0] == '<' and depend[-1] == '>':
358 for dir in include_dirs:
359 path = join_path(dir, depend)
360 if path_exists(path):
361 return os.path.normpath(path)
365 def package(filename):
366 dir = os.path.dirname(os.path.abspath(str(filename)))
367 if dir != filename and path_exists(join_path(dir, '__init__
.py
')):
368 return package(dir) + (os.path.basename(dir),)
373 def fully_qualified_name(filename):
374 module = os.path.splitext(os.path.basename(filename))[0]
375 return '.'.join(package(filename) + (module,))
379 def parse_dependencies(source_filename):
380 # Actual parsing is way to slow, so we use regular expressions.
381 # The only catch is that we must strip comments and string
382 # literals ahead of time.
383 fh = Utils.open_source_file(source_filename, "rU", error_handling='ignore
')
388 distutils_info = DistutilsInfo(source)
389 source, literals = strip_string_literals(source)
390 source = source.replace('\\\n', ' ').replace('\t', ' ')
396 for m in dependancy_regex.finditer(source):
397 cimport_from, cimport, extern, include = m.groups()
399 cimports.append(cimport_from)
401 cimports.append(cimport)
403 externs.append(literals[extern])
405 includes.append(literals[include])
406 return cimports, includes, externs, distutils_info
409 class DependencyTree(object):
411 def __init__(self, context, quiet=False):
412 self.context = context
414 self._transitive_cache = {}
416 def parse_dependencies(self, source_filename):
417 return parse_dependencies(source_filename)
420 def included_files(self, filename):
421 # This is messy because included files are textually included, resolving
422 # cimports (but not includes) relative to the including file.
424 for include in self.parse_dependencies(filename)[1]:
425 include_path = join_path(os.path.dirname(filename), include)
426 if not path_exists(include_path):
427 include_path = self.context.find_include_file(include, None)
429 if '.' + os.path.sep in include_path:
430 include_path = os.path.normpath(include_path)
431 all.add(include_path)
432 all.update(self.included_files(include_path))
434 print("Unable to locate '%s' referenced from '%s'" % (filename, include))
438 def cimports_and_externs(self, filename):
439 # This is really ugly. Nested cimports are resolved with respect to the
440 # includer, but includes are resolved with respect to the includee.
441 cimports, includes, externs = self.parse_dependencies(filename)[:3]
442 cimports = set(cimports)
443 externs = set(externs)
444 for include in self.included_files(filename):
445 included_cimports, included_externs = self.cimports_and_externs(include)
446 cimports.update(included_cimports)
447 externs.update(included_externs)
448 return tuple(cimports), normalize_existing(filename, externs)
450 def cimports(self, filename):
451 return self.cimports_and_externs(filename)[0]
453 def package(self, filename):
454 return package(filename)
456 def fully_qualified_name(self, filename):
457 return fully_qualified_name(filename)
460 def find_pxd(self, module, filename=None):
461 is_relative = module[0] == '.'
462 if is_relative and not filename:
463 raise NotImplementedError("New relative imports.")
464 if filename is not None:
465 module_path = module.split('.')
467 module_path.pop(0) # just explicitly relative
468 package_path = list(self.package(filename))
469 while module_path and not module_path[0]:
473 return None # FIXME: error?
475 relative = '.'.join(package_path + module_path)
476 pxd = self.context.find_pxd_file(relative, None)
480 return None # FIXME: error?
481 return self.context.find_pxd_file(module, None)
484 def cimported_files(self, filename):
485 if filename[-4:] == '.pyx
' and path_exists(filename[:-4] + '.pxd
'):
486 pxd_list = [filename[:-4] + '.pxd
']
489 for module in self.cimports(filename):
490 if module[:7] == 'cython
.' or module == 'cython
':
492 pxd_file = self.find_pxd(module, filename)
493 if pxd_file is not None:
494 pxd_list.append(pxd_file)
496 print("missing cimport in module '%s': %s" % (module, filename))
497 return tuple(pxd_list)
500 def immediate_dependencies(self, filename):
501 all = set([filename])
502 all.update(self.cimported_files(filename))
503 all.update(self.included_files(filename))
506 def all_dependencies(self, filename):
507 return self.transitive_merge(filename, self.immediate_dependencies, set.union)
510 def timestamp(self, filename):
511 return os.path.getmtime(filename)
513 def extract_timestamp(self, filename):
514 return self.timestamp(filename), filename
516 def newest_dependency(self, filename):
517 return max([self.extract_timestamp(f) for f in self.all_dependencies(filename)])
519 def transitive_fingerprint(self, filename, extra=None):
521 m = hashlib.md5(__version__)
522 m.update(file_hash(filename))
523 for x in sorted(self.all_dependencies(filename)):
524 if os.path.splitext(x)[1] not in ('.c
', '.cpp
', '.h
'):
525 m.update(file_hash(x))
526 if extra is not None:
532 def distutils_info0(self, filename):
533 info = self.parse_dependencies(filename)[3]
534 externs = self.cimports_and_externs(filename)[1]
536 if 'depends
' in info.values:
537 info.values['depends
'] = list(set(info.values['depends
']).union(externs))
539 info.values['depends
'] = list(externs)
542 def distutils_info(self, filename, aliases=None, base=None):
543 return (self.transitive_merge(filename, self.distutils_info0, DistutilsInfo.merge)
547 def transitive_merge(self, node, extract, merge):
549 seen = self._transitive_cache[extract, merge]
551 seen = self._transitive_cache[extract, merge] = {}
552 return self.transitive_merge_helper(
553 node, extract, merge, seen, {}, self.cimported_files)[0]
555 def transitive_merge_helper(self, node, extract, merge, seen, stack, outgoing):
557 return seen[node], None
562 stack[node] = len(stack)
564 for next in outgoing(node):
565 sub_deps, sub_loop = self.transitive_merge_helper(next, extract, merge, seen, stack, outgoing)
566 if sub_loop is not None:
567 if loop is not None and stack[loop] < stack[sub_loop]:
571 deps = merge(deps, sub_deps)
581 def create_dependency_tree(ctx=None, quiet=False):
583 if _dep_tree is None:
585 ctx = Context(["."], CompilationOptions(default_options))
586 _dep_tree = DependencyTree(ctx, quiet=quiet)
589 # This may be useful for advanced users?
590 def create_extension_list(patterns, exclude=[], ctx=None, aliases=None, quiet=False, exclude_failures=False):
591 if not isinstance(patterns, (list, tuple)):
592 patterns = [patterns]
593 explicit_modules = set([m.name for m in patterns if isinstance(m, Extension)])
595 deps = create_dependency_tree(ctx, quiet=quiet)
597 if not isinstance(exclude, list):
599 for pattern in exclude:
600 to_exclude.update(map(os.path.abspath, extended_iglob(pattern)))
602 for pattern in patterns:
603 if isinstance(pattern, str):
604 filepattern = pattern
609 elif isinstance(pattern, Extension):
610 filepattern = pattern.sources[0]
611 if os.path.splitext(filepattern)[1] not in ('.py
', '.pyx
'):
612 # ignore non-cython modules
613 module_list.append(pattern)
617 base = DistutilsInfo(exn=template)
618 exn_type = template.__class__
620 raise TypeError(pattern)
621 for file in extended_iglob(filepattern):
622 if os.path.abspath(file) in to_exclude:
624 pkg = deps.package(file)
626 module_name = deps.fully_qualified_name(file)
627 if module_name in explicit_modules:
631 if module_name not in seen:
633 kwds = deps.distutils_info(file, aliases, base).values
639 for key, value in base.values.items():
643 if template is not None:
644 sources += template.sources[1:]
645 if 'sources
' in kwds:
646 # allow users to add .c files etc.
647 for source in kwds['sources
']:
648 source = encode_filename_in_py2(source)
649 if source not in sources:
650 sources.append(source)
652 if 'depends
' in kwds:
653 depends = resolve_depends(kwds['depends
'], (kwds.get('include_dirs
') or []) + [find_root_package_dir(file)])
654 if template is not None:
655 # Always include everything from the template.
656 depends = list(set(template.depends).union(set(depends)))
657 kwds['depends
'] = depends
658 module_list.append(exn_type(
666 # This is the user-exposed entry point.
667 def cythonize(module_list, exclude=[], nthreads=0, aliases=None, quiet=False, force=False,
668 exclude_failures=False, **options):
670 Compile a set of source modules into C/C++ files and return a list of distutils
671 Extension objects for them.
673 As module list, pass either a glob pattern, a list of glob patterns or a list of
674 Extension objects. The latter allows you to configure the extensions separately
675 through the normal distutils options.
677 When using glob patterns, you can exclude certain module names explicitly
678 by passing them into the 'exclude
' option.
680 For parallel compilation, set the 'nthreads
' option to the number of
683 For a broad 'try to
compile' mode that ignores compilation failures and
684 simply excludes the failed extensions, pass 'exclude_failures
=True'. Note
685 that this only really makes sense for compiling .py files which can also
686 be used without compilation.
688 Additional compilation options can be passed as keyword arguments.
690 if 'include_path
' not in options:
691 options['include_path
'] = ['.']
692 if 'common_utility_include_dir
' in options:
693 if options.get('cache
'):
694 raise NotImplementedError("common_utility_include_dir does not yet work with caching")
695 if not os.path.exists(options['common_utility_include_dir
']):
696 os.makedirs(options['common_utility_include_dir
'])
697 c_options = CompilationOptions(**options)
698 cpp_options = CompilationOptions(**options); cpp_options.cplus = True
699 ctx = c_options.create_context()
701 module_list = create_extension_list(
706 exclude_failures=exclude_failures,
708 deps = create_dependency_tree(ctx, quiet=quiet)
709 build_dir = getattr(options, 'build_dir
', None)
710 modules_by_cfile = {}
712 for m in module_list:
714 root = os.path.realpath(os.path.abspath(find_root_package_dir(m.sources[0])))
715 def copy_to_build_dir(filepath, root=root):
716 filepath_abs = os.path.realpath(os.path.abspath(filepath))
717 if os.path.isabs(filepath):
718 filepath = filepath_abs
719 if filepath_abs.startswith(root):
720 mod_dir = os.path.join(build_dir,
721 os.path.dirname(_relpath(filepath, root)))
722 if not os.path.isdir(mod_dir):
724 shutil.copy(filepath, mod_dir)
725 for dep in m.depends:
726 copy_to_build_dir(dep)
729 for source in m.sources:
730 base, ext = os.path.splitext(source)
731 if ext in ('.pyx
', '.py
'):
732 if m.language == 'c
++':
733 c_file = base + '.cpp
'
734 options = cpp_options
739 # setup for out of place build directory if enabled
741 c_file = os.path.join(build_dir, c_file)
742 dir = os.path.dirname(c_file)
743 if not os.path.isdir(dir):
746 if os.path.exists(c_file):
747 c_timestamp = os.path.getmtime(c_file)
751 # Priority goes first to modified files, second to direct
752 # dependents, and finally to indirect dependents.
753 if c_timestamp < deps.timestamp(source):
754 dep_timestamp, dep = deps.timestamp(source), source
757 dep_timestamp, dep = deps.newest_dependency(source)
758 priority = 2 - (dep in deps.immediate_dependencies(source))
759 if force or c_timestamp < dep_timestamp:
762 print("Compiling %s because it changed." % source)
764 print("Compiling %s because it depends on %s." % (source, dep))
765 if not force and hasattr(options, 'cache
'):
767 fingerprint = deps.transitive_fingerprint(source, extra)
770 to_compile.append((priority, source, c_file, fingerprint, quiet,
771 options, not exclude_failures))
772 new_sources.append(c_file)
773 if c_file not in modules_by_cfile:
774 modules_by_cfile[c_file] = [m]
776 modules_by_cfile[c_file].append(m)
778 new_sources.append(source)
780 copy_to_build_dir(source)
781 m.sources = new_sources
782 if hasattr(options, 'cache
'):
783 if not os.path.exists(options.cache):
784 os.makedirs(options.cache)
787 # Requires multiprocessing (or Python >= 2.6)
789 import multiprocessing
790 pool = multiprocessing.Pool(nthreads)
791 except (ImportError, OSError):
792 print("multiprocessing required for parallel cythonization")
795 pool.map(cythonize_one_helper, to_compile)
797 for args in to_compile:
798 cythonize_one(*args[1:])
800 failed_modules = set()
801 for c_file, modules in modules_by_cfile.iteritems():
802 if not os.path.exists(c_file):
803 failed_modules.update(modules)
804 elif os.path.getsize(c_file) < 200:
805 f = io_open(c_file, 'r
', encoding='iso8859
-1')
807 if f.read(len('#error ')) == '#error ':
808 # dead compilation result
809 failed_modules
.update(modules
)
813 for module
in failed_modules
:
814 module_list
.remove(module
)
815 print("Failed compilations: %s" % ', '.join(sorted([
816 module
.name
for module
in failed_modules
])))
817 if hasattr(options
, 'cache'):
818 cleanup_cache(options
.cache
, getattr(options
, 'cache_size', 1024 * 1024 * 100))
819 # cythonize() is often followed by the (non-Python-buffered)
820 # compiler output, flush now to avoid interleaving output.
825 if os
.environ
.get('XML_RESULTS'):
826 compile_result_dir
= os
.environ
['XML_RESULTS']
827 def record_results(func
):
828 def with_record(*args
):
838 module
= fully_qualified_name(args
[0])
839 name
= "cythonize." + module
840 failures
= 1 - success
844 failure_item
= "failure"
845 output
= open(os
.path
.join(compile_result_dir
, name
+ ".xml"), "w")
847 <?xml version="1.0" ?>
848 <testsuite name="%(name)s" errors="0" failures="%(failures)s" tests="1" time="%(t)s">
849 <testcase classname="%(name)s" name="cythonize">
853 """.strip() % locals())
857 record_results
= lambda x
: x
859 # TODO: Share context? Issue: pyx processing leaks into pxd module
861 def cythonize_one(pyx_file
, c_file
, fingerprint
, quiet
, options
=None, raise_on_failure
=True):
862 from Cython
.Compiler
.Main
import compile, default_options
863 from Cython
.Compiler
.Errors
import CompileError
, PyrexError
866 if not os
.path
.exists(options
.cache
):
868 os
.mkdir(options
.cache
)
870 if not os
.path
.exists(options
.cache
):
872 # Cython-generated c files are highly compressible.
873 # (E.g. a compression ratio of about 10 for Sage).
874 fingerprint_file
= join_path(
875 options
.cache
, "%s-%s%s" % (os
.path
.basename(c_file
), fingerprint
, gzip_ext
))
876 if os
.path
.exists(fingerprint_file
):
878 print("Found compiled %s in cache" % pyx_file
)
879 os
.utime(fingerprint_file
, None)
880 g
= gzip_open(fingerprint_file
, 'rb')
882 f
= open(c_file
, 'wb')
884 shutil
.copyfileobj(g
, f
)
891 print("Cythonizing %s" % pyx_file
)
893 options
= CompilationOptions(default_options
)
894 options
.output_file
= c_file
898 result
= compile([pyx_file
], options
)
899 if result
.num_errors
> 0:
901 except (EnvironmentError, PyrexError
), e
:
902 sys
.stderr
.write('%s\n' % e
)
906 traceback
.print_exc()
911 traceback
.print_exc()
915 raise CompileError(None, pyx_file
)
916 elif os
.path
.exists(c_file
):
919 f
= open(c_file
, 'rb')
921 g
= gzip_open(fingerprint_file
, 'wb')
923 shutil
.copyfileobj(f
, g
)
929 def cythonize_one_helper(m
):
932 return cythonize_one(*m
[1:])
934 traceback
.print_exc()
937 def cleanup_cache(cache
, target_size
, ratio
=.85):
939 p
= subprocess
.Popen(['du', '-s', '-k', os
.path
.abspath(cache
)], stdout
=subprocess
.PIPE
)
942 total_size
= 1024 * int(p
.stdout
.read().strip().split()[0])
943 if total_size
< target_size
:
945 except (OSError, ValueError):
949 for file in os
.listdir(cache
):
950 path
= join_path(cache
, file)
952 total_size
+= s
.st_size
953 all
.append((s
.st_atime
, s
.st_size
, path
))
954 if total_size
> target_size
:
955 for time
, size
, file in reversed(sorted(all
)):
958 if total_size
< target_size
* ratio
: