/usr/share/pyshared/webassets/script.py is in python-webassets 3:0.9-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 | from __future__ import print_function
import shutil
import os, sys
import time
import logging
from webassets.loaders import PythonLoader, YAMLLoader
from webassets.bundle import get_all_bundle_files
from webassets.exceptions import BuildError
from webassets.updater import TimestampUpdater
from webassets.merge import MemoryHunk
from webassets.version import get_manifest
from webassets.cache import FilesystemCache
from webassets.utils import set, StringIO
__all__ = ('CommandError', 'CommandLineEnvironment', 'main')
# logging has WARNING as default level, for the CLI we want INFO. Set this
# as early as possible, so that user customizations will not be overwritten.
logging.getLogger('webassets.script').setLevel(logging.INFO)
class CommandError(Exception):
pass
class Command(object):
"""Base-class for a command used by :class:`CommandLineEnvironment`.
Each command being a class opens up certain possibilities with respect to
subclassing and customizing the default CLI.
"""
def __init__(self, cmd_env):
self.cmd = cmd_env
def __getattr__(self, name):
# Make stuff from cmd environment easier to access
return getattr(self.cmd, name)
def __call__(self, *args, **kwargs):
raise NotImplementedError()
class BuildCommand(Command):
def __call__(self, bundles=None, output=None, directory=None, no_cache=None,
manifest=None, production=None):
"""Build assets.
``bundles``
A list of bundle names. If given, only this list of bundles
should be built.
``output``
List of (bundle, filename) 2-tuples. If given, only these
bundles will be built, using the custom output filenames.
Cannot be used with ``bundles``.
``directory``
Custom output directory to use for the bundles. The original
basenames defined in the bundle ``output`` attribute will be
used. If the ``output`` of the bundles are pointing to different
directories, they will be offset by their common prefix.
Cannot be used with ``output``.
``no_cache``
If set, a cache (if one is configured) will not be used.
``manifest``
If set, the given manifest instance will be used, instead of
any that might have been configured in the Environment. The value
passed will be resolved through ``get_manifest()``. If this fails,
a file-based manifest will be used using the given value as the
filename.
``production``
If set to ``True``, then :attr:`Environment.debug`` will forcibly
be disabled (set to ``False``) during the build.
"""
# Validate arguments
if bundles and output:
raise CommandError(
'When specifying explicit output filenames you must '
'do so for all bundles you want to build.')
if directory and output:
raise CommandError('A custom output directory cannot be '
'combined with explicit output filenames '
'for individual bundles.')
if production:
# TODO: Reset again (refactor commands to be classes)
self.environment.debug = False
# TODO: Oh how nice it would be to use the future options stack.
if manifest is not None:
try:
manifest = get_manifest(manifest, env=self.environment)
except ValueError:
manifest = get_manifest(
# abspath() is important, or this will be considered
# relative to Environment.directory.
"file:%s" % os.path.abspath(manifest),
env=self.environment)
self.environment.manifest = manifest
# Use output as a dict.
if output:
output = dict(output)
# Validate bundle names
bundle_names = bundles if bundles else (output.keys() if output else [])
for name in bundle_names:
if not name in self.environment:
raise CommandError(
'I do not know a bundle name named "%s".' % name)
# Make a list of bundles to build, and the filename to write to.
if bundle_names:
# TODO: It's not ok to use an internal property here.
bundles = [(n,b) for n, b in self.environment._named_bundles.items()
if n in bundle_names]
else:
# Includes unnamed bundles as well.
bundles = [(None, b) for b in self.environment]
# Determine common prefix for use with ``directory`` option.
if directory:
prefix = os.path.commonprefix(
[os.path.normpath(b.resolve_output(self.environment))
for _, b in bundles if b.output])
# dirname() gives the right value for a single file.
prefix = os.path.dirname(prefix)
to_build = []
for name, bundle in bundles:
# TODO: We really should support this. This error here
# is just in place of a less understandable error that would
# otherwise occur.
if bundle.is_container and directory:
raise CommandError(
'A custom output directory cannot currently be '
'used with container bundles.')
# Determine which filename to use, if not the default.
overwrite_filename = None
if output:
overwrite_filename = output[name]
elif directory:
offset = os.path.normpath(
bundle.resolve_output(self.environment))[len(prefix)+1:]
overwrite_filename = os.path.join(directory, offset)
to_build.append((bundle, overwrite_filename, name,))
# Build.
built = []
for bundle, overwrite_filename, name in to_build:
if name:
# A name is not necessary available of the bundle was
# registered without one.
self.log.info("Building bundle: %s (to %s)" % (
name, overwrite_filename or bundle.output))
else:
self.log.info("Building bundle: %s" % bundle.output)
try:
if not overwrite_filename:
bundle.build(force=True, env=self.environment,
disable_cache=no_cache)
else:
# TODO: Rethink how we deal with container bundles here.
# As it currently stands, we write all child bundles
# to the target output, merged (which is also why we
# create and force writing to a StringIO instead of just
# using the ``Hunk`` objects that build() would return
# anyway.
output = StringIO()
bundle.build(force=True, env=self.environment, output=output,
disable_cache=no_cache)
if directory:
# Only auto-create directories in this mode.
output_dir = os.path.dirname(overwrite_filename)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
MemoryHunk(output.getvalue()).save(overwrite_filename)
built.append(bundle)
except BuildError as e:
self.log.error("Failed, error was: %s" % e)
if len(built):
self.event_handlers['post_build']()
if len(built) != len(to_build):
return 2
class WatchCommand(Command):
def __call__(self, loop=None):
"""Watch assets for changes.
``loop``
A callback, taking no arguments, to be called once every loop
iteration. Can be useful to integrate the command with other code.
If not specified, the loop wil call ``time.sleep()``.
"""
# TODO: This should probably also restart when the code changes.
mtimes = {}
try:
# Before starting to watch for changes, also recognize changes
# made while we did not run, and apply those immediately.
for bundle in self.environment:
print('Bringing up to date: %s' % bundle.output)
bundle.build(force=False)
self.log.info("Watching %d bundles for changes..." %
len(self.environment))
while True:
changed_bundles = self.check_for_changes(mtimes)
built = []
for bundle in changed_bundles:
print("Building bundle: %s ..." % bundle.output, end=' ')
sys.stdout.flush()
try:
bundle.build(force=True)
built.append(bundle)
except BuildError as e:
print("")
print("Failed: %s" % e)
else:
print("done")
if len(built):
self.event_handlers['post_build']()
do_end = loop() if loop else time.sleep(0.1)
if do_end:
break
except KeyboardInterrupt:
pass
def check_for_changes(self, mtimes):
# Do not update original mtimes dict right away, so that we detect
# all bundle changes if a file is in multiple bundles.
_new_mtimes = mtimes.copy()
changed_bundles = set()
# TODO: An optimization was lost here, skipping a bundle once
# a single file has been found to have changed. Bring back.
for filename, bundles_to_update in self.yield_files_to_watch():
stat = os.stat(filename)
mtime = stat.st_mtime
if sys.platform == "win32":
mtime -= stat.st_ctime
if mtimes.get(filename, mtime) != mtime:
if callable(bundles_to_update):
# Hook for when file has changed
try:
bundles_to_update = bundles_to_update()
except EnvironmentError:
# EnvironmentError is what the hooks is allowed to
# raise for a temporary problem, like an invalid config
import traceback
traceback.print_exc()
# Don't update anything, wait for another change
bundles_to_update = set()
if bundles_to_update is True:
# Indicates all bundles should be rebuilt for the change
bundles_to_update = set(self.environment)
changed_bundles |= bundles_to_update
_new_mtimes[filename] = mtime
_new_mtimes[filename] = mtime
mtimes.update(_new_mtimes)
return changed_bundles
def yield_files_to_watch(self):
for bundle in self.environment:
for filename in get_all_bundle_files(bundle):
yield filename, set([bundle])
class CleanCommand(Command):
def __call__(self):
"""Delete generated assets.
"""
self.log.info('Cleaning generated assets...')
for bundle in self.environment:
if not bundle.output:
continue
file_path = bundle.resolve_output(self.environment)
if os.path.exists(file_path):
os.unlink(file_path)
self.log.info("Deleted asset: %s" % bundle.output)
if isinstance(self.environment.cache, FilesystemCache):
shutil.rmtree(self.environment.cache.directory)
class CheckCommand(Command):
def __call__(self):
"""Check to see if assets need to be rebuilt.
A non-zero exit status will be returned if any of the input files are
newer (based on mtime) than their output file. This is intended to be
used in pre-commit hooks.
"""
needsupdate = False
updater = self.environment.updater
if not updater:
self.log.debug('no updater configured, using TimestampUpdater')
updater = TimestampUpdater()
for bundle in self.environment:
self.log.info('Checking asset: %s', bundle.output)
if updater.needs_rebuild(bundle, self.environment):
self.log.info(' needs update')
needsupdate = True
if needsupdate:
sys.exit(-1)
class CommandLineEnvironment(object):
"""Implements the core functionality for a command line frontend to
``webassets``, abstracted in a way to allow frameworks to integrate the
functionality into their own tools, for example, as a Django management
command, or a command for ``Flask-Script``.
"""
def __init__(self, env, log, post_build=None, commands=None):
self.environment = env
self.log = log
self.event_handlers = dict(post_build=lambda: True)
if callable(post_build):
self.event_handlers['post_build'] = post_build
# Instantiate each command
command_def = self.DefaultCommands.copy()
command_def.update(commands or {})
self.commands = {}
for name, construct in command_def.items():
if not construct:
continue
if not isinstance(construct, (list, tuple)):
construct = [construct, (), {}]
self.commands[name] = construct[0](
self, *construct[1], **construct[2])
def __getattr__(self, item):
# Allow method-like access to commands.
if item in self.commands:
return self.commands[item]
raise AttributeError(item)
def invoke(self, command, args):
"""Invoke ``command``, or throw a CommandError.
This is essentially a simple validation mechanism. Feel free
to call the individual command methods manually.
"""
try:
function = self.commands[command]
except KeyError as e:
raise CommandError('unknown command: %s' % e)
else:
return function(**args)
# List of commands installed
DefaultCommands = {
'build': BuildCommand,
'watch': WatchCommand,
'clean': CleanCommand,
'check': CheckCommand
}
class GenericArgparseImplementation(object):
"""Generic command line utility to interact with an webassets environment.
This is effectively a reference implementation of a command line utility
based on the ``CommandLineEnvironment`` class. Implementers may find it
feasible to simple base their own command line utility on this, rather than
implementing something custom on top of ``CommandLineEnvironment``. In
fact, if that is possible, you are encouraged to do so for greater
consistency across implementations.
"""
class WatchCommand(WatchCommand):
"""Extended watch command that also looks at the config file itself."""
def __init__(self, cmd_env, argparse_ns):
WatchCommand.__init__(self, cmd_env)
self.ns = argparse_ns
def yield_files_to_watch(self):
for result in WatchCommand.yield_files_to_watch(self):
yield result
# If the config changes, rebuild all bundles
if getattr(self.ns, 'config', None):
yield self.ns.config, self.reload_config
def reload_config(self):
try:
self.cmd.environment = YAMLLoader(self.ns.config).load_environment()
except Exception as e:
raise EnvironmentError(e)
return True
def __init__(self, env=None, log=None, prog=None, no_global_options=False):
try:
import argparse
except ImportError:
raise RuntimeError(
'The webassets command line now requires the '
'"argparse" library on Python versions <= 2.6.')
else:
self.argparse = argparse
self.env = env
self.log = log
self._construct_parser(prog, no_global_options)
def _construct_parser(self, prog=None, no_global_options=False):
self.parser = parser = self.argparse.ArgumentParser(
description="Manage assets.",
prog=prog)
if not no_global_options:
# Start with the base arguments that are valid for any command.
# XXX: Add those to the subparser?
parser.add_argument("-v", dest="verbose", action="store_true",
help="be verbose")
parser.add_argument("-q", action="store_true", dest="quiet",
help="be quiet")
if self.env is None:
loadenv = parser.add_mutually_exclusive_group()
loadenv.add_argument("-c", "--config", dest="config",
help="read environment from a YAML file")
loadenv.add_argument("-m", "--module", dest="module",
help="read environment from a Python module")
# Add subparsers.
subparsers = parser.add_subparsers(dest='command')
for command in CommandLineEnvironment.DefaultCommands.keys():
command_parser = subparsers.add_parser(command)
maker = getattr(self, 'make_%s_parser' % command, False)
if maker:
maker(command_parser)
@staticmethod
def make_build_parser(parser):
parser.add_argument(
'bundles', nargs='*', metavar='BUNDLE',
help='Optional bundle names to process. If none are '
'specified, then all known bundles will be built.')
parser.add_argument(
'--output', '-o', nargs=2, action='append',
metavar=('BUNDLE', 'FILE'),
help='Build the given bundle, and use a custom output '
'file. Can be given multiple times.')
parser.add_argument(
'--directory', '-d',
help='Write built files to this directory, using the '
'basename defined by the bundle. Will offset '
'the original bundle output paths on their common '
'prefix. Cannot be used with --output.')
parser.add_argument(
'--no-cache', action='store_true',
help='Do not use a cache that might be configured.')
parser.add_argument(
'--manifest',
help='Write a manifest to the given file. Also supports '
'the id:arg format, if you want to use a different '
'manifest implementation.')
parser.add_argument(
'--production', action='store_true',
help='Forcably turn off debug mode for the build. This '
'only has an effect if debug is set to "merge".')
def _setup_logging(self, ns):
if self.log:
log = self.log
else:
log = logging.getLogger('webassets.script')
if not log.handlers:
# In theory, this could run multiple times (e.g. tests)
handler = logging.StreamHandler()
log.addHandler(handler)
# Note that setting the level filter at the handler level is
# better than the logger level, since this is "our" handler,
# we create it, for the purposes of having a default output.
# The logger itself the user may be modifying.
handler.setLevel(logging.DEBUG if ns.verbose else (
logging.WARNING if ns.quiet else logging.INFO))
return log
def _setup_assets_env(self, ns, log):
env = self.env
if env is None:
assert not (ns.module and ns.config)
if ns.module:
env = PythonLoader(ns.module).load_environment()
if ns.config:
env = YAMLLoader(ns.config).load_environment()
return env
def _setup_cmd_env(self, assets_env, log, ns):
return CommandLineEnvironment(assets_env, log, commands={
'watch': (GenericArgparseImplementation.WatchCommand, (ns,), {})
})
def _prepare_command_args(self, ns):
# Prepare a dict of arguments cleaned of values that are not
# command-specific, and which the command method would not accept.
args = vars(ns).copy()
for action in self.parser._actions:
dest = action.dest
if dest in args:
del args[dest]
return args
def run_with_ns(self, ns):
log = self._setup_logging(ns)
env = self._setup_assets_env(ns, log)
if env is None:
raise CommandError(
"Error: No environment given or found. Maybe use -m?")
cmd = self._setup_cmd_env(env, log, ns)
# Run the selected command
args = self._prepare_command_args(ns)
return cmd.invoke(ns.command, args)
def run_with_argv(self, argv):
try:
ns = self.parser.parse_args(argv)
except SystemExit:
# We do not want the main() function to exit the program.
# See run() instead.
return 1
return self.run_with_ns(ns)
def main(self, argv):
"""Parse the given command line.
The commandline is expected to NOT including what would be sys.argv[0].
"""
try:
return self.run_with_argv(argv)
except CommandError as e:
print(e)
return 1
def main(argv, env=None):
"""Execute the generic version of the command line interface.
You only need to work directly with ``GenericArgparseImplementation`` if
you desire to customize things.
If no environment is given, additional arguments will be supported to allow
the user to specify/construct the environment on the command line.
"""
return GenericArgparseImplementation(env).main(argv)
def run():
"""Runs the command line interface via ``main``, then exits the process
with a proper return code."""
sys.exit(main(sys.argv[1:]) or 0)
if __name__ == '__main__':
run()
|