importer.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import web
  2. import re
  3. import os.path
  4. from cache import Cache, to_dir, assert_file_exists
  5. ALIAS_PREFIX = '.'
  6. def separate_imports(content):
  7. """
  8. Separate the import section from the rest of the content of a file. Returns
  9. a tuple with a list of imports, and the remaning part of the string.
  10. Imports are located in the beginning of a file, and can take the following
  11. forms:
  12. # No imports
  13. content
  14. # Single file import
  15. import foo
  16. content
  17. # Multiple files import
  18. import foo, bar
  19. content
  20. # Multiline import
  21. import foo, bar, \
  22. baz
  23. content
  24. """
  25. content = content.lstrip()
  26. imports = []
  27. filename = '[a-zA-Z0-9\._-]+'
  28. comma = re.compile(' *, *(?:\\\\\r?\n *)?')
  29. file_list = '%s(?:%s%s?)*' % (filename, comma.pattern, filename)
  30. m = re.match('^import (%s)(?:(?:\r?\n)+(.*))?$' % file_list, content, re.M)
  31. if m:
  32. imports = comma.split(m.group(1))
  33. content = m.group(2)
  34. if not content:
  35. content = ''
  36. return imports, content
  37. class Importer(Cache):
  38. def __init__(self, files=[], extension=None, **kwargs):
  39. super(Importer, self).__init__(**kwargs)
  40. self.aliases = {}
  41. if extension:
  42. self.extension = '.' + extension
  43. else:
  44. self.extension = ''
  45. for f in files:
  46. self.add(f)
  47. def add(self, path):
  48. path = self.create_full_paths([path])[0]
  49. Cache.add(self, path, absolute=True)
  50. def set_alias(self, alias, path):
  51. """
  52. Add an alias for the path to a file, relative to the importer's root
  53. directory. Aliases are used to shorten the names of imports. E.g. using
  54. "@jquery" in an import could point to "static/js/jquery-min-1.7.1.js".
  55. """
  56. path = self.root + path + self.extension
  57. assert_file_exists(path)
  58. self.aliases[alias] = path
  59. web.debug('Added alias "%s" for path "%s".' % (alias, path))
  60. def create_full_paths(self, files, relative_file=None):
  61. """
  62. Create full paths out of a file list:
  63. 1. Replace any aliases.
  64. 2. Look for direct siblings of the loaded file, if any (relative_file).
  65. 3. Look in the root folder of the Import object.
  66. """
  67. replaced = []
  68. alias_len = len(ALIAS_PREFIX)
  69. if relative_file:
  70. relative_dir = to_dir(relative_file)
  71. for i, path in enumerate(files):
  72. if path[:alias_len] == ALIAS_PREFIX:
  73. # Alias syntax is used, assert that the alias exists
  74. alias = path[alias_len:]
  75. if alias not in self.aliases:
  76. raise ValueError('Alias "%s" has not been set.' % alias)
  77. # Alias exists, translate is to a full path
  78. replaced.append(self.aliases[alias])
  79. else:
  80. if relative_file:
  81. relative_path = relative_dir + path + self.extension
  82. if os.path.exists(relative_path):
  83. # Relative import
  84. replaced.append(relative_path)
  85. continue
  86. # Import from root
  87. path = self.root + path + self.extension
  88. assert_file_exists(path)
  89. replaced.append(path)
  90. return replaced
  91. def concatenate(self, files, relative_path=False):
  92. #if relative_path:
  93. # files = self.create_full_paths(files, relative_path)
  94. map(assert_file_exists, files)
  95. self.loaded = []
  96. self.import_map = {}
  97. concat = ''
  98. for path in files:
  99. f = open(path, 'r')
  100. raw = f.read()
  101. f.close()
  102. # Parse imports from file content
  103. imports, content = separate_imports(raw)
  104. self.import_map.update({path: imports})
  105. # Only add imported files that have not been loaded yet
  106. new_imports = filter(lambda i: i not in self.loaded, imports)
  107. # Prepend imports that have not been loaded yet before the file's
  108. # content
  109. if new_imports:
  110. new_imports = self.create_full_paths(new_imports, path)
  111. # Check if there is a recursive import
  112. for f in new_imports:
  113. if f == path:
  114. raise ImportError('Recursive import in file "%s".' % f)
  115. if f in self.import_map and path in self.import_map[f]:
  116. raise ImportError('Recursive import in of "%s" in ' \
  117. + 'file "%s".' % (f, path))
  118. concat += self.concatenate(new_imports)
  119. #concat += self.concatenate(new_imports, path)
  120. concat += content
  121. self.loaded += path
  122. return concat
  123. def content(self):
  124. return self.concatenate(self.files)