Exemple #1
0
def clean_scss(*args, **kwargs) -> None:
    """Unlink compiled CSS (i.e. cache invalidation)."""
    sass_storage = SassFileStorage()
    __, files = sass_storage.listdir("")

    for source_map in filter(lambda x: x.endswith(".css.map"), files):
        sass_storage.delete(source_map)
Exemple #2
0
class Command(BaseCommand):
    help = "Compile SASS/SCSS into CSS outside of the request/response cycle"
    storage = SassFileStorage()

    def __init__(self):
        self.parser = None
        self.template_exts = getattr(settings, 'SASS_TEMPLATE_EXTS', ['.html'])
        self.sass_output_style = getattr(
            settings, 'SASS_OUTPUT_STYLE',
            'nested' if settings.DEBUG else 'compressed')
        self.use_static_root = False
        self.static_root = ''
        super().__init__()

    def add_arguments(self, parser):
        parser.add_argument(
            '--delete-files',
            action='store_true',
            dest='delete_files',
            default=False,
            help=_("Delete generated `*.css` files instead of creating them."))
        parser.add_argument(
            '--use-processor-root',
            action='store_true',
            dest='use_processor_root',
            default=False,
            help=_(
                "Store resulting .css in settings.SASS_PROCESSOR_ROOT folder. "
                "Default: store each css side-by-side with .scss."))
        parser.add_argument(
            '--engine',
            dest='engine',
            default='django',
            help=_(
                "Set templating engine used (django, jinja2). Default: django."
            ))
        parser.add_argument(
            '--sass-precision',
            dest='sass_precision',
            type=int,
            help=
            _("Set the precision for numeric computations in the SASS processor. Default: settings.SASS_PRECISION."
              ))

    def get_loaders(self):
        template_source_loaders = []
        for e in engines.all():
            if hasattr(e, 'engine'):
                template_source_loaders.extend(
                    e.engine.get_template_loaders(e.engine.loaders))
        loaders = []
        # If template loader is CachedTemplateLoader, return the loaders
        # that it wraps around. So if we have
        # TEMPLATE_LOADERS = (
        #    ('django.template.loaders.cached.Loader', (
        #        'django.template.loaders.filesystem.Loader',
        #        'django.template.loaders.app_directories.Loader',
        #    )),
        # )
        # The loaders will return django.template.loaders.filesystem.Loader
        # and django.template.loaders.app_directories.Loader
        # The cached Loader and similar ones include a 'loaders' attribute
        # so we look for that.
        for loader in template_source_loaders:
            if hasattr(loader, 'loaders'):
                loaders.extend(loader.loaders)
            else:
                loaders.append(loader)
        return loaders

    def get_parser(self, engine):
        if engine == 'jinja2':
            from compressor.offline.jinja2 import Jinja2Parser
            env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
            parser = Jinja2Parser(charset='utf-8', env=env)
        elif engine == 'django':
            from compressor.offline.django import DjangoParser
            parser = DjangoParser(charset='utf-8')
        else:
            raise CommandError(
                "Invalid templating engine '{engine}' specified.".format(
                    engine=engine))
        return parser

    def handle(self, *args, **options):
        self.verbosity = int(options['verbosity'])
        self.delete_files = options['delete_files']
        self.use_static_root = options['use_processor_root']
        if self.use_static_root:
            self.static_root = getattr(settings, 'SASS_PROCESSOR_ROOT',
                                       settings.STATIC_ROOT)

        engines = [e.strip() for e in options.get('engines', [])] or ['django']
        for engine in engines:
            self.parser = self.get_parser(engine)
            try:
                self.sass_precision = int(options['sass_precision']
                                          or settings.SASS_PRECISION)
            except (AttributeError, TypeError, ValueError):
                self.sass_precision = None

            self.processed_files = []

            # find all Python files making up this project; They might invoke `sass_processor`
            for py_source in self.find_sources():
                if self.verbosity > 1:
                    self.stdout.write("Parsing file: %s" % py_source)
                elif self.verbosity == 1:
                    self.stdout.write(".", ending="")
                try:
                    self.parse_source(py_source)
                except (SyntaxError, IndentationError) as e:
                    self.stderr.write(
                        "Syntax error encountered processing %s" % py_source)
                    self.stderr.write("Aborting compilation")
                    raise

            # find all Django/Jinja2 templates making up this project; They might invoke `sass_src`
            templates = self.find_templates()
            for template_name in templates:
                self.parse_template(template_name)
                if self.verbosity > 0:
                    self.stdout.write(".", ending="")

            # summarize what has been done
            if self.verbosity > 0:
                self.stdout.write("")
                if self.delete_files:
                    msg = "Successfully deleted {0} previously generated `*.css` files."
                    self.stdout.write(msg.format(len(self.processed_files)))
                else:
                    msg = "Successfully compiled {0} referred SASS/SCSS files."
                    self.stdout.write(msg.format(len(self.processed_files)))

    def find_sources(self):
        """
        Look for Python sources available for the current configuration.
        """
        app_config = apps.get_app_config('sass_processor')
        if app_config.auto_include:
            app_configs = apps.get_app_configs()
            for app_config in app_configs:
                ignore_dirs = []
                for root, dirs, files in os.walk(app_config.path):
                    if [True for idir in ignore_dirs if root.startswith(idir)]:
                        continue
                    if '__init__.py' not in files:
                        ignore_dirs.append(root)
                        continue
                    for filename in files:
                        basename, ext = os.path.splitext(filename)
                        if ext != '.py':
                            continue
                        yield os.path.abspath(os.path.join(root, filename))

    def parse_source(self, filename):
        """
        Extract the statements from the given file, look for function calls
        `sass_processor(scss_file)` and compile the filename into CSS.
        """
        callvisitor = FuncCallVisitor('sass_processor')
        tree = ast.parse(open(filename, 'rb').read())
        callvisitor.visit(tree)
        for sass_fileurl in callvisitor.sass_files:
            sass_filename = find_file(sass_fileurl)
            if not sass_filename or sass_filename in self.processed_files:
                continue
            if self.delete_files:
                self.delete_file(sass_filename, sass_fileurl)
            else:
                self.compile_sass(sass_filename, sass_fileurl)

    def find_templates(self):
        """
        Look for templates and extract the nodes containing the SASS file.
        """
        paths = set()
        for loader in self.get_loaders():
            try:
                module = import_module(loader.__module__)
                get_template_sources = getattr(module, 'get_template_sources',
                                               loader.get_template_sources)
                template_sources = get_template_sources('')
                paths.update([
                    t.name if isinstance(t, Origin) else t
                    for t in template_sources
                ])
            except (ImportError, AttributeError):
                pass
        if not paths:
            raise CommandError(
                "No template paths found. None of the configured template loaders provided template paths"
            )
        templates = set()
        for path in paths:
            for root, _, files in os.walk(str(path)):
                templates.update(
                    os.path.join(root, name) for name in files
                    if not name.startswith('.') and any(
                        name.endswith(ext) for ext in self.template_exts))
        if not templates:
            raise CommandError(
                "No templates found. Make sure your TEMPLATE_LOADERS and TEMPLATE_DIRS settings are correct."
            )
        return templates

    def parse_template(self, template_name):
        try:
            template = self.parser.parse(template_name)
        except IOError:  # unreadable file -> ignore
            if self.verbosity > 0:
                self.stderr.write(
                    "\nUnreadable template at: {}".format(template_name))
            return
        except TemplateSyntaxError as e:  # broken template -> ignore
            if self.verbosity > 0:
                self.stderr.write("\nInvalid template {}: {}".format(
                    template_name, e))
            return
        except TemplateDoesNotExist:  # non existent template -> ignore
            if self.verbosity > 0:
                self.stderr.write(
                    "\nNon-existent template at: {}".format(template_name))
            return
        except UnicodeDecodeError:
            if self.verbosity > 0:
                self.stderr.write(
                    "\nUnicodeDecodeError while trying to read template {}".
                    format(template_name))
        try:
            nodes = list(self.walk_nodes(template, original=template))
        except Exception as e:
            # Could be an error in some base template
            if self.verbosity > 0:
                self.stderr.write("\nError parsing template {}: {}".format(
                    template_name, e))
        else:
            for node in nodes:
                sass_filename = find_file(node.path)
                if not sass_filename or sass_filename in self.processed_files:
                    continue
                if self.delete_files:
                    self.delete_file(sass_filename, node.path)
                else:
                    self.compile_sass(sass_filename, node.path)

    def compile_sass(self, sass_filename, sass_fileurl):
        """
        Compile the given SASS file into CSS
        """
        compile_kwargs = {
            'filename': sass_filename,
            'include_paths': SassProcessor.include_paths + APPS_INCLUDE_DIRS,
            'custom_functions': get_custom_functions(),
        }
        if self.sass_precision:
            compile_kwargs['precision'] = self.sass_precision
        if self.sass_output_style:
            compile_kwargs['output_style'] = self.sass_output_style
        content = sass.compile(**compile_kwargs)
        self.save_to_destination(content, sass_filename, sass_fileurl)
        self.processed_files.append(sass_filename)
        if self.verbosity > 1:
            self.stdout.write(
                "Compiled SASS/SCSS file: '{0}'\n".format(sass_filename))

    def delete_file(self, sass_filename, sass_fileurl):
        """
        Delete a *.css file, but only if it has been generated through a SASS/SCSS file.
        """
        if self.use_static_root:
            destpath = os.path.join(self.static_root,
                                    os.path.splitext(sass_fileurl)[0] + '.css')
        else:
            destpath = os.path.splitext(sass_filename)[0] + '.css'
        if os.path.isfile(destpath):
            os.remove(destpath)
            self.processed_files.append(sass_filename)
            if self.verbosity > 1:
                self.stdout.write("Deleted '{0}'\n".format(destpath))

    def save_to_destination(self, content, sass_filename, sass_fileurl):
        if self.use_static_root:
            basename, _ = os.path.splitext(sass_fileurl)
            destpath = os.path.join(self.static_root, basename + '.css')
            self.storage.save(destpath, ContentFile(content))
        else:
            basename, _ = os.path.splitext(sass_filename)
            destpath = basename + '.css'
            with open(destpath, 'wb') as fh:
                fh.write(force_bytes(content))

    def walk_nodes(self, node, original):
        """
        Iterate over the nodes recursively yielding the templatetag 'sass_src'
        """
        try:
            # try with django-compressor<2.1
            nodelist = self.parser.get_nodelist(node, original=original)
        except TypeError:
            nodelist = self.parser.get_nodelist(node,
                                                original=original,
                                                context=None)
        for node in nodelist:
            if isinstance(node, SassSrcNode):
                if node.is_sass:
                    yield node
            else:
                for node in self.walk_nodes(node, original=original):
                    yield node