import web import re import os.path from cache import Cache, to_dir, assert_file_exists ALIAS_PREFIX = '.' def separate_imports(content): """ Separate the import section from the rest of the content of a file. Returns a tuple with a list of imports, and the remaning part of the string. Imports are located in the beginning of a file, and can take the following forms: # No imports content # Single file import import foo content # Multiple files import import foo, bar content # Multiline import import foo, bar, \ baz content """ content = content.lstrip() imports = [] filename = '[a-zA-Z0-9\._-]+' comma = re.compile(' *, *(?:\\\\\r?\n *)?') file_list = '%s(?:%s%s?)*' % (filename, comma.pattern, filename) m = re.match('^import (%s)(?:(?:\r?\n)+(.*))?$' % file_list, content, re.M) if m: imports = comma.split(m.group(1)) content = m.group(2) if not content: content = '' return imports, content class Importer(Cache): def __init__(self, files=[], extension=None, **kwargs): super(Importer, self).__init__(**kwargs) self.aliases = {} if extension: self.extension = '.' + extension else: self.extension = '' for f in files: self.add(f) def add(self, path): path = self.create_full_paths([path])[0] Cache.add(self, path, absolute=True) def set_alias(self, alias, path): """ Add an alias for the path to a file, relative to the importer's root directory. Aliases are used to shorten the names of imports. E.g. using "@jquery" in an import could point to "static/js/jquery-min-1.7.1.js". """ path = self.root + path + self.extension assert_file_exists(path) self.aliases[alias] = path web.debug('Added alias "%s" for path "%s".' % (alias, path)) def create_full_paths(self, files, relative_file=None): """ Create full paths out of a file list: 1. Replace any aliases. 2. Look for direct siblings of the loaded file, if any (relative_file). 3. Look in the root folder of the Import object. """ replaced = [] alias_len = len(ALIAS_PREFIX) if relative_file: relative_dir = to_dir(relative_file) for i, path in enumerate(files): if path[:alias_len] == ALIAS_PREFIX: # Alias syntax is used, assert that the alias exists alias = path[alias_len:] if alias not in self.aliases: raise ValueError('Alias "%s" has not been set.' % alias) # Alias exists, translate is to a full path replaced.append(self.aliases[alias]) else: if relative_file: relative_path = relative_dir + path + self.extension if os.path.exists(relative_path): # Relative import replaced.append(relative_path) continue # Import from root path = self.root + path + self.extension assert_file_exists(path) replaced.append(path) return replaced def concatenate(self, files, relative_path=False): #if relative_path: # files = self.create_full_paths(files, relative_path) map(assert_file_exists, files) self.loaded = [] self.import_map = {} concat = '' for path in files: f = open(path, 'r') raw = f.read() f.close() # Parse imports from file content imports, content = separate_imports(raw) self.import_map.update({path: imports}) # Only add imported files that have not been loaded yet new_imports = filter(lambda i: i not in self.loaded, imports) # Prepend imports that have not been loaded yet before the file's # content if new_imports: new_imports = self.create_full_paths(new_imports, path) # Check if there is a recursive import for f in new_imports: if f == path: raise ImportError('Recursive import in file "%s".' % f) if f in self.import_map and path in self.import_map[f]: raise ImportError('Recursive import in of "%s" in ' \ + 'file "%s".' % (f, path)) concat += self.concatenate(new_imports) #concat += self.concatenate(new_imports, path) concat += content self.loaded += path return concat def content(self): return self.concatenate(self.files)