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 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()
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 finder(): """Initialize and return SCSS finder (scope at module level)""" return ScssFinder()
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)
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
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)
def finder(): """Initialize and return SCSS finder""" return ScssFinder()