importer.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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. comma = re.compile(' *, *(?:\\\\\r?\n *)?')
  28. file_list = '\w+(?:%s\w+?)*' % comma.pattern
  29. m = re.match('^import (%s)(?:(?:\r?\n)+(.*))?$' % file_list, content, re.M)
  30. if m:
  31. imports = comma.split(m.group(1))
  32. content = m.group(2)
  33. if not content:
  34. content = ''
  35. return imports, content
  36. class Importer(Cache):
  37. def __init__(self, root, extension=None):
  38. Cache.__init__(self)
  39. self.root = to_dir(root + '/')
  40. self.aliases = {}
  41. if extension:
  42. self.extension = '.' + extension
  43. else:
  44. self.extension = ''
  45. def set_alias(self, name, path):
  46. """
  47. Add an alias for the path to a file, relative to the importer's root
  48. directory. Aliases are used to shorten the names of imports. E.g. using
  49. "@jquery" in an import could point to "static/js/jquery-min-1.7.1.js".
  50. """
  51. path = self.root + path + self.extension
  52. assert_file_exists(path)
  53. self.aliases[name] = path
  54. def create_full_paths(self, files, relative_file=None):
  55. """
  56. Create full paths out of a file list:
  57. 1. Replace any aliases.
  58. 2. Look for direct siblings of the loaded file, if any (relative_file).
  59. 3. Look in the root folder of the Import object.
  60. """
  61. replaced = []
  62. alias_len = len(ALIAS_PREFIX)
  63. if relative_file:
  64. relative_dir = to_dir(relative_file)
  65. for i, path in enumerate(files):
  66. if path[:alias_len] == ALIAS_PREFIX:
  67. # Alias syntax is used, assert that the alias exists
  68. alias = path[alias_len:]
  69. if alias not in self.aliases:
  70. raise ValueError('Alias "%s" has not been set.' % alias)
  71. # Alias exists, translate is to a full path
  72. replaced.append(self.aliases[alias])
  73. else:
  74. if relative_file:
  75. relative_path = relative_dir + path + self.extension
  76. if os.path.exists(relative_path):
  77. # Relative import
  78. replaced.append(relative_path)
  79. continue
  80. # Import from root
  81. path = self.root + path + self.extension
  82. assert_file_exists(path)
  83. replaced.append(path)
  84. return replaced
  85. def concatenate(self, files):
  86. files = self.create_full_paths(files)
  87. map(assert_file_exists, files)
  88. self.loaded = []
  89. self.import_map = {}
  90. concat = ''
  91. for path in files:
  92. f = open(path, 'r')
  93. raw = f.read()
  94. f.close()
  95. # Parse imports from file content
  96. imports, content = separate_imports(raw)
  97. self.import_map.update({path: imports})
  98. # Only add imported files that have not been loaded yet
  99. new_imports = filter(lambda i: i not in self.loaded, imports)
  100. # Prepend imports that have not been loaded yet before the file's
  101. # content
  102. if new_imports:
  103. new_imports = self.create_full_paths(new_imports, path)
  104. # Check if there is a recursive import
  105. for f in new_imports:
  106. if f == path:
  107. raise ImportError('Recursive import in file "%s".' % f)
  108. if f in self.import_map and path in self.import_map[f]:
  109. raise ImportError('Recursive import in of "%s" in ' \
  110. + 'file "%s".' % (f, path))
  111. concat += self.concatenate(new_imports)
  112. concat += content
  113. self.loaded += path
  114. return concat
  115. def content(self):
  116. return self.concatenate(self.files)