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