Source code for boussole.inspector

# -*- coding: utf-8 -*-
"""
Inspector
=========

Inspector is in charge to inspect a project about Sass stylesheets to search
for their dependencies.
"""
import io
import os

from collections import defaultdict

from .exceptions import CircularImport
from .parser import ScssImportsParser, SassImportsParser
from .resolver import ImportPathsResolver


[docs]class ScssInspector(ImportPathsResolver, ScssImportsParser): """ Project inspector for SCSS sources Inspector is stateful, meaning you will need to invoke ``reset()`` then ``inspect()`` each time a project change, else the parents and children maps will be eventually incorrects. ``__init__`` method use ``reset`` method to initialize some internal buffers. Attributes: _CHILDREN_MAP: Dictionnary of finded direct children for each inspected sources. _PARENTS_MAP: Dictionnary of finded direct parents for each inspected sources. parsers: Dictionnary of available Sass format parsers, where key is the file extension related to the format and value is a parser instance. """ parsers = { "scss": ScssImportsParser(), "sass": SassImportsParser(), } def __init__(self, *args, **kwargs): self.reset()
[docs] def get_parser(self, path): """ Returns parser depending of extension from given file path. Arguments: path (str): Path to split to find extension which will select the right parser. If file path does not have any extension or does not match any enabled extension, ``scss`` will be assumed on default. Returns: parser: Either ``ScssImportsParser`` or ``SassImportsParser`` instance depending of filepath. """ filepath, ext = os.path.splitext(path) # Remove leading dot from splitext to be able to match to # ``self.parsers`` keys if ext and ext.startswith("."): ext = ext[1:] if not ext or ext not in self.parsers: ext = "scss" return self.parsers.get(ext)
[docs] def reset(self): """ Reset internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``. """ self._CHILDREN_MAP = {} self._PARENTS_MAP = defaultdict(set)
[docs] def look_source(self, sourcepath, library_paths=None): """ Open a SCSS file (sourcepath) and find all involved files from import rules. This will fill internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``. Arguments: sourcepath (str): Source file path to start searching for imports. Keyword Arguments: library_paths (list): List of directory paths for libraries to resolve paths if resolving fails on the base source path. Default to None. """ # Don't inspect again source that has allready be inspected as a # children of a previous source if sourcepath not in self._CHILDREN_MAP: parser = self.get_parser(sourcepath) with io.open(sourcepath, "r", encoding="utf-8") as fp: finded_paths = parser.parse(fp.read()) children = self.resolve(sourcepath, finded_paths, library_paths=library_paths) # Those files that are imported by the sourcepath self._CHILDREN_MAP[sourcepath] = children # Those files that import the sourcepath for p in children: self._PARENTS_MAP[p].add(sourcepath) # Start recursive finding through each resolved path that has not # been collected yet for path in children: if path not in self._CHILDREN_MAP: self.look_source(path, library_paths=library_paths) return
[docs] def inspect(self, *args, **kwargs): """ Recursively inspect all given SCSS files to find imported dependencies. This does not return anything. Just fill internal buffers about inspected files. Note: This will ignore orphan files (files that are not imported from any of given SCSS files). Arguments: *args: One or multiple arguments, each one for a source file path to inspect. Keyword Arguments: library_paths (list): List of directory paths for libraries to resolve paths if resolving fails on the base source path. Default to None. """ library_paths = kwargs.get("library_paths", None) for sourcepath in args: self.look_source(sourcepath, library_paths=library_paths)
[docs] def _get_recursive_dependancies(self, dependencies_map, sourcepath, recursive=True): """ Return all dependencies of a source, recursively searching through its dependencies. This is a common method used by ``children`` and ``parents`` methods. Arguments: dependencies_map (dict): Internal buffer (internal buffers ``_CHILDREN_MAP`` or ``_PARENTS_MAP``) to use for searching. sourcepath (str): Source file path to start searching for dependencies. Keyword Arguments: recursive (bool): Switch to enable recursive finding (if True). Default to True. Raises: CircularImport: If circular error is detected from a source. Returns: set: List of dependencies paths. """ # Direct dependencies collected = set([]) collected.update(dependencies_map.get(sourcepath, [])) # Sequence of 'dependencies_map' items to explore sequence = collected.copy() # Exploration list walkthrough = [] # Recursive search starting from direct dependencies if recursive: while True: if not sequence: break item = sequence.pop() # Add current source to the explorated source list walkthrough.append(item) # Current item children current_item_dependancies = dependencies_map.get(item, []) for dependency in current_item_dependancies: # Allready visited item, ignore and continue to the new # item if dependency in walkthrough: continue # Unvisited item yet, add its children to dependencies and # item to explore else: collected.add(dependency) sequence.add(dependency) # Sourcepath has allready been visited but present itself # again, assume it's a circular import if sourcepath in walkthrough: msg = "A circular import has occured by '{}'" raise CircularImport(msg.format(current_item_dependancies)) # No more item to explore, break loop if not sequence: break return collected
[docs] def children(self, sourcepath, recursive=True): """ Recursively find all children that are imported from the given source path. Arguments: sourcepath (str): Source file path to search for. Keyword Arguments: recursive (bool): Switch to enabled recursive finding (if True). Default to True. Returns: set: List of finded parents path. """ return self._get_recursive_dependancies( self._CHILDREN_MAP, sourcepath, recursive=True )
[docs] def parents(self, sourcepath, recursive=True): """ Recursively find all parents that import the given source path. Arguments: sourcepath (str): Source file path to search for. Keyword Arguments: recursive (bool): Switch to enabled recursive finding (if True). Default to True. Returns: set: List of finded parents path. """ return self._get_recursive_dependancies( self._PARENTS_MAP, sourcepath, recursive=True )