Source code for filehandler

# python 2
from __future__ import absolute_import, print_function, unicode_literals, with_statement
from builtins import str
from io import open

# builtins
from os import path, walk, getcwd
from glob import glob
import logging

# plugins
from cssutils import parseString, ser

# custom
from blowdrycss.utilities import get_file_path, make_directory
import blowdrycss_settings as settings

__author__ = 'chad nelson'
__project__ = 'blowdrycss'


[docs]class FileFinder(object): """ Designed to find all ``settings.files_types`` specified within a particular ``project_directory``. All folders within the ``project_directory`` are searched. | **Parameters:** | **recent** (*str*) -- Flag that indicates whether to gather the most recently modified files (True Case) or all eligible files (False Case). | **Members:** | **project_directory** (*str*) -- Set to settings.project_directory. | **files** (*str list*) -- List of all paths to all parsable files. | **file_dict** (*dict*) -- Dictionary of all paths to all parsable files where the file extension e.g. ``*.html`` is the key and the full file path is the value. **Example:** >>> file_finder = FileFinder(recent=False) >>> files = file_finder.files """ def __init__(self, recent=True): self.project_directory = settings.project_directory if path.isdir(self.project_directory): self.recent = recent self.files = [] self.set_files() self.file_dict = {} if self.recent: self.set_recent_file_dict() else: self.set_file_dict() logging.debug(msg='File Types:' + ', '.join(settings.file_types)) logging.debug(msg='Project Directory:' + str(self.project_directory)) logging.debug('\nProject Files Found:') self.print_collection(self.files) else: raise OSError( self.project_directory + ' is not a directory. Check project_directory setting in blowdrycss_settings.py' )
[docs] @staticmethod def print_collection(collection): """ Takes a list or tuple as input and prints each item. :type collection: iterable :param collection: A list or tu of unicode strings to be printed. :return: None """ for item in collection: logging.debug(str(item)) # Python 2 requires str(). logging.debug(' ') # Add a blank line
[docs] def set_files(self): """ Get all files associated with defined ``file_types`` in ``project_directory``. For each ``file_type`` find the full path to each file in the project directory of the current ``file_type``. Add the full path of each file found to the list ``self.files``. Reference: stackoverflow.com/questions/954504/how-to-get-files-in-a-directory-including-all-subdirectories#answer-954948 :return: None """ for directory, _, _ in walk(self.project_directory): for file_type in settings.file_types: self.files.extend(glob(path.join(directory, file_type)))
[docs] def set_file_dict(self): """ Filter and organize files by type in ``file_dict``. Dictionary Format: :: self.file_dict = { '.html': {'filepath_1.html', 'filepath_2.html', ..., 'filepath_n.html'}, '.aspx': {'filepath_1.aspx', 'filepath_2.aspx', ..., 'filepath_n.aspx'}, ... '.file_type': {'filepath_1.file_type', 'filepath_2.file_type', ..., 'filepath_n.file_type'}, } Automatically removes the * wildcard from ``file_type``. :return: None """ for file_type in settings.file_types: file_type = file_type.replace('*', '') # Remove the * wildcard. self.file_dict[file_type] = {_file for _file in self.files if path.splitext(_file)[1] == file_type}
[docs] def set_recent_file_dict(self): """ Filter and organize recent files by type in ``file_dict``. Meaning only files that are newer than the latest version of blowdry.css are added. Dictionary Format: :: self.file_dict = { '.html': {'filepath_1.html', 'filepath_2.html', ..., 'filepath_n.html'}, '.aspx': {'filepath_1.aspx', 'filepath_2.aspx', ..., 'filepath_n.aspx'}, ... '.file_type': {'filepath_1.file_type', 'filepath_2.file_type', ..., 'filepath_n.file_type'}, } Automatically removes the * wildcard from ``file_type``. :return: None """ comparator = FileModificationComparator() for file_type in settings.file_types: file_type = file_type.replace('*', '') # Remove the * wildcard. self.file_dict[file_type] = { _file for _file in self.files if path.splitext(_file)[1] == file_type and comparator.is_newer(_file) }
[docs]class FileConverter(object): """ Converts text files to strings. On initialization checks the existence of ``file_path``. **Example:** >>> from os import getcwd, chdir, path >>> # Valid file_path >>> current_dir = getcwd() >>> chdir('..') >>> file_path = path.join(current_dir, 'examplesite', 'index.html') >>> chdir(current_dir) # Change it back. >>> file_converter = FileConverter(file_path=file_path) >>> file_string = file_converter.get_file_as_string() >>> # >>> # Invalid file_path >>> file_converter = FileConverter(file_path='/not/valid/file.html') FileNotFoundError: file_path /not/valid/file.html does not exist. """ def __init__(self, file_path=''): if path.isfile(file_path): self.file_path = file_path else: raise OSError('file_path ' + file_path + ' does not exist.') # raise FileNotFoundError('file_path ' + file_path + ' does not exist.') # python 3 only
[docs] def get_file_as_string(self): """ Convert the _file to a string and return it. :return: (*str*) Return the _file as a string. **Example:** >>> from os import getcwd, chdir, path >>> current_dir = getcwd() >>> chdir('..') >>> file_path = path.join(current_dir, 'examplesite', 'index.html') >>> chdir(current_dir) # Change it back. >>> file_converter = FileConverter(file_path=file_path) >>> file_string = file_converter.get_file_as_string() """ with open(self.file_path, 'r') as _file: file_as_string = _file.read().replace('\n', '') return file_as_string
[docs]class CSSFile(object): """ A tool for writing and minifying CSS to files. *Reference:* stackoverflow.com/questions/273192/in-python-check-if-a-directory-exists-and-create-it-if-necessary#answer-14364249 | **Parameters:** | **css_directory** (*str*) -- File directory where the .css and .min.css output files are stored. | **Members:** | **file_name** (*str*) -- Defined in blowdrycss_settings.py as ``output_file_name``. Default is 'blowdry'. | **extension** (*str*) -- Defined in blowdrycss_settings.py as ``output_extension``. Default is '.css'. | *Note:* The output file is named ``file_name + extension`` or ``file_name + .min + extension``. ex1: blowdry.css or blowdry.min.css ex2: _custom.scss or _custom.min.scss **Example:** >>> from os import getcwd, chdir, path >>> current_dir = getcwd() >>> chdir('..') >>> project_directory = path.join(current_dir, 'examplesite') >>> css_directory = path.join(project_directory, 'css') >>> chdir(current_dir) # Change it back. >>> css_text = '.margin-top-50px { margin-top: 3.125em }' >>> css_file = CSSFile( >>> file_directory=css_directory, file_name='blowdry' >>> ) >>> css_file.write(css_text=css_text) >>> css_file.minify(css_text=css_text) """ def __init__(self): self.file_directory = settings.css_directory self.file_name = settings.output_file_name self.extension = settings.output_extension make_directory(self.file_directory)
[docs] def write(self, css_text=''): """ Output a human readable version of the css file in utf-8 format. **Notes:** - The file is human readable. It is not intended to be human editable as the file is auto-generated. - Pre-existing files with the same name are overwritten. :type css_text: str :param css_text: Text containing the CSS to be written to the file. :return: None **Example:** >>> css_text = '.margin-top-50px { margin-top: 3.125em }' >>> css_file = CSSFile() >>> css_file.write(css_text=css_text) """ parse_string = parseString(css_text) ser.prefs.useDefaults() # Enables Default / Verbose Mode file_path = get_file_path( file_directory=self.file_directory, file_name=self.file_name, extension=self.extension ) with open(file_path, 'w') as css_file: css_file.write(parse_string.cssText.decode('utf-8'))
[docs] def minify(self, css_text=''): """ Output a minified version of the css file in utf-8 format. **Definition:** The term minify "in the context of CSS means removing all unnecessary characters, such as spaces, new lines, comments without affecting the functionality of the source code." *Source:* https://www.jetbrains.com/phpstorm/help/minifying-css.html **Purpose:** | The purpose of minification is to increase web page load speed. | Reducing the size of the CSS file reduces the time spent downloading the CSS file and waiting for the page to load. **Notes:** - The file is minified and not human readable. - Pre-existing files with the same name are overwritten. - Uses the cssutils minification tool. **Important:** - ``ser.prefs.useMinified()`` is a global setting. It must be reset to ``ser.prefs.useDefaults()``. Otherwise, minification will continue to occur. This can result in strange behavior especially during unit testing or in code called after this method is called. :type css_text: str :param css_text: Text containing the CSS to be written to the file. :return: None **Example:** >>> css_text = '.margin-top-50px { margin-top: 3.125em }' >>> css_file = CSSFile() >>> css_file.minify(css_text=css_text) """ parse_string = parseString(css_text) ser.prefs.useMinified() # Enable minification. file_path = get_file_path( file_directory=self.file_directory, file_name=self.file_name, extension=str('.min' + self.extension) # prepend '.min' ) with open(file_path, 'w') as css_file: css_file.write(parse_string.cssText.decode('utf-8')) ser.prefs.useDefaults() # Disable minification.
[docs]class GenericFile(object): """ A tool for writing extension-independent files. **Reference:** stackoverflow.com/questions/273192/in-python-check-if-a-directory-exists-and-create-it-if-necessary#answer-14364249 | **Parameters:** | **file_directory** (*str*) -- File directory where the output files are saved / overwritten. | **file_name** (*str*) -- The name of the output file. Default is 'blowdry'. | **extension** (*str*) -- A file extension that begins with a ``.`` and only contains ``.``, ``0-9`` or ``a-z``. | *Notes:* - ``file_name`` does not include extension because ``write_file()`` normalizes and appends the extension. - ``extension`` is converted to lowercase. **Example:** >>> from os import getcwd, path >>> file_directory = path.join(getcwd()) >>> css_text = '.margin-top-50px { margin-top: 3.125em }' >>> markdown_file = GenericFile( >>> file_directory=file_directory, >>> file_name='blowdry', >>> extension='.md' >>> ) >>> text = '# blowdrycss' >>> markdown_file.write(text=text) """ def __init__(self, file_directory=getcwd(), file_name='', extension=''): self.file_directory = str(file_directory) self.file_name = str(file_name) self.file_path = get_file_path( file_directory=self.file_directory, file_name=self.file_name, extension=str(extension) ) make_directory(file_directory)
[docs] def write(self, text=''): """ Output a human readable version of the file in utf-8 format. Converts string to bytearray so that no new lines are added to the file. Note: Overwrites any pre-existing files with the same name. :raises TypeError: Raise a TypeError if ``text`` input is not of type ``str``. :type text: unicode or str :param text: The text to be written to the file. :return: None """ if type(text) is str: with open(self.file_path, 'wb') as generic_file: generic_file.write(bytearray(text, 'utf-8')) else: raise TypeError('In GenericFile.write() "' + text + '" input must be a str type.')
[docs]class FileModificationComparator(object): """ A Comparator that compares the last modified time of blowdry.css with the last modified time of another file. :return: None **Example** >>> import blowdrycss_settings as settings >>> from blowdrycss.filehandler import FileModificationComparator >>> file_age_comparator = FileModificationComparator() >>> print(file_age_comparator.is_newer(file_path=path.join(settings.project_directory, '/index.html')) """ def __init__(self): self.blowdrycss_file = path.join(settings.css_directory, 'blowdry.css') self.blowdrymincss_file = path.join(settings.css_directory, 'blowdry.min.css')
[docs] def is_newer(self, file_path): """ Detects if ``self.file_path`` was modified more recently than blowdry.css. If ``self.file_path`` is newer than blowdry.css or blowdry.min.css it returns True otherwise it returns false. If blowdry.css or blowdry.min.css do not exist, then the file under comparison is newer. :type file_path: str :param file_path: The full path to a file. :return: (*bool*) Returns True if modification time of blowdry.css or blowdry.min.css do not exist, or are older i.e. less than the ``self.file_path`` under consideration. """ if settings.human_readable: try: a = path.getmtime(self.blowdrycss_file) except OSError: # file doesn't exist return True elif settings.minify: try: a = path.getmtime(self.blowdrymincss_file) except OSError: # file doesn't exist return True else: raise SystemError( 'Review blowdrycss_settings.py. Either settings.human_readable or settings.minify must be set to True.' ) try: b = path.getmtime(file_path) except OSError: raise OSError('"' + file_path + '" does not exist.') return a <= b