Пример #1
0
def compile_command(context, backend, config):
    """
    Compile Sass project sources to CSS
    """
    logger = logging.getLogger("boussole")
    logger.info(u"Building project")

    # Discover settings file
    try:
        discovering = Discover(
            backends=[SettingsBackendJson, SettingsBackendYaml])
        config_filepath, config_engine = discovering.search(
            filepath=config, basedir=os.getcwd(), kind=backend)

        project = ProjectBase(backend_name=config_engine._kind_name)
        settings = project.backend_engine.load(filepath=config_filepath)
    except BoussoleBaseException as e:
        logger.critical(six.text_type(e))
        raise click.Abort()

    logger.debug(u"Settings file: {} ({})".format(config_filepath,
                                                  config_engine._kind_name))
    logger.debug(u"Project sources directory: {}".format(
        settings.SOURCES_PATH))
    logger.debug(u"Project destination directory: {}".format(
        settings.TARGET_PATH))
    logger.debug(u"Exclude patterns: {}".format(settings.EXCLUDES))

    # Find all sources with their destination path
    try:
        compilable_files = ScssFinder().mirror_sources(
            settings.SOURCES_PATH,
            targetdir=settings.TARGET_PATH,
            excludes=settings.EXCLUDES)
    except BoussoleBaseException as e:
        logger.error(six.text_type(e))
        raise click.Abort()

    # Build all compilable stylesheets
    compiler = SassCompileHelper()
    errors = 0
    for src, dst in compilable_files:
        logger.debug(u"Compile: {}".format(src))

        output_opts = {}
        success, message = compiler.safe_compile(settings, src, dst)

        if success:
            logger.info(u"Output: {}".format(message), **output_opts)
        else:
            errors += 1
            logger.error(message)

    # Ensure correct exit code if error has occured
    if errors:
        raise click.Abort()
Пример #2
0
    def __init__(self, settings, logger, inspector, *args, **kwargs):
        self.settings = settings
        self.logger = logger
        self.inspector = inspector

        self.finder = ScssFinder()
        self.compiler = SassCompileHelper()

        self.compilable_files = {}
        self.source_files = []
        self._event_error = False

        super(SassLibraryEventHandler, self).__init__(*args, **kwargs)
Пример #3
0
def compile_command(context, config):
    """
    Compile SASS project sources to CSS
    """
    logger = context.obj['logger']
    logger.info("Building project")

    # Load settings file
    try:
        backend = SettingsBackendJson(basedir=os.getcwd())
        settings = backend.load(filepath=config)
    except SettingsBackendError as e:
        logger.critical(e.message)
        raise click.Abort()

    logger.debug("Project sources directory: {}".format(
                settings.SOURCES_PATH))
    logger.debug("Project destination directory: {}".format(
                settings.TARGET_PATH))
    logger.debug("Exclude patterns: {}".format(
                settings.EXCLUDES))

    # Find all sources with their destination path
    try:
        compilable_files = ScssFinder().mirror_sources(
            settings.SOURCES_PATH,
            targetdir=settings.TARGET_PATH,
            excludes=settings.EXCLUDES
        )
    except BoussoleBaseException as e:
        logger.error(e.message)
        raise click.Abort()

    # Build all compilable stylesheets
    compiler = SassCompileHelper()
    errors = 0
    for src, dst in compilable_files:
        logger.debug("Compile: {}".format(src))

        output_opts = {}
        success, message = compiler.safe_compile(settings, src, dst)

        if success:
            logger.info("Output: {}".format(message), **output_opts)
        else:
            errors += 1
            logger.error(message)

    # Ensure correct exit code if error has occured
    if errors:
        raise click.Abort()
Пример #4
0
    def __init__(self, settings, inspector, *args, **kwargs):
        self.settings = settings
        self.inspector = inspector

        self.logger = logging.getLogger("boussole")
        self.finder = ScssFinder()
        self.compiler = SassCompileHelper()

        self.compilable_files = {}
        self.source_files = []
        self._event_error = False

        super(SassLibraryEventHandler, self).__init__(*args, **kwargs)
Пример #5
0
def compiler():
    """Initialize and return SCSS compile helper (scope at module level)"""
    return SassCompileHelper()
Пример #6
0
class SassLibraryEventHandler(object):
    """
    Watch mixin handler for library sources

    Handler does not compile source which triggered an event,
    only its parent dependencies. Because libraries are not intended to be
    compiled.

    Args:
        settings (boussole.conf.model.Settings): Project settings.
        logger (logging.Logger): Logger object to write messages.
        inspector (boussole.inspector.ScssInspector): Inspector instance.

    Attributes:
        settings (boussole.conf.model.Settings): Filled from argument.
        logger (logging.Logger): Filled from argument.
        inspector (boussole.inspector.ScssInspector): Filled from argument.
        finder (boussole.finder.ScssFinder): Finder instance.
        compiler (boussole.compiler.SassCompileHelper): Sass compile helper
            object.
        compilable_files (dict): Pair of (source path, destination path) to
            compile. Automatically update from ``index()`` method.
        source_files (list): List of source path to compile. Automatically
            update from ``index()`` method.
        _event_error (bool): Internal flag setted to ``True`` if error has
            occured within an event. ``index()`` will reboot it to ``False``
            each time a new event occurs.
    """
    def __init__(self, settings, logger, inspector, *args, **kwargs):
        self.settings = settings
        self.logger = logger
        self.inspector = inspector

        self.finder = ScssFinder()
        self.compiler = SassCompileHelper()

        self.compilable_files = {}
        self.source_files = []
        self._event_error = False

        super(SassLibraryEventHandler, self).__init__(*args, **kwargs)

    def index(self):
        """
        Reset inspector buffers and index project sources dependencies.

        This have to be executed each time an event occurs.

        Note:
            If a Boussole exception occurs during operation, it will be catched
            and an error flag will be set to ``True`` so event operation will
            be blocked without blocking or breaking watchdog observer.
        """
        self._event_error = False

        try:
            compilable_files = self.finder.mirror_sources(
                self.settings.SOURCES_PATH,
                targetdir=self.settings.TARGET_PATH,
                excludes=self.settings.EXCLUDES)
            self.compilable_files = dict(compilable_files)
            self.source_files = self.compilable_files.keys()

            # Init inspector and do first inspect
            self.inspector.reset()
            self.inspector.inspect(*self.source_files,
                                   library_paths=self.settings.LIBRARY_PATHS)
        except BoussoleBaseException as e:
            self._event_error = True
            self.logger.error(e.message)

    def compile_source(self, sourcepath):
        """
        Compile source to its destination

        Check if the source is eligible to compile (not partial and allowed
        from exclude patterns)

        Args:
            sourcepath (string): Sass source path to compile to its
                destination using project settings.

        Returns:
            tuple or None: A pair of (sourcepath, destination), if source has
                been compiled (or at least tried). If the source was not
                eligible to compile, return will be ``None``.
        """
        relpath = os.path.relpath(sourcepath, self.settings.SOURCES_PATH)

        conditions = {
            'sourcedir': None,
            'nopartial': True,
            'exclude_patterns': self.settings.EXCLUDES,
            'excluded_libdirs': self.settings.LIBRARY_PATHS,
        }
        if self.finder.match_conditions(sourcepath, **conditions):
            destination = self.finder.get_destination(
                relpath, targetdir=self.settings.TARGET_PATH)

            self.logger.debug("Compile: {}".format(sourcepath))
            success, message = self.compiler.safe_compile(
                self.settings, sourcepath, destination)

            if success:
                self.logger.info("Output: {}".format(message))
            else:
                self.logger.error(message)

            return sourcepath, destination

        return None

    def compile_dependencies(self, sourcepath, include_self=False):
        """
        Apply compile on all dependencies

        Args:
            sourcepath (string): Sass source path to compile to its
                destination using project settings.

        Keyword Arguments:
            include_self (bool): If ``True`` the given sourcepath is add to
                items to compile, else only its dependencies are compiled.
        """
        items = self.inspector.parents(sourcepath)

        # Also add the current event related path
        if include_self:
            items.add(sourcepath)

        return filter(None, [self.compile_source(item) for item in items])

    def on_any_event(self, event):
        """
        Catch-all event handler (moved, created, deleted, changed).

        Before any event, index project to have the right and current
        dependencies map.

        Args:
            event: Watchdog event ``watchdog.events.FileSystemEvent``.
        """
        self.index()

    def on_moved(self, event):
        """
        Called when a file or a directory is moved or renamed.

        Many editors don't directly change a file, instead they make a
        transitional file like ``*.part`` then move it to the final filename.

        Args:
            event: Watchdog event, either ``watchdog.events.DirMovedEvent`` or
                ``watchdog.events.FileModifiedEvent``.
        """
        if not self._event_error:
            # We are only interested for final file, not transitional file
            # from editors (like *.part)
            pathtools_options = {
                'included_patterns': self.patterns,
                'excluded_patterns': self.ignore_patterns,
                'case_sensitive': self.case_sensitive,
            }
            # Apply pathtool matching on destination since Watchdog only
            # automatically apply it on source
            if match_path(event.dest_path, **pathtools_options):
                self.logger.info("Change detected from a move on: %s",
                                 event.dest_path)
                self.compile_dependencies(event.dest_path)

    def on_created(self, event):
        """
        Called when a new file or directory is created.

        Todo:
            This should be also used (extended from another class?) to watch
            for some special name file (like ".boussole-watcher-stop" create to
            raise a KeyboardInterrupt, so we may be able to unittest the
            watcher (click.CliRunner is not able to send signal like CTRL+C
            that is required to watchdog observer loop)

        Args:
            event: Watchdog event, either ``watchdog.events.DirCreatedEvent``
                or ``watchdog.events.FileCreatedEvent``.
        """
        if not self._event_error:
            self.logger.info("Change detected from a create on: %s",
                             event.src_path)

            self.compile_dependencies(event.src_path)

    def on_modified(self, event):
        """
        Called when a file or directory is modified.

        Args:
            event: Watchdog event, ``watchdog.events.DirModifiedEvent`` or
                ``watchdog.events.FileModifiedEvent``.
        """
        if not self._event_error:
            self.logger.info("Change detected from an edit on: %s",
                             event.src_path)

            self.compile_dependencies(event.src_path)

    def on_deleted(self, event):
        """
        Called when a file or directory is deleted.

        Todo:
            Bugged with inspector and sass compiler since the does not exists
            anymore.

        Args:
            event: Watchdog event, ``watchdog.events.DirDeletedEvent`` or
                ``watchdog.events.FileDeletedEvent``.
        """
        if not self._event_error:
            self.logger.info("Change detected from deletion of: %s",
                             event.src_path)
            # Never try to compile the deleted source
            self.compile_dependencies(event.src_path, include_self=False)
Пример #7
0
def test_boussole_compile_auto(tests_settings, temp_builds_dir, manifest_name):
    """
    Testing everything:

    * Sass helpers correctly generate CSS;
    * Manifest is correctly serialized to expected datas;
    * Builded CSS is the same than stored one in data fixtures;
    """
    manifest_css = manifest_name + ".css"
    manifest_json = os.path.join(
        tests_settings.fixtures_path,
        'json',
        manifest_name + ".json",
    )

    # Open JSON fixture for expected serialized data from parsed manifest
    with open(manifest_json, "r") as fp:
        expected = json.load(fp)

    basepath = temp_builds_dir.join(
        'sass_helper_boussole_compile_{}'.format(manifest_css))
    basedir = basepath.strpath

    template_sassdir = os.path.join(tests_settings.fixtures_path, 'sass')

    test_sassdir = os.path.join(basedir, 'sass')
    test_config_filepath = os.path.join(test_sassdir, 'settings.json')

    # Copy Sass sources to compile from template
    shutil.copytree(template_sassdir, test_sassdir)

    # Get expected CSS content from file in fixture
    expected_css_filepath = os.path.join(tests_settings.fixtures_path, "sass",
                                         "css", manifest_css)
    with io.open(expected_css_filepath, 'r') as fp:
        expected_css_content = fp.read()

    # Load boussole settings and search for compilable files
    project = ProjectBase(backend_name="json")
    settings = project.backend_engine.load(filepath=test_config_filepath)
    compilable_files = ScssFinder().mirror_sources(
        settings.SOURCES_PATH,
        targetdir=settings.TARGET_PATH,
        excludes=settings.EXCLUDES)

    # Since Boussole list every compilable Sass source, we select only the entry
    # corresponding to the manifest we seek for (from "manifest_css")
    source_css_filename = None
    source_sass_filename = None
    for k, v in compilable_files:
        if v.endswith(manifest_css):
            source_sass_filename = k
            source_css_filename = v
            break

    # Compile only the source we target from "manifest_css"
    compiler = SassCompileHelper()
    success, message = compiler.safe_compile(settings, source_sass_filename,
                                             source_css_filename)

    # Output error to ease debug
    if not success:
        print(u"Compile error with: {}".format(source_sass_filename))
        print(message)
    else:
        # Builded CSS is identical to the expected one from fixture
        with io.open(source_css_filename, 'r') as fp:
            compiled_content = fp.read()
        assert expected_css_content == compiled_content

        # Described manifest is the same as expected payload from fixture
        manifest = Manifest()
        manifest.load(compiled_content)
        dump = json.loads(manifest.to_json())
        assert expected == dump
Пример #8
0
class SassLibraryEventHandler(object):
    """
    Watch mixin handler for library sources

    Handler does not compile source which triggered an event,
    only its parent dependencies. Because libraries are not intended to be
    compiled.

    Args:
        settings (boussole.conf.model.Settings): Project settings.
        inspector (boussole.inspector.ScssInspector): Inspector instance.

    Attributes:
        settings (boussole.conf.model.Settings): Filled from argument.
        logger (logging.Logger): Boussole logger.
        inspector (boussole.inspector.ScssInspector): Filled from argument.
        finder (boussole.finder.ScssFinder): Finder instance.
        compiler (boussole.compiler.SassCompileHelper): Sass compile helper
            object.
        compilable_files (dict): Pair of (source path, destination path) to
            compile. Automatically update from ``index()`` method.
        source_files (list): List of source path to compile. Automatically
            update from ``index()`` method.
        _event_error (bool): Internal flag setted to ``True`` if error has
            occured within an event. ``index()`` will reboot it to ``False``
            each time a new event occurs.
    """
    def __init__(self, settings, inspector, *args, **kwargs):
        self.settings = settings
        self.inspector = inspector

        self.logger = logging.getLogger("boussole")
        self.finder = ScssFinder()
        self.compiler = SassCompileHelper()

        self.compilable_files = {}
        self.source_files = []
        self._event_error = False

        super(SassLibraryEventHandler, self).__init__(*args, **kwargs)

    def index(self):
        """
        Reset inspector buffers and index project sources dependencies.

        This have to be executed each time an event occurs.

        Note:
            If a Boussole exception occurs during operation, it will be catched
            and an error flag will be set to ``True`` so event operation will
            be blocked without blocking or breaking watchdog observer.
        """
        self._event_error = False

        try:
            compilable_files = self.finder.mirror_sources(
                self.settings.SOURCES_PATH,
                targetdir=self.settings.TARGET_PATH,
                excludes=self.settings.EXCLUDES
            )
            self.compilable_files = dict(compilable_files)
            self.source_files = self.compilable_files.keys()

            # Init inspector and do first inspect
            self.inspector.reset()
            self.inspector.inspect(
                *self.source_files,
                library_paths=self.settings.LIBRARY_PATHS
            )
        except BoussoleBaseException as e:
            self._event_error = True
            self.logger.error(six.text_type(e))

    def compile_source(self, sourcepath):
        """
        Compile source to its destination

        Check if the source is eligible to compile (not partial and allowed
        from exclude patterns)

        Args:
            sourcepath (string): Sass source path to compile to its
                destination using project settings.

        Returns:
            tuple or None: A pair of (sourcepath, destination), if source has
                been compiled (or at least tried). If the source was not
                eligible to compile, return will be ``None``.
        """
        relpath = os.path.relpath(sourcepath, self.settings.SOURCES_PATH)

        conditions = {
            'sourcedir': None,
            'nopartial': True,
            'exclude_patterns': self.settings.EXCLUDES,
            'excluded_libdirs': self.settings.LIBRARY_PATHS,
        }
        if self.finder.match_conditions(sourcepath, **conditions):
            destination = self.finder.get_destination(
                relpath,
                targetdir=self.settings.TARGET_PATH
            )

            self.logger.debug(u"Compile: {}".format(sourcepath))
            success, message = self.compiler.safe_compile(
                self.settings,
                sourcepath,
                destination
            )

            if success:
                self.logger.info(u"Output: {}".format(message))
            else:
                self.logger.error(message)

            return sourcepath, destination

        return None

    def compile_dependencies(self, sourcepath, include_self=False):
        """
        Apply compile on all dependencies

        Args:
            sourcepath (string): Sass source path to compile to its
                destination using project settings.

        Keyword Arguments:
            include_self (bool): If ``True`` the given sourcepath is add to
                items to compile, else only its dependencies are compiled.
        """
        items = self.inspector.parents(sourcepath)

        # Also add the current event related path
        if include_self:
            items.add(sourcepath)

        return filter(None, [self.compile_source(item) for item in items])

    def on_any_event(self, event):
        """
        Catch-all event handler (moved, created, deleted, changed).

        Before any event, index project to have the right and current
        dependencies map.

        Args:
            event: Watchdog event ``watchdog.events.FileSystemEvent``.
        """
        self.index()

    def on_moved(self, event):
        """
        Called when a file or a directory is moved or renamed.

        Many editors don't directly change a file, instead they make a
        transitional file like ``*.part`` then move it to the final filename.

        Args:
            event: Watchdog event, either ``watchdog.events.DirMovedEvent`` or
                ``watchdog.events.FileModifiedEvent``.
        """
        if not self._event_error:
            # We are only interested for final file, not transitional file
            # from editors (like *.part)
            pathtools_options = {
                'included_patterns': self.patterns,
                'excluded_patterns': self.ignore_patterns,
                'case_sensitive': self.case_sensitive,
            }
            # Apply pathtool matching on destination since Watchdog only
            # automatically apply it on source
            if match_path(event.dest_path, **pathtools_options):
                self.logger.info(u"Change detected from a move on: %s",
                                 event.dest_path)
                self.compile_dependencies(event.dest_path)

    def on_created(self, event):
        """
        Called when a new file or directory is created.

        Todo:
            This should be also used (extended from another class?) to watch
            for some special name file (like ".boussole-watcher-stop" create to
            raise a KeyboardInterrupt, so we may be able to unittest the
            watcher (click.CliRunner is not able to send signal like CTRL+C
            that is required to watchdog observer loop)

        Args:
            event: Watchdog event, either ``watchdog.events.DirCreatedEvent``
                or ``watchdog.events.FileCreatedEvent``.
        """
        if not self._event_error:
            self.logger.info(u"Change detected from a create on: %s",
                             event.src_path)

            self.compile_dependencies(event.src_path)

    def on_modified(self, event):
        """
        Called when a file or directory is modified.

        Args:
            event: Watchdog event, ``watchdog.events.DirModifiedEvent`` or
                ``watchdog.events.FileModifiedEvent``.
        """
        if not self._event_error:
            self.logger.info(u"Change detected from an edit on: %s",
                             event.src_path)

            self.compile_dependencies(event.src_path)

    def on_deleted(self, event):
        """
        Called when a file or directory is deleted.

        Todo:
            May be bugged with inspector and sass compiler since the does not
            exists anymore.

        Args:
            event: Watchdog event, ``watchdog.events.DirDeletedEvent`` or
                ``watchdog.events.FileDeletedEvent``.
        """
        if not self._event_error:
            self.logger.info(u"Change detected from deletion of: %s",
                             event.src_path)
            # Never try to compile the deleted source
            self.compile_dependencies(event.src_path, include_self=False)
Пример #9
0
def compile_command(context, backend, config):
    """
    Compile Sass project sources to CSS
    """
    logger = logging.getLogger("boussole")
    logger.info(u"Building project")

    # Discover settings file
    try:
        discovering = Discover(backends=[SettingsBackendJson,
                                         SettingsBackendYaml])
        config_filepath, config_engine = discovering.search(
            filepath=config,
            basedir=os.getcwd(),
            kind=backend
        )

        project = ProjectBase(backend_name=config_engine._kind_name)
        settings = project.backend_engine.load(filepath=config_filepath)
    except BoussoleBaseException as e:
        logger.critical(six.text_type(e))
        raise click.Abort()

    logger.debug(u"Settings file: {} ({})".format(
                 config_filepath, config_engine._kind_name))
    logger.debug(u"Project sources directory: {}".format(
                 settings.SOURCES_PATH))
    logger.debug(u"Project destination directory: {}".format(
                 settings.TARGET_PATH))
    logger.debug(u"Exclude patterns: {}".format(
                 settings.EXCLUDES))

    # Find all sources with their destination path
    try:
        compilable_files = ScssFinder().mirror_sources(
            settings.SOURCES_PATH,
            targetdir=settings.TARGET_PATH,
            excludes=settings.EXCLUDES
        )
    except BoussoleBaseException as e:
        logger.error(six.text_type(e))
        raise click.Abort()

    # Build all compilable stylesheets
    compiler = SassCompileHelper()
    errors = 0
    for src, dst in compilable_files:
        logger.debug(u"Compile: {}".format(src))

        output_opts = {}
        success, message = compiler.safe_compile(settings, src, dst)

        if success:
            logger.info(u"Output: {}".format(message), **output_opts)
        else:
            errors += 1
            logger.error(message)

    # Ensure correct exit code if error has occured
    if errors:
        raise click.Abort()
Пример #10
0
def compiler():
    """Initialize and return SCSS compile helper"""
    return SassCompileHelper()