Source code for boussole.finder

# -*- coding: utf-8 -*-
"""
.. _SASS partials Reference:
    http://sass-lang.com/documentation/file.SASS_REFERENCE.html#partials

Finder
======

Finder is in charge to find *main SASS stylesheets* files to compile to CSS
files, meaning it will ignore all partials SASS stylesheets (see
`SASS partials Reference`_).

"""
import fnmatch
import os

from boussole.exceptions import FinderException


[docs]def paths_by_depth(paths): """Sort list of paths by number of directories in it .. todo:: check if a final '/' is consistently given or ommitted. :param iterable paths: iterable containing paths (str) :rtype: list """ return sorted( paths, key=lambda path: path.count(os.path.sep), reverse=True )
[docs]class ScssFinder(object): """ Project finder for SCSS sources Attributes: FINDER_STYLESHEET_EXTS: List of file extensions regarded as compilable stylesheet sources. """ FINDER_STYLESHEET_EXTS = ['scss', ]
[docs] def get_relative_from_paths(self, filepath, paths): """ Find the relative filepath from the most relevant multiple paths. This is somewhat like a ``os.path.relpath(path[, start])`` but where ``start`` is a list. The most relevant item from ``paths`` will be used to apply the relative transform. Args: filepath (str): Path to transform to relative. paths (list): List of absolute paths to use to find and remove the start path from ``filepath`` argument. If there is multiple path starting with the same directories, the biggest will match. Raises: boussole.exception.FinderException: If no ``filepath`` start could be finded. Returns: str: Relative filepath where the start coming from ``paths`` is removed. """ for systempath in paths_by_depth(paths): if filepath.startswith(systempath): return os.path.relpath(filepath, systempath) raise FinderException("'Finder.get_relative_from_paths()' could not " "find filepath start from '{}'".format(filepath))
[docs] def is_partial(self, filepath): """ Check if file is a SASS partial source (see `SASS partials Reference`_). Args: filepath (str): A file path. Can be absolute, relative or just a filename. Returns: bool: True if file is a partial source, else False. """ path, filename = os.path.split(filepath) return filename.startswith('_')
[docs] def is_allowed(self, filepath, excludes=[]): """ Check from exclude patterns if a relative filepath is allowed Args: filepath (str): A relative file path. (exclude patterns are allways based from the source directory). Keyword Arguments: excludes (list): A list of excluding (glob) patterns. If filepath matchs one of patterns, filepath is not allowed. Raises: boussole.exception.FinderException: If given filepath is absolute. Returns: str: Filepath with new extension. """ if os.path.isabs(filepath): raise FinderException("'Finder.is_allowed()' only accept relative" " filepath") if excludes: for pattern in excludes: if fnmatch.fnmatch(filepath, pattern): return False return True
[docs] def match_conditions(self, filepath, sourcedir=None, nopartial=True, exclude_patterns=[], excluded_libdirs=[]): """ Find if a filepath match all required conditions. Available conditions are (in order): * Is allowed file extension; * Is a partial source; * Is from an excluded directory; * Is matching an exclude pattern; Args: filepath (str): Absolute filepath to match against conditions. Keyword Arguments: sourcedir (str or None): Absolute sources directory path. Can be ``None`` but then the exclude_patterns won't be matched against (because this method require to distinguish source dir from lib dirs). nopartial (bool): Accept partial sources if ``False``. Default is ``True`` (partial sources fail matchind condition). See ``Finder.is_partial()``. exclude_patterns (list): List of glob patterns, if filepath match one these pattern, it wont match conditions. See ``Finder.is_allowed()``. excluded_libdirs (list): A list of directory to match against filepath, if filepath starts with one them, it won't match condtions. Returns: bool: ``True`` if match all conditions, else ``False``. """ # Ensure libdirs ends with / to avoid missmatching with # 'startswith' usage excluded_libdirs = [os.path.join(d, "") for d in excluded_libdirs] # Match an filename extension admitted as compilable stylesheet filename, ext = os.path.splitext(filepath) ext = ext[1:] if ext not in self.FINDER_STYLESHEET_EXTS: return False # Not a partial source if nopartial and self.is_partial(filepath): return False # Not in an excluded directory if any( filepath.startswith(excluded_path) for excluded_path in paths_by_depth(excluded_libdirs) ): return False # Not matching an exclude pattern if sourcedir and exclude_patterns: candidates = [sourcedir]+excluded_libdirs relative_path = self.get_relative_from_paths(filepath, candidates) if not self.is_allowed(relative_path, excludes=exclude_patterns): return False return True
[docs] def change_extension(self, filepath, new_extension): """ Change final filename extension. Args: filepath (str): A file path (relative or absolute). new_extension (str): New extension name (without leading dot) to apply. Returns: str: Filepath with new extension. """ filename, ext = os.path.splitext(filepath) return '.'.join([filename, new_extension])
[docs] def get_destination(self, filepath, targetdir=None): """ Return destination path from given source file path. Destination is allways a file with extension ``.css``. Args: filepath (str): A file path. The path is allways relative to sources directory. If not relative, ``targetdir`` won't be joined. absolute (bool): If given will be added at beginning of file path. Returns: str: Destination filepath. """ dst = self.change_extension(filepath, 'css') if targetdir: dst = os.path.join(targetdir, dst) return dst
[docs] def compilable_sources(self, sourcedir, absolute=False, recursive=True, excludes=[]): """ Find all scss sources that should be compiled, aka all sources that are not "partials" SASS sources. Args: sourcedir (str): Directory path to scan. Keyword Arguments: absolute (bool): Returned paths will be absolute using ``sourcedir`` argument (if True), else return relative paths. recursive (bool): Switch to enabled recursive finding (if True). Default to True. excludes (list): A list of excluding patterns (glob patterns). Patterns are matched against the relative filepath (from its sourcedir). Returns: list: List of source paths. """ filepaths = [] for root, dirs, files in os.walk(sourcedir): # Sort structure to avoid arbitrary order dirs.sort() files.sort() for item in files: # Store relative directory but drop it if at root ('.') relative_dir = os.path.relpath(root, sourcedir) if relative_dir == '.': relative_dir = '' # Matching all conditions absolute_filepath = os.path.join(root, item) conditions = { 'sourcedir': sourcedir, 'nopartial': True, 'exclude_patterns': excludes, 'excluded_libdirs': [], } if self.match_conditions(absolute_filepath, **conditions): relative_filepath = os.path.join(relative_dir, item) if absolute: filepath = absolute_filepath else: filepath = relative_filepath filepaths.append(filepath) # For non recursive usage, break from the first entry if not recursive: break return filepaths
[docs] def mirror_sources(self, sourcedir, targetdir=None, recursive=True, excludes=[]): """ Mirroring compilable sources filepaths to their targets. Args: sourcedir (str): Directory path to scan. Keyword Arguments: absolute (bool): Returned paths will be absolute using ``sourcedir`` argument (if True), else return relative paths. recursive (bool): Switch to enabled recursive finding (if True). Default to True. excludes (list): A list of excluding patterns (glob patterns). Patterns are matched against the relative filepath (from its sourcedir). Returns: list: A list of pairs ``(source, target)``. Where ``target`` is the ``source`` path but renamed with ``.css`` extension. Relative directory from source dir is left unchanged but if given, returned paths will be absolute (using ``sourcedir`` for sources and ``targetdir`` for targets). """ sources = self.compilable_sources( sourcedir, absolute=False, recursive=recursive, excludes=excludes ) maplist = [] for filepath in sources: src = filepath dst = self.get_destination(src, targetdir=targetdir) # In absolute mode if targetdir: src = os.path.join(sourcedir, src) maplist.append((src, dst)) return maplist