1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module contains the Module class and other classes for resources.
6
7The Module class represents a module in the trace viewer system. A module has
8a name, and may require a variety of other resources, such as stylesheets,
9template objects, raw JavaScript, or other modules.
10
11Other resources include HTML templates, raw JavaScript files, and stylesheets.
12"""
13
14import os
15import inspect
16import codecs
17
18from py_vulcanize import js_utils
19
20
21class DepsException(Exception):
22  """Exceptions related to module dependency resolution."""
23
24  def __init__(self, fmt, *args):
25    from py_vulcanize import style_sheet as style_sheet_module
26    context = []
27    frame = inspect.currentframe()
28    while frame:
29      frame_locals = frame.f_locals
30
31      module_name = None
32      if 'self' in frame_locals:
33        s = frame_locals['self']
34        if isinstance(s, Module):
35          module_name = s.name
36        if isinstance(s, style_sheet_module.StyleSheet):
37          module_name = s.name + '.css'
38      if not module_name:
39        if 'module' in frame_locals:
40          module = frame_locals['module']
41          if isinstance(s, Module):
42            module_name = module.name
43        elif 'm' in frame_locals:
44          module = frame_locals['m']
45          if isinstance(s, Module):
46            module_name = module.name
47
48      if module_name:
49        if len(context):
50          if context[-1] != module_name:
51            context.append(module_name)
52        else:
53          context.append(module_name)
54
55      frame = frame.f_back
56
57    context.reverse()
58    self.context = context
59    context_str = '\n'.join('  %s' % x for x in context)
60    Exception.__init__(
61        self, 'While loading:\n%s\nGot: %s' % (context_str, (fmt % args)))
62
63
64class ModuleDependencyMetadata(object):
65
66  def __init__(self):
67    self.dependent_module_names = []
68    self.dependent_raw_script_relative_paths = []
69    self.style_sheet_names = []
70
71  def AppendMetdata(self, other):
72    self.dependent_module_names += other.dependent_module_names
73    self.dependent_raw_script_relative_paths += \
74        other.dependent_raw_script_relative_paths
75    self.style_sheet_names += other.style_sheet_names
76
77
78_next_module_id = 1
79
80
81class Module(object):
82  """Represents a JavaScript module.
83
84  Interesting properties include:
85    name: Module name, may include a namespace, e.g. 'py_vulcanize.foo'.
86    filename: The filename of the actual module.
87    contents: The text contents of the module.
88    dependent_modules: Other modules that this module depends on.
89
90  In addition to these properties, a Module also contains lists of other
91  resources that it depends on.
92  """
93
94  def __init__(self, loader, name, resource, load_resource=True):
95    assert isinstance(name, basestring), 'Got %s instead' % repr(name)
96
97    global _next_module_id
98    self._id = _next_module_id
99    _next_module_id += 1
100
101    self.loader = loader
102    self.name = name
103    self.resource = resource
104
105    if load_resource:
106      f = codecs.open(self.filename, mode='r', encoding='utf-8')
107      self.contents = f.read()
108      f.close()
109    else:
110      self.contents = None
111
112    # Dependency metadata, set up during Parse().
113    self.dependency_metadata = None
114
115    # Actual dependencies, set up during load().
116    self.dependent_modules = []
117    self.dependent_raw_scripts = []
118    self.style_sheets = []
119
120    # Caches.
121    self._all_dependent_modules_recursive = None
122
123  def __repr__(self):
124    return '%s(%s)' % (self.__class__.__name__, self.name)
125
126  @property
127  def id(self):
128    return self._id
129
130  @property
131  def filename(self):
132    return self.resource.absolute_path
133
134  def IsThirdPartyComponent(self):
135    """Checks whether this module is a third-party Polymer component."""
136    if os.path.join('third_party', 'components') in self.filename:
137      return True
138    if os.path.join('third_party', 'polymer', 'components') in self.filename:
139      return True
140    return False
141
142  def Parse(self):
143    """Parses self.contents and fills in the module's dependency metadata."""
144    raise NotImplementedError()
145
146  def GetTVCMDepsModuleType(self):
147    """Returns the py_vulcanize.setModuleInfo type for this module"""
148    raise NotImplementedError()
149
150  def AppendJSContentsToFile(self,
151                             f,
152                             use_include_tags_for_scripts,
153                             dir_for_include_tag_root):
154    """Appends the js for this module to the provided file."""
155    for dependent_raw_script in self.dependent_raw_scripts:
156      if use_include_tags_for_scripts:
157        rel_filename = os.path.relpath(dependent_raw_script.filename,
158                                       dir_for_include_tag_root)
159        f.write("""<include src="%s">\n""" % rel_filename)
160      else:
161        f.write(js_utils.EscapeJSIfNeeded(dependent_raw_script.contents))
162        f.write('\n')
163
164  def AppendHTMLContentsToFile(self, f, ctl, minify=False):
165    """Appends the HTML for this module [without links] to the provided file."""
166    pass
167
168  def Load(self):
169    """Loads the sub-resources that this module depends on from its dependency
170    metadata.
171
172    Raises:
173      DepsException: There was a problem finding one of the dependencies.
174      Exception: There was a problem parsing a module that this one depends on.
175    """
176    assert self.name, 'Module name must be set before dep resolution.'
177    assert self.filename, 'Module filename must be set before dep resolution.'
178    assert self.name in self.loader.loaded_modules, (
179        'Module must be registered in resource loader before loading.')
180
181    metadata = self.dependency_metadata
182    for name in metadata.dependent_module_names:
183      module = self.loader.LoadModule(module_name=name)
184      self.dependent_modules.append(module)
185
186    for path in metadata.dependent_raw_script_relative_paths:
187      raw_script = self.loader.LoadRawScript(path)
188      self.dependent_raw_scripts.append(raw_script)
189
190    for name in metadata.style_sheet_names:
191      style_sheet = self.loader.LoadStyleSheet(name)
192      self.style_sheets.append(style_sheet)
193
194  @property
195  def all_dependent_modules_recursive(self):
196    if self._all_dependent_modules_recursive:
197      return self._all_dependent_modules_recursive
198
199    self._all_dependent_modules_recursive = set(self.dependent_modules)
200    for dependent_module in self.dependent_modules:
201      self._all_dependent_modules_recursive.update(
202          dependent_module.all_dependent_modules_recursive)
203    return self._all_dependent_modules_recursive
204
205  def ComputeLoadSequenceRecursive(self, load_sequence, already_loaded_set,
206                                   depth=0):
207    """Recursively builds up a load sequence list.
208
209    Args:
210      load_sequence: A list which will be incrementally built up.
211      already_loaded_set: A set of modules that has already been added to the
212          load sequence list.
213      depth: The depth of recursion. If it too deep, that indicates a loop.
214    """
215    if depth > 32:
216      raise Exception('Include loop detected on %s', self.name)
217    for dependent_module in self.dependent_modules:
218      if dependent_module.name in already_loaded_set:
219        continue
220      dependent_module.ComputeLoadSequenceRecursive(
221          load_sequence, already_loaded_set, depth + 1)
222    if self.name not in already_loaded_set:
223      already_loaded_set.add(self.name)
224      load_sequence.append(self)
225
226  def GetAllDependentFilenamesRecursive(self, include_raw_scripts=True):
227    dependent_filenames = []
228
229    visited_modules = set()
230
231    def Get(module):
232      module.AppendDirectlyDependentFilenamesTo(
233          dependent_filenames, include_raw_scripts)
234      visited_modules.add(module)
235      for m in module.dependent_modules:
236        if m in visited_modules:
237          continue
238        Get(m)
239
240    Get(self)
241    return dependent_filenames
242
243  def AppendDirectlyDependentFilenamesTo(
244      self, dependent_filenames, include_raw_scripts=True):
245    dependent_filenames.append(self.resource.absolute_path)
246    if include_raw_scripts:
247      for raw_script in self.dependent_raw_scripts:
248        dependent_filenames.append(raw_script.resource.absolute_path)
249    for style_sheet in self.style_sheets:
250      style_sheet.AppendDirectlyDependentFilenamesTo(dependent_filenames)
251
252
253class RawScript(object):
254  """Represents a raw script resource referenced by a module via the
255  py_vulcanize.requireRawScript(xxx) directive."""
256
257  def __init__(self, resource):
258    self.resource = resource
259
260  @property
261  def filename(self):
262    return self.resource.absolute_path
263
264  @property
265  def contents(self):
266    return self.resource.contents
267
268  def __repr__(self):
269    return 'RawScript(%s)' % self.filename
270