merged (and partially rewrote) dependency tracking and package resolution changes...
authorStefan Behnel <scoder@users.berlios.de>
Wed, 4 Jun 2008 16:55:23 +0000 (18:55 +0200)
committerStefan Behnel <scoder@users.berlios.de>
Wed, 4 Jun 2008 16:55:23 +0000 (18:55 +0200)
Cython/Compiler/CmdLine.py
Cython/Compiler/Main.py
Cython/Compiler/ModuleNode.py
Cython/Compiler/Parsing.py
Cython/Compiler/Scanning.py
Cython/Compiler/Symtab.py
Cython/Utils.py

index 11caafc8eb3149e643c4a859f4b7984ac8985fef..16315d0b64770cad5c7c97edf8a36565f41de565 100644 (file)
@@ -18,6 +18,10 @@ Options:
   -I, --include-dir <directory>  Search for include files in named directory
                                  (multiply include directories are allowed).
   -o, --output-file <filename>   Specify name of generated C file
+  -r, --recursive                Recursively find and compile dependencies
+  -t, --timestamps               Only compile newer source files (implied with -r)
+  -f, --force                    Compile all source files (overrides implied -t)
+  -q, --quiet                    Don't print module names in recursive mode
   -p, --embed-positions          If specified, the positions in Cython files of each
                                  function definition is embedded in its docstring.
   -z, --pre-import <module>      If specified, assume undeclared names in this 
@@ -111,6 +115,12 @@ def parse_command_line(args):
                 options.working_path = pop_arg()
             elif option in ("-o", "--output-file"):
                 options.output_file = pop_arg()
+            elif option in ("-r", "--recursive"):
+                options.recursive = 1
+            elif option in ("-t", "--timestamps"):
+                options.timestamps = 1
+            elif option in ("-f", "--force"):
+                options.timestamps = 0
             elif option in ("-p", "--embed-positions"):
                 Options.embed_pos_in_docstring = 1
             elif option in ("-z", "--pre-import"):
index 0782f52f43c8a2e0a73615fb6e1e8d84d3add49d..d7a3d6c6663ffd13f80ac07d97357dce4ec948e2 100644 (file)
@@ -3,8 +3,8 @@
 #
 
 import os, sys, re, codecs
-if sys.version_info[:2] < (2, 2):
-    sys.stderr.write("Sorry, Cython requires Python 2.2 or later\n")
+if sys.version_info[:2] < (2, 3):
+    sys.stderr.write("Sorry, Cython requires Python 2.3 or later\n")
     sys.exit(1)
 
 try:
@@ -14,14 +14,13 @@ except NameError:
     from sets import Set as set
 
 from time import time
-import Version
-from Scanning import PyrexScanner
+import Code
 import Errors
-from Errors import PyrexError, CompileError, error
 import Parsing
+import Version
+from Errors import PyrexError, CompileError, error
+from Scanning import PyrexScanner
 from Symtab import BuiltinScope, ModuleScope
-import Code
-from Cython.Utils import replace_suffix
 from Cython import Utils
 import Transform
 
@@ -93,31 +92,34 @@ class Context:
                 try:
                     if debug_find_module:
                         print("Context.find_module: Parsing %s" % pxd_pathname)
-                    pxd_tree = self.parse(pxd_pathname, scope.type_names, pxd = 1,
+                    pxd_tree = self.parse(pxd_pathname, scope, pxd = 1,
                                           full_module_name = module_name)
                     pxd_tree.analyse_declarations(scope)
                 except CompileError:
                     pass
         return scope
     
-    def find_pxd_file(self, module_name, pos):
-        # Search include directories for the .pxd file
-        # corresponding to the given (full) module name.
-        if "." in module_name:
-            pxd_filename = "%s.pxd" % os.path.join(*module_name.split('.'))
-        else:
-            pxd_filename = "%s.pxd" % module_name
-        return self.search_include_directories(pxd_filename, pos)
+    def find_pxd_file(self, qualified_name, pos):
+        # Search include path for the .pxd file corresponding to the
+        # given fully-qualified module name.
+        return self.search_include_directories(qualified_name, ".pxd", pos)
+
+    def find_pyx_file(self, qualified_name, pos):
+        # Search include path for the .pyx file corresponding to the
+        # given fully-qualified module name, as for find_pxd_file().
+        return self.search_include_directories(qualified_name, ".pyx", pos)
     
     def find_include_file(self, filename, pos):
         # Search list of include directories for filename.
         # Reports an error and returns None if not found.
-        path = self.search_include_directories(filename, pos)
+        path = self.search_include_directories(filename, "", pos,
+                                               split_package=False)
         if not path:
             error(pos, "'%s' not found" % filename)
         return path
     
-    def search_include_directories(self, filename, pos):
+    def search_include_directories(self, qualified_name, suffix, pos,
+                                   split_package=True):
         # Search the list of include directories for the given
         # file name. If a source file position is given, first
         # searches the directory containing that file. Returns
@@ -126,12 +128,81 @@ class Context:
         if pos:
             here_dir = os.path.dirname(pos[0])
             dirs = [here_dir] + dirs
+
+        dotted_filename = qualified_name + suffix
+        if split_package:
+            names = qualified_name.split('.')
+            package_names = names[:-1]
+            module_name = names[-1]
+            module_filename = module_name + suffix
+            package_filename = "__init__" + suffix
+
         for dir in dirs:
-            path = os.path.join(dir, filename)
+            path = os.path.join(dir, dotted_filename)
             if os.path.exists(path):
                 return path
+            if split_package:
+                package_dir = self.check_package_dir(dir, package_names)
+                if package_dir is not None:
+                    path = os.path.join(package_dir, module_filename)
+                    if os.path.exists(path):
+                        return path
+                    path = os.path.join(dir, package_dir, module_name,
+                                        package_filename)
+                    if os.path.exists(path):
+                        return path
         return None
 
+    def check_package_dir(self, dir, package_names):
+        package_dir = os.path.join(dir, *package_names)
+        if not os.path.exists(package_dir):
+            return None
+        for dirname in package_names:
+            dir = os.path.join(dir, dirname)
+            package_init = os.path.join(dir, "__init__.py")
+            if not os.path.exists(package_init) and \
+                    not os.path.exists(package_init + "x"): # same with .pyx ?
+                return None
+        return package_dir
+
+    def c_file_out_of_date(self, source_path):
+        c_path = Utils.replace_suffix(source_path, ".c")
+        if not os.path.exists(c_path):
+            return 1
+        c_time = Utils.modification_time(c_path)
+        if Utils.file_newer_than(source_path, c_time):
+            return 1
+        pos = [source_path]
+        pxd_path = Utils.replace_suffix(source_path, ".pxd")
+        if os.path.exists(pxd_path) and Utils.file_newer_than(pxd_path, c_time):
+            return 1
+        for kind, name in self.read_dependency_file(source_path):
+            if kind == "cimport":
+                dep_path = self.find_pxd_file(name, pos)
+            elif kind == "include":
+                dep_path = self.search_include_directories(name, pos)
+            else:
+                continue
+            if dep_path and Utils.file_newer_than(dep_path, c_time):
+                return 1
+        return 0
+    
+    def find_cimported_module_names(self, source_path):
+        return [ name for kind, name in self.read_dependency_file(source_path)
+                 if kind == "cimport" ]
+    
+    def read_dependency_file(self, source_path):
+        dep_path = replace_suffix(source_path, ".dep")
+        if os.path.exists(dep_path):
+            f = open(dep_path, "rU")
+            chunks = [ line.strip().split(" ", 1)
+                       for line in f.readlines()
+                       if " " in line.strip() ]
+            f.close()
+            return chunks
+        else:
+            return ()
+
     def lookup_submodule(self, name):
         # Look up a top-level module. Returns None if not found.
         return self.modules.get(name, None)
@@ -145,14 +216,14 @@ class Context:
             self.modules[name] = scope
         return scope
 
-    def parse(self, source_filename, type_names, pxd, full_module_name):
+    def parse(self, source_filename, scope, pxd, full_module_name):
         name = Utils.encode_filename(source_filename)
         # Parse the given source file and return a parse tree.
         try:
             f = Utils.open_source_file(source_filename, "rU")
             try:
                 s = PyrexScanner(f, name, source_encoding = f.encoding,
-                                 type_names = type_names, context = self)
+                                 scope = scope, context = self)
                 tree = Parsing.p_module(s, pxd, full_module_name)
             finally:
                 f.close()
@@ -185,7 +256,7 @@ class Context:
         result.main_source_file = source
 
         if options.use_listing_file:
-            result.listing_file = replace_suffix(source, ".lis")
+            result.listing_file = Utils.replace_suffix(source, ".lis")
             Errors.open_listing_file(result.listing_file,
                 echo_to_stderr = options.errors_to_stderr)
         else:
@@ -197,19 +268,14 @@ class Context:
                 c_suffix = ".cpp"
             else:
                 c_suffix = ".c"
-            result.c_file = replace_suffix(source, c_suffix)
-        c_stat = None
-        if result.c_file:
-            try:
-                c_stat = os.stat(result.c_file)
-            except EnvironmentError:
-                pass
+            result.c_file = Utils.replace_suffix(source, c_suffix)
         module_name = full_module_name # self.extract_module_name(source, options)
         initial_pos = (source, 1, 0)
         scope = self.find_module(module_name, pos = initial_pos, need_pxd = 0)
         errors_occurred = False
         try:
-            tree = self.parse(source, scope.type_names, pxd = 0, full_module_name = full_module_name)
+            tree = self.parse(source, scope, pxd = 0,
+                              full_module_name = full_module_name)
             tree.process_implementation(scope, options, result)
         except CompileError:
             errors_occurred = True
@@ -219,8 +285,7 @@ class Context:
             errors_occurred = True
         if errors_occurred and result.c_file:
             try:
-                #os.unlink(result.c_file)
-                Utils.castrate_file(result.c_file, c_stat)
+                Utils.castrate_file(result.c_file, os.stat(source))
             except EnvironmentError:
                 pass
             result.c_file = None
@@ -237,7 +302,7 @@ class Context:
 
 #------------------------------------------------------------------------
 #
-#  Main Python entry point
+#  Main Python entry points
 #
 #------------------------------------------------------------------------
 
@@ -251,6 +316,10 @@ class CompilationOptions:
     include_path      [string]  Directories to search for include files
     output_file       string    Name of generated .c file
     generate_pxi      boolean   Generate .pxi file for public declarations
+    recursive         boolean   Recursively find and compile dependencies
+    timestamps        boolean   Only compile changed source files. If None,
+                                defaults to true when recursive is true.
+    quiet             boolean   Don't print source names in recursive mode
     transforms        Transform.TransformSet Transforms to use on the parse tree
     
     Following options are experimental and only used on MacOSX:
@@ -261,7 +330,7 @@ class CompilationOptions:
     cplus             boolean   Compile as c++ code
     """
     
-    def __init__(self, defaults = None, **kw):
+    def __init__(self, defaults = None, c_compile = 0, c_link = 0, **kw):
         self.include_path = []
         self.objects = []
         if defaults:
@@ -271,6 +340,10 @@ class CompilationOptions:
             defaults = default_options
         self.__dict__.update(defaults)
         self.__dict__.update(kw)
+        if c_compile:
+            self.c_only = 0
+        if c_link:
+            self.obj_only = 0
 
 
 class CompilationResult:
@@ -298,24 +371,87 @@ class CompilationResult:
         self.main_source_file = None
 
 
-def compile(source, options = None, c_compile = 0, c_link = 0,
-            full_module_name = None):
+class CompilationResultSet(dict):
     """
-    compile(source, options = default_options)
+    Results from compiling multiple Pyrex source files. A mapping
+    from source file paths to CompilationResult instances. Also
+    has the following attributes:
     
-    Compile the given Cython implementation file and return
-    a CompilationResult object describing what was produced.
+    num_errors   integer   Total number of compilation errors
+    """
+    
+    num_errors = 0
+
+    def add(self, source, result):
+        self[source] = result
+        self.num_errors += result.num_errors
+
+
+def compile_single(source, options, full_module_name = None):
+    """
+    compile_single(source, options, full_module_name)
+    
+    Compile the given Pyrex implementation file and return a CompilationResult.
+    Always compiles a single file; does not perform timestamp checking or
+    recursion.
     """
-    if not options:
-        options = default_options
-    options = CompilationOptions(defaults = options)
-    if c_compile:
-        options.c_only = 0
-    if c_link:
-        options.obj_only = 0
     context = Context(options.include_path)
     return context.compile(source, options, full_module_name)
 
+def compile_multiple(sources, options):
+    """
+    compile_multiple(sources, options)
+    
+    Compiles the given sequence of Pyrex implementation files and returns
+    a CompilationResultSet. Performs timestamp checking and/or recursion
+    if these are specified in the options.
+    """
+    sources = [os.path.abspath(source) for source in sources]
+    processed = set()
+    results = CompilationResultSet()
+    context = Context(options.include_path)
+    recursive = options.recursive
+    timestamps = options.timestamps
+    if timestamps is None:
+        timestamps = recursive
+    verbose = recursive and not options.quiet
+    for source in sources:
+        if source not in processed:
+            if not timestamps or context.c_file_out_of_date(source):
+                if verbose:
+                    sys.stderr.write("Compiling %s\n" % source)
+                result = context.compile(source, options)
+                results.add(source, result)
+            processed.add(source)
+            if recursive:
+                for module_name in context.find_cimported_module_names(source):
+                    path = context.find_pyx_file(module_name, [source])
+                    if path:
+                        sources.append(path)
+                    else:
+                        sys.stderr.write(
+                            "Cannot find .pyx file for cimported module '%s'\n" % module_name)
+    return results
+
+def compile(source, options = None, c_compile = 0, c_link = 0,
+            full_module_name = None, **kwds):
+    """
+    compile(source [, options], [, <option> = <value>]...)
+    
+    Compile one or more Pyrex implementation files, with optional timestamp
+    checking and recursing on dependecies. The source argument may be a string
+    or a sequence of strings If it is a string and no recursion or timestamp
+    checking is requested, a CompilationResult is returned, otherwise a
+    CompilationResultSet is returned.
+    """
+    options = CompilationOptions(defaults = options, c_compile = c_compile,
+        c_link = c_link, **kwds)
+    if isinstance(source, basestring) and not options.timestamps \
+            and not options.recursive:
+        return compile_single(source, options, full_module_name)
+    else:
+        return compile_multiple(source, options)
+
 #------------------------------------------------------------------------
 #
 #  Main command-line entry point
@@ -329,21 +465,19 @@ def main(command_line = 0):
         from CmdLine import parse_command_line
         options, sources = parse_command_line(args)
     else:
-        options = default_options
+        options = CompilationOptions(default_options)
         sources = args
     if options.show_version:
         sys.stderr.write("Cython version %s\n" % Version.version)
     if options.working_path!="":
         os.chdir(options.working_path)
-    context = Context(options.include_path)
-    for source in sources:
-        try:
-            result = context.compile(source, options)
-            if result.num_errors > 0:
-                any_failures = 1
-        except PyrexError, e:
-            sys.stderr.write(str(e) + '\n')
+    try:
+        result = compile(sources, options)
+        if result.num_errors > 0:
             any_failures = 1
+    except (EnvironmentError, PyrexError), e:
+        sys.stderr.write(str(e) + '\n')
+        any_failures = 1
     if any_failures:
         sys.exit(1)
 
@@ -363,6 +497,9 @@ default_options = dict(
     output_file = None,
     annotate = False,
     generate_pxi = 0,
+    recursive = 0,
+    timestamps = None,
+    quiet = 0,
     transforms = Transform.TransformSet(),
     working_path = "")
     
index 066bc568b47b9e6ace0bd454fc3c12923c6aa638..b5334c1ae6002994027466131ec1f2fe1b417c3a 100644 (file)
@@ -54,6 +54,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
         if self.has_imported_c_functions():
             self.module_temp_cname = env.allocate_temp_pyobject()
             env.release_temp(self.module_temp_cname)
+        self.generate_dep_file(env, result)
         self.generate_c_code(env, options, result)
         self.generate_h_code(env, options, result)
         self.generate_api_code(env, result)
@@ -65,6 +66,20 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
                     return 1
         return 0
     
+    def generate_dep_file(self, env, result):
+        modules = self.referenced_modules
+        if len(modules) > 1 or env.included_files:
+            dep_file = replace_suffix(result.c_file, ".dep")
+            f = open(dep_file, "w")
+            try:
+                for module in modules:
+                    if module is not env:
+                        f.write("cimport %s\n" % module.qualified_name)
+                    for path in module.included_files:
+                        f.write("include %s\n" % path)
+            finally:
+                f.close()
+
     def generate_h_code(self, env, options, result):
         def h_entries(entries, pxd = 0):
             return [entry for entry in entries
index 91f8bae4a853f00e1ce1e3f52b2c22f301986f73..edd4e7b3a8a632d7d78a7923af0cd2bd881bd954 100644 (file)
@@ -1203,8 +1203,10 @@ def p_include_statement(s, level):
     if s.compile_time_eval:
         include_file_path = s.context.find_include_file(include_file_name, pos)
         if include_file_path:
+            s.included_files.append(include_file_name)
             f = Utils.open_source_file(include_file_path, mode="rU")
-            s2 = PyrexScanner(f, include_file_path, s, source_encoding=f.encoding)
+            s2 = PyrexScanner(f, include_file_path, parent_scanner = s,
+                              source_encoding=f.encoding)
             try:
                 tree = p_statement_list(s2, level)
             finally:
index bb338a5eb182819b6cfc0502b4e58dd4db335e6f..39b89433fc701953982109a10dfed90886a2d043 100644 (file)
@@ -206,24 +206,26 @@ def initial_compile_time_env():
 class PyrexScanner(Scanner):
     #  context            Context  Compilation context
     #  type_names         set      Identifiers to be treated as type names
+    #  included_files     [string] Files included with 'include' statement
     #  compile_time_env   dict     Environment for conditional compilation
     #  compile_time_eval  boolean  In a true conditional compilation context
     #  compile_time_expr  boolean  In a compile-time expression context
-    
     resword_dict = build_resword_dict()
 
     def __init__(self, file, filename, parent_scanner = None, 
-            type_names = None, context = None, source_encoding=None):
+                 scope = None, context = None, source_encoding=None):
         Scanner.__init__(self, get_lexicon(), file, filename)
         if parent_scanner:
             self.context = parent_scanner.context
             self.type_names = parent_scanner.type_names
+            self.included_files = parent_scanner.included_files
             self.compile_time_env = parent_scanner.compile_time_env
             self.compile_time_eval = parent_scanner.compile_time_eval
             self.compile_time_expr = parent_scanner.compile_time_expr
         else:
             self.context = context
-            self.type_names = type_names
+            self.type_names = scope.type_names
+            self.included_files = scope.included_files
             self.compile_time_env = initial_compile_time_env()
             self.compile_time_eval = 1
             self.compile_time_expr = 0
index 3a0921e1e7c235798cbd9c9d19f2c0d31f2c6f52..3f8a8f4beb254db309fe56ada81b318e4f2f8295 100644 (file)
@@ -554,10 +554,6 @@ class Scope:
         return [entry for entry in self.temp_entries
             if entry not in self.free_temp_entries]
     
-    #def recycle_pending_temps(self):
-    #  # Obsolete
-    #  pass
-
     def use_utility_code(self, new_code):
         self.global_scope().use_utility_code(new_code)
     
@@ -687,6 +683,7 @@ class ModuleScope(Scope):
     # parent_module        Scope              Parent in the import namespace
     # module_entries       {string : Entry}   For cimport statements
     # type_names           {string : 1}       Set of type names (used during parsing)
+    # included_files       [string]           Cython sources included with 'include'
     # pxd_file_loaded      boolean            Corresponding .pxd file has been processed
     # cimported_modules    [ModuleScope]      Modules imported with cimport
     # new_interned_string_entries [Entry]     New interned strings waiting to be declared
@@ -723,6 +720,7 @@ class ModuleScope(Scope):
         self.interned_objs = []
         self.all_pystring_entries = []
         self.types_imported = {}
+        self.included_files = []
         self.pynum_entries = []
         self.has_extern_class = 0
         self.cached_builtins = []
index de22a6a9da14e3d12ea2e5221c064cd155896898..c7ecf2c469a608ee99306ff23fa539529bc09e33 100644 (file)
@@ -24,7 +24,6 @@ def castrate_file(path, st):
     except EnvironmentError:
         pass
     else:
-        #st = os.stat(path)
         f.seek(0, 0)
         f.truncate()
         f.write(
@@ -33,6 +32,14 @@ def castrate_file(path, st):
         if st:
             os.utime(path, (st.st_atime, st.st_mtime))
 
+def modification_time(path):
+    st = os.stat(path)
+    return st.st_mtime
+
+def file_newer_than(path, time):
+    ftime = modification_time(path)
+    return ftime > time
+
 # support for source file encoding detection and unicode decoding
 
 def encode_filename(filename):