bundle.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. from contextlib import contextmanager
  2. import os
  3. from os import path
  4. from webassets import six
  5. from webassets.six.moves import map
  6. from webassets.six.moves import zip
  7. from .filter import get_filter
  8. from .merge import (FileHunk, UrlHunk, FilterTool, merge, merge_filters,
  9. select_filters, MoreThanOneFilterError, NoFilters)
  10. from .updater import SKIP_CACHE
  11. from .exceptions import BundleError, BuildError
  12. from .utils import cmp_debug_levels, hash_func
  13. from .env import ConfigurationContext, DictConfigStorage, BaseEnvironment
  14. from .utils import is_url
  15. __all__ = ('Bundle', 'get_all_bundle_files',)
  16. def has_placeholder(s):
  17. return '%(version)s' in s
  18. class ContextWrapper(object):
  19. """Implements a hierarchy-aware configuration context.
  20. Since each bundle can provide settings that augment the values of
  21. the parent bundle, and ultimately the environment, as the bundle
  22. hierarchy is processed, this class is used to provide an interface
  23. that searches through the hierarchy of settings. It's what you get
  24. when you are given a ``ctx`` value.
  25. """
  26. def __init__(self, parent, overwrites=None):
  27. self._parent, self._overwrites = parent, overwrites
  28. def __getitem__(self, key):
  29. try:
  30. if self._overwrites is None:
  31. raise KeyError()
  32. return self._overwrites.config[key]
  33. except KeyError:
  34. return self._parent.config.get(key)
  35. def __getattr__(self, item):
  36. try:
  37. return self.getattr(self._overwrites, item)
  38. except (KeyError, AttributeError, EnvironmentError):
  39. return self.getattr(self._parent, item)
  40. def getattr(self, object, item):
  41. # Helper because Bundles are special in that the config attributes
  42. # are in bundle.config (bundle.config.url vs env.url or ctx.url).
  43. if isinstance(object, Bundle):
  44. return getattr(object.config, item)
  45. else:
  46. return getattr(object, item)
  47. def get(self, key, default=None):
  48. try:
  49. return self.__getitem__(key)
  50. except KeyError:
  51. return default
  52. @property
  53. def environment(self):
  54. """Find the root environment context."""
  55. if isinstance(self._parent, BaseEnvironment):
  56. return self._parent
  57. return self._parent.environment
  58. def wrap(parent, overwrites):
  59. """Return a context object where the values from ``overwrites``
  60. augment the ``parent`` configuration. See :class:`ContextWrapper`.
  61. """
  62. return ContextWrapper(parent, overwrites)
  63. class BundleConfig(DictConfigStorage, ConfigurationContext):
  64. """A configuration dict that also supports Environment-like attribute
  65. access, i.e. ``config['resolver']`` and ``config.resolver``.
  66. """
  67. def __init__(self, bundle):
  68. DictConfigStorage.__init__(self, bundle)
  69. ConfigurationContext.__init__(self, self)
  70. class Bundle(object):
  71. """A bundle is the unit webassets uses to organize groups of media files,
  72. which filters to apply and where to store them.
  73. Bundles can be nested arbitrarily.
  74. A note on the connection between a bundle and an "environment" instance:
  75. The bundle requires a environment that it belongs to. Without an
  76. environment, it lacks information about how to behave, and cannot know
  77. where relative paths are actually based. However, I don't want to make the
  78. ``Bundle.__init__`` syntax more complicated than it already is by requiring
  79. an Environment object to be passed. This would be a particular nuisance
  80. when nested bundles are used. Further, nested bundles are never explicitly
  81. connected to an Environment, and what's more, the same child bundle can be
  82. used in multiple parent bundles.
  83. This is the reason why basically every method of the Bundle class takes an
  84. ``env`` parameter - so a parent bundle can provide the environment for
  85. child bundles that do not know it.
  86. """
  87. def __init__(self, *contents, **options):
  88. self._env = options.pop('env', None)
  89. self.contents = contents
  90. self.output = options.pop('output', None)
  91. self.filters = options.pop('filters', None)
  92. self.depends = options.pop('depends', [])
  93. self.version = options.pop('version', [])
  94. self.remove_duplicates = options.pop('remove_duplicates', True)
  95. self.extra = options.pop('extra', {})
  96. self._config = BundleConfig(self)
  97. self._config.update(options.pop('config', {}))
  98. if 'debug' in options:
  99. debug = options.pop('debug')
  100. if debug is not None:
  101. self._config['debug'] = debug
  102. if options:
  103. raise TypeError("got unexpected keyword argument '%s'" %
  104. list(options.keys())[0])
  105. def __repr__(self):
  106. return "<%s output=%s, filters=%s, contents=%s>" % (
  107. self.__class__.__name__,
  108. self.output,
  109. self.filters,
  110. self.contents,
  111. )
  112. @property
  113. def config(self):
  114. # This is a property so that user are not tempted to assign
  115. # a custom dictionary which won't uphold our caseless semantics.
  116. return self._config
  117. def _get_debug(self):
  118. return self.config.get('debug', None)
  119. def _set_debug(self, value):
  120. self.config['debug'] = value
  121. debug = property(_get_debug, _set_debug)
  122. def _get_filters(self):
  123. return self._filters
  124. def _set_filters(self, value):
  125. """Filters may be specified in a variety of different ways, including
  126. by giving their name; we need to make sure we resolve everything to an
  127. actual filter instance.
  128. """
  129. if value is None:
  130. self._filters = ()
  131. return
  132. if isinstance(value, six.string_types):
  133. # 333: Simplify w/o condition?
  134. if six.PY3:
  135. filters = map(str.strip, value.split(','))
  136. else:
  137. filters = map(unicode.strip, unicode(value).split(','))
  138. elif isinstance(value, (list, tuple)):
  139. filters = value
  140. else:
  141. filters = [value]
  142. self._filters = [get_filter(f) for f in filters]
  143. filters = property(_get_filters, _set_filters)
  144. def _get_contents(self):
  145. return self._contents
  146. def _set_contents(self, value):
  147. self._contents = value
  148. self._resolved_contents = None
  149. contents = property(_get_contents, _set_contents)
  150. def _get_extra(self):
  151. if not self._extra and not has_files(self):
  152. # If this bundle has no extra values of it's own, and only
  153. # wraps child bundles, use the extra values of those.
  154. result = {}
  155. for bundle in self.contents:
  156. if bundle.extra is not None:
  157. result.update(bundle.extra)
  158. return result
  159. else:
  160. return self._extra
  161. def _set_extra(self, value):
  162. self._extra = value
  163. extra = property(_get_extra, _set_extra, doc="""A custom user dict of
  164. extra values attached to this bundle. Those will be available in
  165. template tags, and can be used to attach things like a CSS
  166. 'media' value.""")
  167. def resolve_contents(self, ctx=None, force=False):
  168. """Return an actual list of source files.
  169. What the user specifies as the bundle contents cannot be
  170. processed directly. There may be glob patterns of course. We
  171. may need to search the load path. It's common for third party
  172. extensions to provide support for referencing assets spread
  173. across multiple directories.
  174. This passes everything through :class:`Environment.resolver`,
  175. through which this process can be customized.
  176. At this point, we also validate source paths to complain about
  177. missing files early.
  178. The return value is a list of 2-tuples ``(original_item,
  179. abspath)``. In the case of urls and nested bundles both tuple
  180. values are the same.
  181. Set ``force`` to ignore any cache, and always re-resolve
  182. glob patterns.
  183. """
  184. if not ctx:
  185. ctx = wrap(self.env, self)
  186. # TODO: We cache the values, which in theory is problematic, since
  187. # due to changes in the env object, the result of the globbing may
  188. # change. Not to mention that a different env object may be passed
  189. # in. We should find a fix for this.
  190. if getattr(self, '_resolved_contents', None) is None or force:
  191. resolved = []
  192. for item in self.contents:
  193. try:
  194. result = ctx.resolver.resolve_source(ctx, item)
  195. except IOError as e:
  196. raise BundleError(e)
  197. if not isinstance(result, list):
  198. result = [result]
  199. # Exclude the output file.
  200. # TODO: This will not work for nested bundle contents. If it
  201. # doesn't work properly anyway, should be do it in the first
  202. # place? If there are multiple versions, it will fail as well.
  203. # TODO: There is also the question whether we can/should
  204. # exclude glob duplicates.
  205. if self.output:
  206. try:
  207. result.remove(self.resolve_output(ctx))
  208. except (ValueError, BundleError):
  209. pass
  210. resolved.extend(map(lambda r: (item, r), result))
  211. # Exclude duplicate files from the bundle.
  212. # This will only keep the first occurrence of a file in the bundle.
  213. if self.remove_duplicates:
  214. resolved = self._filter_duplicates(resolved)
  215. self._resolved_contents = resolved
  216. return self._resolved_contents
  217. @staticmethod
  218. def _filter_duplicates(resolved):
  219. # Keep track of the resolved filenames that have been seen, and only
  220. # add it the first time it is encountered.
  221. seen_files = set()
  222. result = []
  223. for item, r in resolved:
  224. if r not in seen_files:
  225. seen_files.add(r)
  226. result.append((item, r))
  227. return result
  228. def _get_depends(self):
  229. return self._depends
  230. def _set_depends(self, value):
  231. self._depends = [value] if isinstance(value, six.string_types) else value
  232. self._resolved_depends = None
  233. depends = property(_get_depends, _set_depends, doc=
  234. """Allows you to define an additional set of files (glob syntax
  235. is supported), which are considered when determining whether a
  236. rebuild is required.
  237. """)
  238. def resolve_depends(self, ctx):
  239. # TODO: Caching is as problematic here as it is in resolve_contents().
  240. if not self.depends:
  241. return []
  242. if getattr(self, '_resolved_depends', None) is None:
  243. resolved = []
  244. for item in self.depends:
  245. try:
  246. result = ctx.resolver.resolve_source(ctx, item)
  247. except IOError as e:
  248. raise BundleError(e)
  249. if not isinstance(result, list):
  250. result = [result]
  251. resolved.extend(result)
  252. self._resolved_depends = resolved
  253. return self._resolved_depends
  254. def get_version(self, ctx=None, refresh=False):
  255. """Return the current version of the Bundle.
  256. If the version is not cached in memory, it will first look in the
  257. manifest, then ask the versioner.
  258. ``refresh`` causes a value in memory to be ignored, and the version
  259. to be looked up anew.
  260. """
  261. if not ctx:
  262. ctx = wrap(self.env, self)
  263. if not self.version or refresh:
  264. version = None
  265. # First, try a manifest. This should be the fastest way.
  266. if ctx.manifest:
  267. version = ctx.manifest.query(self, ctx)
  268. # Often the versioner is able to help.
  269. if not version:
  270. from .version import VersionIndeterminableError
  271. if ctx.versions:
  272. try:
  273. version = ctx.versions.determine_version(self, ctx)
  274. assert version
  275. except VersionIndeterminableError as e:
  276. reason = e
  277. else:
  278. reason = '"versions" option not set'
  279. if not version:
  280. raise BundleError((
  281. 'Cannot find version of %s. There is no manifest '
  282. 'which knows the version, and it cannot be '
  283. 'determined dynamically, because: %s') % (self, reason))
  284. self.version = version
  285. return self.version
  286. def resolve_output(self, ctx=None, version=None):
  287. """Return the full, absolute output path.
  288. If a %(version)s placeholder is used, it is replaced.
  289. """
  290. if not ctx:
  291. ctx = wrap(self.env, self)
  292. output = ctx.resolver.resolve_output_to_path(ctx, self.output, self)
  293. if has_placeholder(output):
  294. output = output % {'version': version or self.get_version(ctx)}
  295. return output
  296. def id(self):
  297. """This is used to determine when a bundle definition has changed so
  298. that a rebuild is required.
  299. The hash therefore should be built upon data that actually affect the
  300. final build result.
  301. """
  302. return hash_func((tuple(self.contents),
  303. self.output,
  304. tuple(self.filters),
  305. bool(self.debug)))
  306. # Note how self.depends is not included here. It could be, but we
  307. # really want this hash to only change for stuff that affects the
  308. # actual output bytes. Note that modifying depends will be effective
  309. # after the first rebuild in any case.
  310. @property
  311. def is_container(self):
  312. """Return true if this is a container bundle, that is, a bundle that
  313. acts only as a container for a number of sub-bundles.
  314. It must not contain any files of its own, and must have an empty
  315. ``output`` attribute.
  316. """
  317. return not has_files(self) and not self.output
  318. @contextmanager
  319. def bind(self, env):
  320. old_env = self._env
  321. self._env = env
  322. try:
  323. yield
  324. finally:
  325. self._env = old_env
  326. def _get_env(self):
  327. if self._env is None:
  328. raise BundleError('Bundle is not connected to an environment')
  329. return self._env
  330. def _set_env(self, env):
  331. self._env = env
  332. env = property(_get_env, _set_env)
  333. def _merge_and_apply(self, ctx, output, force, parent_debug=None,
  334. parent_filters=None, extra_filters=None,
  335. disable_cache=None):
  336. """Internal recursive build method.
  337. ``parent_debug`` is the debug setting used by the parent bundle. This
  338. is not necessarily ``bundle.debug``, but rather what the calling method
  339. in the recursion tree is actually using.
  340. ``parent_filters`` are what the parent passes along, for us to be
  341. applied as input filters. Like ``parent_debug``, it is a collection of
  342. the filters of all parents in the hierarchy.
  343. ``extra_filters`` may exist if the parent is a container bundle passing
  344. filters along to its children; these are applied as input and output
  345. filters (since there is no parent who could do the latter), and they
  346. are not passed further down the hierarchy (but instead they become part
  347. of ``parent_filters``.
  348. ``disable_cache`` is necessary because in some cases, when an external
  349. bundle dependency has changed, we must not rely on the cache, since the
  350. cache key is not taking into account changes in those dependencies
  351. (for now).
  352. """
  353. parent_filters = parent_filters or []
  354. extra_filters = extra_filters or []
  355. # Determine the debug level to use. It determines if and which filters
  356. # should be applied.
  357. #
  358. # The debug level is inherited (if the parent bundle is merging, a
  359. # child bundle clearly cannot act in full debug=True mode). Bundles
  360. # may define a custom ``debug`` attributes, but child bundles may only
  361. # ever lower it, not increase it.
  362. #
  363. # If not parent_debug is given (top level), use the Environment value.
  364. parent_debug = parent_debug if parent_debug is not None else ctx.debug
  365. # Consider bundle's debug attribute and other things.
  366. current_debug_level = _effective_debug_level(
  367. ctx, self, extra_filters, default=parent_debug)
  368. # Special case: If we end up with ``True``, assume ``False`` instead.
  369. # The alternative would be for the build() method to refuse to work at
  370. # this point, which seems unnecessarily inconvenient (Instead how it
  371. # works is that urls() simply doesn't call build() when debugging).
  372. # Note: This can only happen if the Environment sets debug=True and
  373. # nothing else overrides it.
  374. if current_debug_level is True:
  375. current_debug_level = False
  376. # Put together a list of filters that we would want to run here.
  377. # These will be the bundle's filters, and any extra filters given
  378. # to use if the parent is a container bundle. Note we do not yet
  379. # include input/open filters pushed down by a parent build iteration.
  380. filters = merge_filters(self.filters, extra_filters)
  381. # Initialize the filters. This happens before we choose which of
  382. # them should actually run, so that Filter.setup() can influence
  383. # this choice.
  384. for filter in filters:
  385. filter.set_context(ctx)
  386. # Since we call this now every single time before the filter
  387. # is used, we might pass the bundle instance it is going
  388. # to be used with. For backwards-compatibility reasons, this
  389. # is problematic. However, by inspecting the support arguments,
  390. # we can deal with it. We probably then want to deprecate
  391. # the old syntax before 1.0 (TODO).
  392. filter.setup()
  393. # Given the debug level, determine which of the filters want to run
  394. selected_filters = select_filters(filters, current_debug_level)
  395. # We construct two lists of filters. The ones we want to use in this
  396. # iteration, and the ones we want to pass down to child bundles.
  397. # Why? Say we are in merge mode. Assume an "input()" filter which does
  398. # not run in merge mode, and a child bundle that switches to
  399. # debug=False. The child bundle then DOES want to run those input
  400. # filters, so we do need to pass them.
  401. filters_to_run = merge_filters(
  402. selected_filters, select_filters(parent_filters, current_debug_level))
  403. filters_to_pass_down = merge_filters(filters, parent_filters)
  404. # Prepare contents
  405. resolved_contents = self.resolve_contents(ctx, force=True)
  406. # Unless we have been told by our caller to use or not use the cache
  407. # for this, try to decide for ourselves. The issue here is that when a
  408. # bundle has dependencies, like a sass file with includes otherwise not
  409. # listed in the bundle sources, a change in such an external include
  410. # would not influence the cache key, thus the use of the cache causing
  411. # such a change to be ignored. For now, we simply do not use the cache
  412. # for any bundle with dependencies. Another option would be to read
  413. # the contents of all files declared via "depends", and use them as a
  414. # cache key modifier. For now I am worried about the performance impact.
  415. #
  416. # Note: This decision only affects the current bundle instance. Even if
  417. # dependencies cause us to ignore the cache for this bundle instance,
  418. # child bundles may still use it!
  419. actually_skip_cache_here = disable_cache or bool(self.resolve_depends(ctx))
  420. filtertool = FilterTool(
  421. ctx.cache, no_cache_read=actually_skip_cache_here,
  422. kwargs={'output': output[0],
  423. 'output_path': output[1]})
  424. # Apply input()/open() filters to all the contents.
  425. hunks = []
  426. for item, cnt in resolved_contents:
  427. if isinstance(cnt, Bundle):
  428. # Recursively process nested bundles.
  429. hunk = cnt._merge_and_apply(
  430. wrap(ctx, cnt), output, force, current_debug_level,
  431. filters_to_pass_down, disable_cache=disable_cache)
  432. if hunk is not None:
  433. hunks.append((hunk, {}))
  434. else:
  435. # Give a filter the chance to open his file.
  436. try:
  437. hunk = filtertool.apply_func(
  438. filters_to_run, 'open', [cnt],
  439. # Also pass along the original relative path, as
  440. # specified by the user, before resolving.
  441. kwargs={'source': item},
  442. # We still need to open the file ourselves too and use
  443. # it's content as part of the cache key, otherwise this
  444. # filter application would only be cached by filename,
  445. # and changes in the source not detected. The other
  446. # option is to not use the cache at all here. Both have
  447. # different performance implications, but I'm guessing
  448. # that reading and hashing some files unnecessarily
  449. # very often is better than running filters
  450. # unnecessarily occasionally.
  451. cache_key=[FileHunk(cnt)] if not is_url(cnt) else [])
  452. except MoreThanOneFilterError as e:
  453. raise BuildError(e)
  454. except NoFilters:
  455. # Open the file ourselves.
  456. if is_url(cnt):
  457. hunk = UrlHunk(cnt, env=ctx)
  458. else:
  459. hunk = FileHunk(cnt)
  460. # With the hunk, remember both the original relative
  461. # path, as specified by the user, and the one that has
  462. # been resolved to a filesystem location. We'll pass
  463. # them along to various filter steps.
  464. item_data = {'source': item, 'source_path': cnt}
  465. # Run input filters, unless open() told us not to.
  466. hunk = filtertool.apply(hunk, filters_to_run, 'input',
  467. kwargs=item_data)
  468. hunks.append((hunk, item_data))
  469. # If this bundle is empty (if it has nested bundles, they did
  470. # not yield any hunks either), return None to indicate so.
  471. if len(hunks) == 0:
  472. return None
  473. # Merge the individual files together. There is an optional hook for
  474. # a filter here, by implementing a concat() method.
  475. try:
  476. try:
  477. final = filtertool.apply_func(filters_to_run, 'concat', [hunks])
  478. except MoreThanOneFilterError as e:
  479. raise BuildError(e)
  480. except NoFilters:
  481. final = merge([h for h, _ in hunks])
  482. except IOError as e:
  483. # IOErrors can be raised here if hunks are loaded for the
  484. # first time. TODO: IOErrors can also be raised when
  485. # a file is read during the filter-apply phase, but we don't
  486. # convert it to a BuildError there...
  487. raise BuildError(e)
  488. # Apply output filters.
  489. # TODO: So far, all the situations where bundle dependencies are
  490. # used/useful, are based on input filters having those dependencies. Is
  491. # it even required to consider them here with respect to the cache? We
  492. # might be able to run this operation with the cache on (the FilterTool
  493. # being possibly configured with cache reads off).
  494. return filtertool.apply(final, selected_filters, 'output')
  495. def _build(self, ctx, extra_filters=None, force=None, output=None,
  496. disable_cache=None):
  497. """Internal bundle build function.
  498. This actually tries to build this very bundle instance, as opposed to
  499. the public-facing ``build()``, which first deals with the possibility
  500. that we are a container bundle, i.e. having no files of our own.
  501. First checks whether an update for this bundle is required, via the
  502. configured ``updater`` (which is almost always the timestamp-based one).
  503. Unless ``force`` is given, in which case the bundle will always be
  504. built, without considering timestamps.
  505. A ``FileHunk`` will be returned, or in a certain case, with no updater
  506. defined and force=False, the return value may be ``False``.
  507. TODO: Support locking. When called from inside a template tag, this
  508. should lock, so that multiple requests don't all start to build. When
  509. called from the command line, there is no need to lock.
  510. """
  511. extra_filters = extra_filters or []
  512. if not self.output:
  513. raise BuildError('No output target found for %s' % self)
  514. # Determine if we really need to build, or if the output file
  515. # already exists and nothing has changed.
  516. if force:
  517. update_needed = True
  518. elif not has_placeholder(self.output) and \
  519. not path.exists(self.resolve_output(ctx, self.output)):
  520. update_needed = True
  521. else:
  522. update_needed = ctx.updater.needs_rebuild(self, ctx) \
  523. if ctx.updater else True
  524. if update_needed==SKIP_CACHE:
  525. disable_cache = True
  526. if not update_needed:
  527. # We can simply return the existing output file
  528. return FileHunk(self.resolve_output(ctx, self.output))
  529. hunk = self._merge_and_apply(
  530. ctx, [self.output, self.resolve_output(ctx, version='?')],
  531. force, disable_cache=disable_cache, extra_filters=extra_filters)
  532. if hunk is None:
  533. raise BuildError('Nothing to build for %s, is empty' % self)
  534. if output:
  535. # If we are given a stream, just write to it.
  536. output.write(hunk.data())
  537. else:
  538. if has_placeholder(self.output) and not ctx.versions:
  539. raise BuildError((
  540. 'You have not set the "versions" option, but %s '
  541. 'uses a version placeholder in the output target'
  542. % self))
  543. version = None
  544. if ctx.versions:
  545. version = ctx.versions.determine_version(self, ctx, hunk)
  546. output_filename = self.resolve_output(ctx, version=version)
  547. # If it doesn't exist yet, create the target directory.
  548. output_dir = path.dirname(output_filename)
  549. if not path.exists(output_dir):
  550. os.makedirs(output_dir)
  551. hunk.save(output_filename)
  552. self.version = version
  553. if ctx.manifest:
  554. ctx.manifest.remember(self, ctx, version)
  555. if ctx.versions and version:
  556. # Hook for the versioner (for example set the timestamp of
  557. # the file) to the actual version.
  558. ctx.versions.set_version(self, ctx, output_filename, version)
  559. # The updater may need to know this bundle exists and how it
  560. # has been last built, in order to detect changes in the
  561. # bundle definition, like new source files.
  562. if ctx.updater:
  563. ctx.updater.build_done(self, ctx)
  564. return hunk
  565. def build(self, force=None, output=None, disable_cache=None):
  566. """Build this bundle, meaning create the file given by the ``output``
  567. attribute, applying the configured filters etc.
  568. If the bundle is a container bundle, then multiple files will be built.
  569. Unless ``force`` is given, the configured ``updater`` will be used to
  570. check whether a build is even necessary.
  571. If ``output`` is a file object, the result will be written to it rather
  572. than to the filesystem.
  573. The return value is a list of ``FileHunk`` objects, one for each bundle
  574. that was built.
  575. """
  576. ctx = wrap(self.env, self)
  577. hunks = []
  578. for bundle, extra_filters, new_ctx in self.iterbuild(ctx):
  579. hunks.append(bundle._build(
  580. new_ctx, extra_filters, force=force, output=output,
  581. disable_cache=disable_cache))
  582. return hunks
  583. def iterbuild(self, ctx):
  584. """Iterate over the bundles which actually need to be built.
  585. This will often only entail ``self``, though for container bundles
  586. (and container bundle hierarchies), a list of all the non-container
  587. leafs will be yielded.
  588. Essentially, what this does is "skip" bundles which do not need to be
  589. built on their own (container bundles), and gives the caller the child
  590. bundles instead.
  591. The return values are 3-tuples of (bundle, filter_list, new_ctx), with
  592. the second item being a list of filters that the parent "container
  593. bundles" this method is processing are passing down to the children.
  594. """
  595. if self.is_container:
  596. for bundle, _ in self.resolve_contents(ctx):
  597. if bundle.is_container:
  598. for child, child_filters, new_ctx in \
  599. bundle.iterbuild(wrap(ctx, bundle)):
  600. yield (
  601. child,
  602. merge_filters(child_filters, self.filters),
  603. new_ctx)
  604. else:
  605. yield bundle, self.filters, wrap(ctx, bundle)
  606. else:
  607. yield self, [], ctx
  608. def _make_output_url(self, ctx):
  609. """Return the output url, modified for expire header handling.
  610. """
  611. # Only query the version if we need to for performance
  612. version = None
  613. if has_placeholder(self.output) or ctx.url_expire != False:
  614. # If auto-build is enabled, we must not use a cached version
  615. # value, or we might serve old versions.
  616. version = self.get_version(ctx, refresh=ctx.auto_build)
  617. url = self.output
  618. if has_placeholder(url):
  619. url = url % {'version': version}
  620. url = ctx.resolver.resolve_output_to_url(ctx, url)
  621. if ctx.url_expire or (
  622. ctx.url_expire is None and not has_placeholder(self.output)):
  623. url = "%s?%s" % (url, version)
  624. return url
  625. def _urls(self, ctx, extra_filters, *args, **kwargs):
  626. """Return a list of urls for this bundle, and all subbundles,
  627. and, when it becomes necessary, start a build process.
  628. """
  629. # Look at the debug value to see if this bundle should return the
  630. # source urls (in debug mode), or a single url of the bundle in built
  631. # form. Once a bundle needs to be built, all of it's child bundles
  632. # are built as well of course, so at this point we leave the urls()
  633. # recursion and start a build() recursion.
  634. debug = _effective_debug_level(ctx, self, extra_filters)
  635. if debug == 'merge':
  636. supposed_to_merge = True
  637. elif debug is True:
  638. supposed_to_merge = False
  639. elif debug is False:
  640. supposed_to_merge = True
  641. else:
  642. raise BundleError('Invalid debug value: %s' % debug)
  643. # We will output a single url for this bundle unless a) the
  644. # configuration tells us to output the source urls
  645. # ("supposed_to_merge"), or b) this bundle isn't actually configured to
  646. # be built, that is, has no filters and no output target.
  647. if supposed_to_merge and (self.filters or self.output):
  648. # With ``auto_build``, build the bundle to make sure the output is
  649. # up to date; otherwise, we just assume the file already exists.
  650. # (not wasting any IO ops)
  651. if ctx.auto_build:
  652. self._build(ctx, extra_filters=extra_filters, force=False,
  653. *args, **kwargs)
  654. return [self._make_output_url(ctx)]
  655. else:
  656. # We either have no files (nothing to build), or we are
  657. # in debug mode: Instead of building the bundle, we
  658. # source all contents instead.
  659. urls = []
  660. for org, cnt in self.resolve_contents(ctx):
  661. if isinstance(cnt, Bundle):
  662. urls.extend(org._urls(
  663. wrap(ctx, cnt),
  664. merge_filters(extra_filters, self.filters),
  665. *args, **kwargs))
  666. elif is_url(cnt):
  667. urls.append(cnt)
  668. else:
  669. try:
  670. url = ctx.resolver.resolve_source_to_url(ctx, cnt, org)
  671. except ValueError:
  672. # If we cannot generate a url to a path outside the
  673. # media directory. So if that happens, we copy the
  674. # file into the media directory.
  675. external = pull_external(ctx, cnt)
  676. url = ctx.resolver.resolve_source_to_url(ctx, external, org)
  677. urls.append(url)
  678. return urls
  679. def urls(self, *args, **kwargs):
  680. """Return a list of urls for this bundle.
  681. Depending on the environment and given options, this may be a single
  682. url (likely the case in production mode), or many urls (when we source
  683. the original media files in DEBUG mode).
  684. Insofar necessary, this will automatically create or update the files
  685. behind these urls.
  686. """
  687. ctx = wrap(self.env, self)
  688. urls = []
  689. for bundle, extra_filters, new_ctx in self.iterbuild(ctx):
  690. urls.extend(bundle._urls(new_ctx, extra_filters, *args, **kwargs))
  691. return urls
  692. def pull_external(ctx, filename):
  693. """Helper which will pull ``filename`` into
  694. :attr:`Environment.directory`, for the purposes of being able to
  695. generate a url for it.
  696. """
  697. # Generate the target filename. Use a hash to keep it unique and short,
  698. # but attach the base filename for readability.
  699. # The bit-shifting rids us of ugly leading - characters.
  700. hashed_filename = hash_func(filename)
  701. rel_path = path.join('webassets-external',
  702. "%s_%s" % (hashed_filename, path.basename(filename)))
  703. full_path = path.join(ctx.directory, rel_path)
  704. # Copy the file if necessary
  705. if path.isfile(full_path):
  706. gs = lambda p: os.stat(p).st_mtime
  707. if gs(full_path) > gs(filename):
  708. return full_path
  709. directory = path.dirname(full_path)
  710. if not path.exists(directory):
  711. os.makedirs(directory)
  712. FileHunk(filename).save(full_path)
  713. return full_path
  714. def get_all_bundle_files(bundle, ctx=None):
  715. """Return a flattened list of all source files of the given bundle, all
  716. its dependencies, recursively for all nested bundles.
  717. Making this a helper function rather than a part of the official
  718. Bundle feels right.
  719. """
  720. if not ctx:
  721. ctx = wrap(bundle.env, bundle)
  722. if not isinstance(ctx, ContextWrapper):
  723. ctx = ContextWrapper(ctx)
  724. files = []
  725. for _, c in bundle.resolve_contents(ctx):
  726. if isinstance(c, Bundle):
  727. files.extend(get_all_bundle_files(c, wrap(ctx, c)))
  728. elif not is_url(c):
  729. files.append(c)
  730. files.extend(bundle.resolve_depends(ctx))
  731. return files
  732. def _effective_debug_level(ctx, bundle, extra_filters=None, default=None):
  733. """This is a helper used both in the urls() and the build() recursions.
  734. It returns the debug level that this bundle, in a tree structure
  735. of bundles, should use. It looks at any bundle-specific ``debug``
  736. attribute, considers an automatic upgrade to "merge" due to filters that
  737. are present, and will finally use the value in the ``default`` argument,
  738. which in turn defaults to ``env.debug``.
  739. It also ensures our rule that in a bundle hierarchy, the debug level may
  740. only ever be lowered. Nested bundle may lower the level from ``True`` to
  741. ``"merge"`` to ``False``, but never in the other direction. Which makes
  742. sense: If a bundle is already being merged, we cannot start exposing the
  743. source urls a child bundle, not if the correct order should be maintained.
  744. And while in theory it would seem possible to switch between full-out
  745. production (debug=False) and ``"merge"``, the complexity there, in
  746. particular with view as to how certain filter types like input() and
  747. open() need to be applied to child bundles, is just not worth it.
  748. """
  749. if default is None:
  750. default = ctx.environment.debug
  751. if bundle.config.get('debug') is not None:
  752. level = bundle.config.debug
  753. else:
  754. # If bundle doesn't force a level, then the presence of filters which
  755. # declare they should always run puts the bundle automatically in
  756. # merge mode.
  757. filters = merge_filters(bundle.filters, extra_filters)
  758. level = 'merge' if select_filters(filters, True) else None
  759. if level is not None:
  760. # The new level must be lower than the older one. We do not thrown an
  761. # error if this is NOT the case, but silently ignore it. This is so
  762. # that a debug=True can be used to overwrite auto_debug_upgrade.
  763. # Otherwise debug=True would always fail.
  764. if cmp_debug_levels(default, level) > 0:
  765. return level
  766. return default
  767. has_files = lambda bundle: \
  768. any([c for c in bundle.contents if not isinstance(c, Bundle)])