@@ -8,18 +8,39 @@
 8 8 `# -----------------------------------------------------------------------------` 9 9 `"""Libsass functions."""` 10 10 11 + `# yapf: disable` 12 + 11 13 `# Third party imports` 12 14 `import sass` 13 15 14 16 17 + `# yapf: enable` 18 + 19 + 15 20 `def rgba(r, g, b, a):` 21 + ` """Convert r,g,b,a values to standard format.` 22 + 23 + ` Where `a` is alpha! In CSS alpha can be given as:` 24 + ` * float from 0.0 (fully transparent) to 1.0 (opaque)` 25 + ` In Qt or qss that is:` 26 + ` * int from 0 (fully transparent) to 255 (opaque)` 27 + ` A percentage value 0% (fully transparent) to 100% (opaque) works` 28 + ` in BOTH systems the same way!` 29 + ` """` 16 30 ` result = 'rgba({}, {}, {}, {}%)'` 17 31 ` if isinstance(r, sass.SassNumber):` 32 + ` if a.unit == '%':` 33 + ` alpha = a.value` 34 + ` elif a.value > 1.0:` 35 + ` # A value from 0 to 255 is coming in, convert to %` 36 + ` alpha = a.value / 2.55` 37 + ` else:` 38 + ` alpha = a.value * 100` 18 39 ` return result.format(` 19 40 ` int(r.value),` 20 41 ` int(g.value),` 21 42 ` int(b.value),` 22 - ` int(a.value * 100)` 43 + ` int(alpha),` 23 44 ` )` 24 45 ` elif isinstance(r, float):` 25 46 ` return result.format(int(r), int(g), int(b), int(a * 100))`
@@ -27,16 +48,20 @@
 27 48 28 49 `def rgba_from_color(color):` 29 50 ` """` 30 - ` Conform rgba` 51 + ` Conform rgba.` 31 52 32 53 ` :type color: sass.SassColor` 33 54 ` """` 55 + ` # Inner rgba() call` 56 + ` if not isinstance(color, sass.SassColor):` 57 + ` return '{}'.format(color)` 58 + 34 59 ` return rgba(color.r, color.g, color.b, color.a)` 35 60 36 61 37 62 `def qlineargradient(x1, y1, x2, y2, stops):` 38 63 ` """` 39 - ` Implementation of qss qlineargradient function for scss.` 64 + ` Implement qss qlineargradient function for scss.` 40 65 41 66 ` :type x1: sass.SassNumber` 42 67 ` :type y1: sass.SassNumber`
@@ -45,15 +70,13 @@
 45 70 ` :type stops: sass.SassList` 46 71 ` :return:` 47 72 ` """` 48 - ` stops_str = ''` 73 + ` stops_str = []` 49 74 ` for stop in stops[0]:` 50 75 ` pos, color = stop[0]` 51 - ` stops_str += ' stop: {} {}'.format(pos.value, rgba_from_color(color))` 52 - 53 - ` return 'qlineargradient(x1: {}, y1: {}, x2: {}, y2: {},{})'.format(` 54 - ` x1.value,` 55 - ` y1.value,` 56 - ` x2.value,` 57 - ` y2.value,` 58 - ` stops_str.rstrip(',')` 59 - ` )` 76 + ` stops_str.append('stop: {} {}'.format(` 77 + ` pos.value,` 78 + ` rgba_from_color(color),` 79 + ` ))` 80 + ` template = 'qlineargradient(x1: {}, y1: {}, x2: {}, y2: {}, {})'` 81 + ` return template.format(x1.value, y1.value, x2.value, y2.value,` 82 + ` ', '.join(stops_str))`

@@ -9,59 +9,100 @@
 9 9 `# -----------------------------------------------------------------------------` 10 10 `"""qtsass command line interface."""` 11 11 12 - `# Standard library imports` 12 + `# yapf: disable` 13 + 13 14 `from __future__ import absolute_import, print_function` 15 + 16 + `# Standard library imports` 14 17 `import argparse` 18 + `import logging` 15 19 `import os` 16 20 `import sys` 17 21 `import time` 18 - `import logging` 19 - `import signal` 20 22 21 23 `# Local imports` 22 - `from qtsass.api import compile, compile_filename, compile_dirname, watch` 24 + `from qtsass.api import (` 25 + ` compile,` 26 + ` compile_dirname,` 27 + ` compile_filename,` 28 + ` enable_logging,` 29 + ` watch,` 30 + `)` 31 + 23 32 33 + `# yapf: enable` 24 34 25 - `logging.basicConfig(level=logging.DEBUG)` 26 35 `_log = logging.getLogger(__name__)` 27 36 28 37 29 38 `def create_parser():` 30 39 ` """Create qtsass's cli parser."""` 31 - 32 40 ` parser = argparse.ArgumentParser(` 33 41 ` prog='QtSASS',` 34 42 ` description='Compile a Qt compliant CSS file from a SASS stylesheet.',` 35 43 ` )` 36 - ` parser.add_argument('input', type=str, help='The SASS stylesheet file.')` 44 + ` parser.add_argument(` 45 + ` 'input',` 46 + ` type=str,` 47 + ` help='The SASS stylesheet file.',` 48 + ` )` 37 49 ` parser.add_argument(` 38 50 ` '-o',` 39 51 ` '--output',` 40 52 ` type=str,` 41 - ` help='The path of the generated Qt compliant CSS file.'` 53 + ` help='The path of the generated Qt compliant CSS file.',` 42 54 ` )` 43 55 ` parser.add_argument(` 44 56 ` '-w',` 45 57 ` '--watch',` 46 58 ` action='store_true',` 47 - ` help='If set, recompile when the source file changes.'` 59 + ` help='If set, recompile when the source file changes.',` 60 + ` )` 61 + ` parser.add_argument(` 62 + ` '-d',` 63 + ` '--debug',` 64 + ` action='store_true',` 65 + ` help='Set the logging level to DEBUG.',` 48 66 ` )` 49 67 ` return parser` 50 68 51 69 52 - `def main(args):` 53 - ` """qtsass's cli entry point."""` 70 + `def main():` 71 + ` """CLI entry point."""` 72 + ` args = create_parser().parse_args()` 73 + 74 + ` # Setup CLI logging` 75 + ` debug = os.environ.get('QTSASS_DEBUG', args.debug)` 76 + ` if debug in ('1', 'true', 'True', 'TRUE', 'on', 'On', 'ON', True):` 77 + ` level = logging.DEBUG` 78 + ` else:` 79 + ` level = logging.INFO` 80 + ` enable_logging(level)` 81 + 82 + ` # Add a StreamHandler` 83 + ` handler = logging.StreamHandler()` 84 + ` if level == logging.DEBUG:` 85 + ` fmt = '%(levelname)-8s: %(name)s> %(message)s'` 86 + ` handler.setFormatter(logging.Formatter(fmt))` 87 + ` logging.root.addHandler(handler)` 88 + ` logging.root.setLevel(level)` 54 89 55 - ` args = create_parser().parse_args(args)` 56 90 ` file_mode = os.path.isfile(args.input)` 57 91 ` dir_mode = os.path.isdir(args.input)` 58 92 59 93 ` if file_mode and not args.output:` 60 - ` css = compile(args.input)` 94 + ` with open(args.input, 'r') as f:` 95 + ` string = f.read()` 96 + 97 + ` css = compile(` 98 + ` string,` 99 + ` include_paths=os.path.abspath(os.path.dirname(args.input)),` 100 + ` )` 61 101 ` print(css)` 62 102 ` sys.exit(0)` 63 103 64 104 ` elif file_mode:` 105 + ` _log.debug('compile_filename({}, {})'.format(args.input, args.output))` 65 106 ` compile_filename(args.input, args.output)` 66 107 67 108 ` elif dir_mode and not args.output:`
@@ -69,6 +110,7 @@
 69 110 ` sys.exit(1)` 70 111 71 112 ` elif dir_mode:` 113 + ` _log.debug('compile_dirname({}, {})'.format(args.input, args.output))` 72 114 ` compile_dirname(args.input, args.output)` 73 115 74 116 ` else:`
@@ -77,12 +119,13 @@
 77 119 78 120 ` if args.watch:` 79 121 ` _log.info('qtsass is watching {}...'.format(args.input))` 80 - ` observer = watch(args.input, args.output)` 81 - ` observer.start()` 122 + 123 + ` watcher = watch(args.input, args.output)` 124 + ` watcher.start()` 82 125 ` try:` 83 126 ` while True:` 84 - ` time.sleep(1)` 127 + ` time.sleep(0.5)` 85 128 ` except KeyboardInterrupt:` 86 - ` observer.stop()` 87 - ` observer.join()` 129 + ` watcher.stop()` 130 + ` watcher.join()` 88 131 ` sys.exit(0)`

@@ -8,28 +8,36 @@
 8 8 `# -----------------------------------------------------------------------------` 9 9 `"""Libsass importers."""` 10 10 11 - `# Standard library imports` 11 + `# yapf: disable` 12 + 12 13 `from __future__ import absolute_import` 14 + 15 + `# Standard library imports` 13 16 `import os` 14 17 15 18 `# Local imports` 16 19 `from qtsass.conformers import scss_conform` 17 20 18 21 22 + `# yapf: enable` 23 + 24 + 19 25 `def norm_path(*parts):` 20 - ` return os.path.normpath(os.path.join(*parts))` 26 + ` """Normalize path."""` 27 + ` return os.path.normpath(os.path.join(*parts)).replace('\\', '/')` 21 28 22 29 23 - `def qss_importer(where):` 30 + `def qss_importer(*include_paths):` 24 31 ` """` 25 - ` Returns a function which conforms imported qss files to valid scss to be` 26 - ` used as an importer for sass.compile.` 32 + ` Return function which conforms imported qss files to valid scss.` 33 + 34 + ` This fucntion is to be used as an importer for sass.compile.` 27 35 28 - ` :param where: Directory containing scss, css, and sass files` 36 + ` :param include_paths: Directorys containing scss, css, and sass files.` 29 37 ` """` 38 + ` include_paths` 30 39 31 40 ` def find_file(import_file):` 32 - 33 41 ` # Create partial import filename` 34 42 ` dirname, basename = os.path.split(import_file)` 35 43 ` if dirname:`
@@ -44,8 +52,9 @@
 44 52 ` partial_name = import_partial_file + ext` 45 53 ` potential_files.append(full_name)` 46 54 ` potential_files.append(partial_name)` 47 - ` potential_files.append(norm_path(where, full_name))` 48 - ` potential_files.append(norm_path(where, partial_name))` 55 + ` for path in include_paths:` 56 + ` potential_files.append(norm_path(path, full_name))` 57 + ` potential_files.append(norm_path(path, partial_name))` 49 58 50 59 ` # Return first existing potential file` 51 60 ` for potential_file in potential_files:`
@@ -55,7 +64,7 @@
 55 64 ` return None` 56 65 57 66 ` def import_and_conform_file(import_file):` 58 - 67 + ` """Return base file and conformed scss file."""` 59 68 ` real_import_file = find_file(import_file)` 60 69 ` with open(real_import_file, 'r') as f:` 61 70 ` import_str = f.read()`

@@ -0,0 +1,31 @@
 1 + `# -*- coding: utf-8 -*-` 2 + `# -----------------------------------------------------------------------------` 3 + `# Copyright (c) 2015 Yann Lanthony` 4 + `# Copyright (c) 2017-2018 Spyder Project Contributors` 5 + `#` 6 + `# Licensed under the terms of the MIT License` 7 + `# (See LICENSE.txt for details)` 8 + `# -----------------------------------------------------------------------------` 9 + `"""The qtsass Watcher is responsible for watching and recompiling sass.` 10 + 11 + `The default Watcher is the QtWatcher. If Qt is unavailable we fallback to the` 12 + `PollingWatcher.` 13 + `"""` 14 + 15 + `# yapf: disable` 16 + 17 + `from __future__ import absolute_import` 18 + 19 + `# Local imports` 20 + `from qtsass.watchers.polling import PollingWatcher` 21 + 22 + 23 + `try:` 24 + ` from qtsass.watchers.qt import QtWatcher` 25 + `except ImportError:` 26 + ` QtWatcher = None` 27 + 28 + 29 + `# yapf: enable` 30 + 31 + `Watcher = QtWatcher or PollingWatcher`

@@ -0,0 +1,96 @@
 1 + `# -*- coding: utf-8 -*-` 2 + `# -----------------------------------------------------------------------------` 3 + `# Copyright (c) 2015 Yann Lanthony` 4 + `# Copyright (c) 2017-2018 Spyder Project Contributors` 5 + `#` 6 + `# Licensed under the terms of the MIT License` 7 + `# (See LICENSE.txt for details)` 8 + `# -----------------------------------------------------------------------------` 9 + `"""Contains the Qt implementation of the Watcher api."""` 10 + 11 + `# yapf: disable` 12 + 13 + `from __future__ import absolute_import` 14 + 15 + `# Local imports` 16 + `from qtsass.watchers.polling import PollingWatcher` 17 + 18 + 19 + `# We cascade through Qt bindings here rather than relying on a comprehensive` 20 + `# Qt compatability library like qtpy or Qt.py. This prevents us from forcing a` 21 + `# specific compatability library on users.` 22 + `QT_BINDING = None` 23 + `if not QT_BINDING:` 24 + ` try:` 25 + ` from PySide2.QtWidgets import QApplication` 26 + ` from PySide2.QtCore import QObject, Signal` 27 + ` QT_BINDING = 'pyside2'` 28 + ` except ImportError:` 29 + ` pass` 30 + `if not QT_BINDING:` 31 + ` try:` 32 + ` from PyQt5.QtWidgets import QApplication` 33 + ` from PyQt5.QtCore import QObject` 34 + ` from PyQt5.QtCore import pyqtSignal as Signal` 35 + ` QT_BINDING = 'pyqt5'` 36 + ` except ImportError:` 37 + ` pass` 38 + `if not QT_BINDING:` 39 + ` try:` 40 + ` from PySide.QtGui import QApplication` 41 + ` from PySide2.QtCore import QObject, Signal` 42 + ` QT_BINDING == 'pyside'` 43 + ` except ImportError:` 44 + ` pass` 45 + `if not QT_BINDING:` 46 + ` from PyQt4.QtGui import QApplication` 47 + ` from PyQt4.QtCore import QObject` 48 + ` from PyQt4.QtCore import pyqtSignal as Signal` 49 + ` QT_BINDING == 'pyqt4'` 50 + 51 + 52 + `# yapf: enable` 53 + 54 + 55 + `class QtDispatcher(QObject):` 56 + ` """Used by QtWatcher to dispatch callbacks in the main ui thread."""` 57 + 58 + ` signal = Signal()` 59 + 60 + 61 + `class QtWatcher(PollingWatcher):` 62 + ` """The Qt implementation of the Watcher api.` 63 + 64 + ` Subclasses PollingWatcher but dispatches :meth:`compile_and_dispatch`` 65 + ` using a Qt Signal to ensure that these calls are executed in the main ui` 66 + ` thread. We aren't using a QFileSystemWatcher because it fails to report` 67 + ` changes in certain circumstances.` 68 + ` """` 69 + 70 + ` _qt_binding = QT_BINDING` 71 + 72 + ` def setup(self):` 73 + ` """Set up QtWatcher."""` 74 + ` super(QtWatcher, self).setup()` 75 + ` self._qtdispatcher = None` 76 + 77 + ` @property` 78 + ` def qtdispatcher(self):` 79 + ` """Get the QtDispatcher."""` 80 + ` if self._qtdispatcher is None:` 81 + ` self._qtdispatcher = QtDispatcher()` 82 + ` self._qtdispatcher.signal.connect(self.compile_and_dispatch)` 83 + ` return self._qtdispatcher` 84 + 85 + ` def on_change(self):` 86 + ` """Call when a change is detected."""` 87 + ` self._log.debug('Change detected...')` 88 + 89 + ` # If a QApplication event loop has not been started` 90 + ` # call compile_and_dispatch in the current thread.` 91 + ` if not QApplication.instance():` 92 + ` return super(PollingWatcher, self).compile_and_dispatch()` 93 + 94 + ` # Create and use a QtDispatcher to ensure compile and any` 95 + ` # connected callbacks get executed in the main gui thread.` 96 + ` self.qtdispatcher.signal.emit()`

@@ -0,0 +1,126 @@
 1 + `# -*- coding: utf-8 -*-` 2 + `# -----------------------------------------------------------------------------` 3 + `# Copyright (c) 2015 Yann Lanthony` 4 + `# Copyright (c) 2017-2018 Spyder Project Contributors` 5 + `#` 6 + `# Licensed under the terms of the MIT License` 7 + `# (See LICENSE.txt for details)` 8 + `# -----------------------------------------------------------------------------` 9 + `"""Contains the fallback implementation of the Watcher api."""` 10 + 11 + `# yapf: disable` 12 + 13 + `from __future__ import absolute_import, print_function` 14 + 15 + `# Standard library imports` 16 + `import atexit` 17 + `import threading` 18 + 19 + `# Local imports` 20 + `from qtsass.watchers import snapshots` 21 + `from qtsass.watchers.api import Watcher` 22 + 23 + 24 + `# yapf: enable` 25 + 26 + 27 + `class PollingThread(threading.Thread):` 28 + ` """A thread that fires a callback at an interval."""` 29 + 30 + ` def __init__(self, callback, interval):` 31 + ` """Initialize the thread.` 32 + 33 + ` :param callback: Callback function to repeat.` 34 + ` :param interval: Number of seconds to sleep between calls.` 35 + ` """` 36 + ` super(PollingThread, self).__init__()` 37 + ` self.daemon = True` 38 + ` self.callback = callback` 39 + ` self.interval = interval` 40 + ` self._shutdown = threading.Event()` 41 + ` self._stopped = threading.Event()` 42 + ` self._started = threading.Event()` 43 + ` atexit.register(self.stop)` 44 + 45 + ` @property` 46 + ` def started(self):` 47 + ` """Check if the thread has started."""` 48 + ` return self._started.is_set()` 49 + 50 + ` @property` 51 + ` def stopped(self):` 52 + ` """Check if the thread has stopped."""` 53 + ` return self._stopped.is_set()` 54 + 55 + ` @property` 56 + ` def shutdown(self):` 57 + ` """Check if the thread has shutdown."""` 58 + ` return self._shutdown.is_set()` 59 + 60 + ` def stop(self):` 61 + ` """Set the shutdown event for this thread and wait for it to stop."""` 62 + ` if not self.started and not self.shutdown:` 63 + ` return` 64 + 65 + ` self._shutdown.set()` 66 + ` self._stopped.wait()` 67 + 68 + ` def run(self):` 69 + ` """Threads main loop."""` 70 + ` try:` 71 + ` self._started.set()` 72 + 73 + ` while True:` 74 + ` self.callback()` 75 + ` if self._shutdown.wait(self.interval):` 76 + ` break` 77 + 78 + ` finally:` 79 + ` self._stopped.set()` 80 + 81 + 82 + `class PollingWatcher(Watcher):` 83 + ` """Polls a directory recursively for changes.` 84 + 85 + ` Detects file and directory changes, deletions, and creations. Recursion` 86 + ` depth is limited to 2 levels. We use a limit because the scss file we're` 87 + ` watching for changes could be sitting in the root of a project rather than` 88 + ` a dedicated scss directory. That could lead to snapshots taking too long` 89 + ` to build and diff. It's probably safe to assume that users aren't nesting` 90 + ` scss deeper than a couple of levels.` 91 + ` """` 92 + 93 + ` def setup(self):` 94 + ` """Set up the PollingWatcher.` 95 + 96 + ` A PollingThread is created but not started.` 97 + ` """` 98 + ` self._snapshot_depth = 2` 99 + ` self._snapshot = snapshots.take(self._watch_dir, self._snapshot_depth)` 100 + ` self._thread = PollingThread(self.run, interval=1)` 101 + 102 + ` def start(self):` 103 + ` """Start the PollingThread."""` 104 + ` self._thread.start()` 105 + 106 + ` def stop(self):` 107 + ` """Stop the PollingThread."""` 108 + ` self._thread.stop()` 109 + 110 + ` def join(self):` 111 + ` """Wait for the PollingThread to finish.` 112 + 113 + ` You should always call stop before join.` 114 + ` """` 115 + ` self._thread.join()` 116 + 117 + ` def run(self):` 118 + ` """Take a new snapshot and call on_change when a change is detected.` 119 + 120 + ` Called repeatedly by the PollingThread.` 121 + ` """` 122 + ` next_snapshot = snapshots.take(self._watch_dir, self._snapshot_depth)` 123 + ` changes = snapshots.diff(self._snapshot, next_snapshot)` 124 + ` if changes:` 125 + ` self._snapshot = next_snapshot` 126 + ` self.on_change()`

@@ -8,22 +8,28 @@
 8 8 `# -----------------------------------------------------------------------------` 9 9 `"""Conform qss to compliant scss and css to valid qss."""` 10 10 11 - `# Standard library imports` 11 + `# yapf: disable` 12 + 12 13 `from __future__ import absolute_import, print_function` 14 + 15 + `# Standard library imports` 13 16 `import re` 14 17 15 18 19 + `# yapf: enable` 20 + 21 + `_DEFAULT_COORDS = ('x1', 'y1', 'x2', 'y2')` 22 + 23 + 16 24 `class Conformer(object):` 17 25 ` """Base class for all text transformations."""` 18 26 19 27 ` def to_scss(self, qss):` 20 28 ` """Transform some qss to valid scss."""` 21 - 22 29 ` return NotImplemented` 23 30 24 31 ` def to_qss(self, css):` 25 32 ` """Transform some css to valid qss."""` 26 - 27 33 ` return NotImplemented` 28 34 29 35
@@ -31,13 +37,11 @@
 31 37 ` """Conform QSS "!" in selectors."""` 32 38 33 39 ` def to_scss(self, qss):` 34 - ` """Replaces "!" in selectors with "_qnot_"."""` 35 - 40 + ` """Replace "!" in selectors with "_qnot_"."""` 36 41 ` return qss.replace(':!', ':_qnot_')` 37 42 38 43 ` def to_qss(self, css):` 39 - ` """Replaces "_qnot_" in selectors with "!"."""` 40 - 44 + ` """Replace "_qnot_" in selectors with "!"."""` 41 45 ` return css.replace(':_qnot_', ':!')` 42 46 43 47
@@ -45,25 +49,55 @@
 45 49 ` """Conform QSS qlineargradient function."""` 46 50 47 51 ` qss_pattern = re.compile(` 48 - ` 'qlineargradient\('` 49 - ` '((?:(?:\s+)?(?:x1|y1|x2|y2):(?:\s+)?[0-9A-Za-z\$_-]+,?)+)' # coords` 50 - ` '((?:(?:\s+)?stop:.*,?)+(?:\s+)?)?' # stops` 51 - ` '\)',` 52 - ` re.MULTILINE` 52 + ` r'qlineargradient\('` 53 + ` r'((?:(?:\s+)?(?:x1|y1|x2|y2):(?:\s+)?[0-9A-Za-z\$_\.-]+,?)+)' # coords` 54 + ` r'((?:(?:\s+)?stop:.*,?)+(?:\s+)?)?' # stops` 55 + ` r'\)',` 56 + ` re.MULTILINE,` 53 57 ` )` 54 58 55 - ` def _conform_group_to_scss(self, group):` 59 + ` def _conform_coords_to_scss(self, group):` 56 60 ` """` 57 - ` Takes a qss str containing xy coords or stops and returns a str` 58 - ` containing just the values.` 61 + ` Take a qss str with xy coords and returns the values.` 59 62 60 - ` 'x1: 0, y1: 0, x2: 0, y2: 0' => '0, 0, 0, 0'` 61 - ` 'stop: 0 red, stop: 1 blue' => '0 red, 1 blue'` 63 + ` 'x1: 0, y1: 0, x2: 0, y2: 0' => '0, 0, 0, 0'` 64 + ` 'y1: 1' => '0, 1, 0, 0'` 65 + ` """` 66 + ` values = ['0', '0', '0', '0']` 67 + ` for key_values in [part.split(':', 1) for part in group.split(',')]:` 68 + ` try:` 69 + ` key, value = key_values` 70 + ` key = key.strip()` 71 + ` if key in _DEFAULT_COORDS:` 72 + ` pos = _DEFAULT_COORDS.index(key)` 73 + ` if pos >= 0 and pos <= 3:` 74 + ` values[pos] = value.strip()` 75 + ` except ValueError:` 76 + ` pass` 77 + ` return ', '.join(values)` 78 + 79 + ` def _conform_stops_to_scss(self, group):` 80 + ` """` 81 + ` Take a qss str with stops and returns the values.` 82 + 83 + ` 'stop: 0 red, stop: 1 blue' => '0 red, 1 blue'` 62 84 ` """` 63 85 ` new_group = []` 64 - ` for part in group.strip().split(','):` 86 + ` split = [""]` 87 + ` bracket_level = 0` 88 + ` for char in group:` 89 + ` if not bracket_level and char == ",":` 90 + ` split.append("")` 91 + ` continue` 92 + ` elif char == "(":` 93 + ` bracket_level += 1` 94 + ` elif char == ")":` 95 + ` bracket_level -= 1` 96 + ` split[-1] += char` 97 + 98 + ` for part in split:` 65 99 ` if part:` 66 - ` _, value = part.split(':')` 100 + ` _, value = part.split(':', 1)` 67 101 ` new_group.append(value.strip())` 68 102 ` return ', '.join(new_group)` 69 103
@@ -71,31 +105,28 @@
 71 105 ` """` 72 106 ` Conform qss qlineargradient to scss qlineargradient form.` 73 107 74 - ` Normalizes all whitespace including the removal of newline chars.` 108 + ` Normalize all whitespace including the removal of newline chars.` 75 109 76 110 ` qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0, stop: 0 red, stop: 1 blue)` 77 111 ` =>` 78 112 ` qlineargradient(0, 0, 0, 0, (0 red, 1 blue))` 79 113 ` """` 80 - 81 114 ` conformed = qss` 82 115 83 116 ` for coords, stops in self.qss_pattern.findall(qss):` 84 - 85 - ` new_coords = self._conform_group_to_scss(coords)` 117 + ` new_coords = self._conform_coords_to_scss(coords)` 86 118 ` conformed = conformed.replace(coords, new_coords, 1)` 87 119 88 120 ` if not stops:` 89 121 ` continue` 90 122 91 - ` new_stops = ', ({})'.format(self._conform_group_to_scss(stops))` 123 + ` new_stops = ', ({})'.format(self._conform_stops_to_scss(stops))` 92 124 ` conformed = conformed.replace(stops, new_stops, 1)` 93 125 94 126 ` return conformed` 95 127 96 128 ` def to_qss(self, css):` 97 - ` """Handled by qlineargradient function passed to sass.compile"""` 98 - 129 + ` """Transform to qss from css."""` 99 130 ` return css` 100 131 101 132
@@ -112,7 +143,6 @@
 112 143 ` :param input_str: QSS string` 113 144 ` :returns: Valid SCSS string` 114 145 ` """` 115 - 116 146 ` conformed = input_str` 117 147 ` for conformer in conformers:` 118 148 ` conformed = conformer.to_scss(conformed)`
@@ -130,7 +160,6 @@
 130 160 ` :param input_str: CSS string` 131 161 ` :returns: Valid QSS string` 132 162 ` """` 133 - 134 163 ` conformed = input_str` 135 164 ` for conformer in conformers[::-1]:` 136 165 ` conformed = conformer.to_qss(conformed)`

@@ -8,81 +8,207 @@
 8 8 `# -----------------------------------------------------------------------------` 9 9 `"""qtsass - Compile SCSS files to valid Qt stylesheets."""` 10 10 11 - `# Standard library imports` 11 + `# yapf: disable` 12 + 12 13 `from __future__ import absolute_import, print_function` 14 + 15 + `# Standard library imports` 13 16 `import logging` 14 17 `import os` 18 + `import sys` 15 19 16 20 `# Third party imports` 17 21 `import sass` 18 - `from watchdog.observers import Observer` 19 22 20 23 `# Local imports` 21 - `from qtsass.conformers import scss_conform, qt_conform` 24 + `from qtsass.conformers import qt_conform, scss_conform` 22 25 `from qtsass.functions import qlineargradient, rgba` 23 26 `from qtsass.importers import qss_importer` 24 - `from qtsass.events import SourceEventHandler` 25 27 26 28 27 - `logging.basicConfig(level=logging.DEBUG)` 29 + `if sys.version_info[0] == 3:` 30 + ` from collections.abc import Mapping, Sequence` 31 + `else:` 32 + ` from collections import Mapping, Sequence` 33 + 34 + 35 + `# yapf: enable` 36 + 37 + `# Constants` 38 + `DEFAULT_CUSTOM_FUNCTIONS = {'qlineargradient': qlineargradient, 'rgba': rgba}` 39 + `DEFAULT_SOURCE_COMMENTS = False` 40 + 41 + `# Logger setup` 28 42 `_log = logging.getLogger(__name__)` 29 43 30 44 31 - `def compile(input_file):` 32 - ` """Compile QtSASS to CSS."""` 45 + `def compile(string, **kwargs):` 46 + ` """` 47 + ` Conform and Compile QtSASS source code to CSS.` 33 48 34 - ` _log.debug('Compiling {}...'.format(input_file))` 49 + ` This function conforms QtSASS to valid SCSS before passing it to` 50 + ` sass.compile. Any keyword arguments you provide will be combined with` 51 + ` qtsass's default keyword arguments and passed to sass.compile.` 35 52 36 - ` with open(input_file, 'r') as f:` 37 - ` input_str = f.read()` 53 + ` .. code-block:: python` 54 + 55 + ` >>> import qtsass` 56 + ` >>> qtsass.compile("QWidget {background: rgb(0, 0, 0);}")` 57 + ` QWidget {background:black;}` 38 58 59 + ` :param string: QtSASS source code to conform and compile.` 60 + ` :param kwargs: Keyword arguments to pass to sass.compile` 61 + ` :returns: CSS string` 62 + ` """` 63 + ` kwargs.setdefault('source_comments', DEFAULT_SOURCE_COMMENTS)` 64 + ` kwargs.setdefault('custom_functions', [])` 65 + ` kwargs.setdefault('importers', [])` 66 + ` kwargs.setdefault('include_paths', [])` 67 + 68 + ` # Add QtSass importers` 69 + ` if isinstance(kwargs['importers'], Sequence):` 70 + ` kwargs['importers'] = (list(kwargs['importers']) +` 71 + ` [(0, qss_importer(*kwargs['include_paths']))])` 72 + ` else:` 73 + ` raise ValueError('Expected Sequence for importers '` 74 + ` 'got {}'.format(type(kwargs['importers'])))` 75 + 76 + ` # Add QtSass custom_functions` 77 + ` if isinstance(kwargs['custom_functions'], Sequence):` 78 + ` kwargs['custom_functions'] = dict(` 79 + ` DEFAULT_CUSTOM_FUNCTIONS,` 80 + ` **{fn.__name__: fn` 81 + ` for fn in kwargs['custom_functions']})` 82 + ` elif isinstance(kwargs['custom_functions'], Mapping):` 83 + ` kwargs['custom_functions'].update(DEFAULT_CUSTOM_FUNCTIONS)` 84 + ` else:` 85 + ` raise ValueError('Expected Sequence or Mapping for custom_functions '` 86 + ` 'got {}'.format(type(kwargs['custom_functions'])))` 87 + 88 + ` # Conform QtSass source code` 89 + ` try:` 90 + ` kwargs['string'] = scss_conform(string)` 91 + ` except Exception:` 92 + ` _log.error('Failed to conform source code')` 93 + ` raise` 94 + 95 + ` if _log.isEnabledFor(logging.DEBUG):` 96 + ` from pprint import pformat` 97 + ` log_kwargs = dict(kwargs)` 98 + ` log_kwargs['string'] = 'Conformed SCSS<...>'` 99 + ` _log.debug('Calling sass.compile with:')` 100 + ` _log.debug(pformat(log_kwargs))` 101 + ` _log.debug('Conformed scss:\n{}'.format(kwargs['string']))` 102 + 103 + ` # Compile QtSass source code` 39 104 ` try:` 40 - ` importer_root = os.path.dirname(os.path.abspath(input_file))` 41 - ` return qt_conform(` 42 - ` sass.compile(` 43 - ` string=scss_conform(input_str),` 44 - ` source_comments=False,` 45 - ` custom_functions={` 46 - ` 'qlineargradient': qlineargradient,` 47 - ` 'rgba': rgba` 48 - ` },` 49 - ` importers=[(0, qss_importer(importer_root))]` 50 - ` )` 51 - ` )` 52 - ` except sass.CompileError as e:` 53 - ` _log.error('Failed to compile {}:\n{}'.format(input_file, e))` 54 - ` return ""` 55 - 56 - 57 - `def compile_filename(input_file, dest_file):` 58 - ` """Compile QtSASS to CSS and save."""` 59 - 60 - ` css = compile(input_file)` 61 - ` with open(dest_file, 'w') as css_file:` 105 + ` return qt_conform(sass.compile(**kwargs))` 106 + ` except sass.CompileError:` 107 + ` _log.error('Failed to compile source code')` 108 + ` raise` 109 + 110 + 111 + `def compile_filename(input_file, output_file, **kwargs):` 112 + ` """Compile and save QtSASS file as CSS.` 113 + 114 + ` .. code-block:: python` 115 + 116 + ` >>> import qtsass` 117 + ` >>> qtsass.compile_filename("dummy.scss", "dummy.css")` 118 + 119 + ` :param input_file: Path to QtSass file.` 120 + ` :param output_file: Path to write Qt compliant CSS.` 121 + ` :param kwargs: Keyword arguments to pass to sass.compile` 122 + ` :returns: CSS string` 123 + ` """` 124 + ` input_root = os.path.abspath(os.path.dirname(input_file))` 125 + ` kwargs.setdefault('include_paths', [input_root])` 126 + 127 + ` with open(input_file, 'r') as f:` 128 + ` string = f.read()` 129 + 130 + ` _log.info('Compiling {}...'.format(os.path.normpath(input_file)))` 131 + ` css = compile(string, **kwargs)` 132 + 133 + ` output_root = os.path.abspath(os.path.dirname(output_file))` 134 + ` if not os.path.isdir(output_root):` 135 + ` os.makedirs(output_root)` 136 + 137 + ` with open(output_file, 'w') as css_file:` 62 138 ` css_file.write(css)` 63 - ` _log.info('Created CSS file {}'.format(dest_file))` 139 + ` _log.info('Created CSS file {}'.format(os.path.normpath(output_file)))` 140 + 141 + ` return css` 64 142 65 143 66 - `def compile_dirname(input_dir, output_dir):` 67 - ` """Compiles QtSASS files in a directory including subdirectories."""` 144 + `def compile_dirname(input_dir, output_dir, **kwargs):` 145 + ` """Compiles QtSASS files in a directory including subdirectories.` 68 146 69 - ` def is_valid(file):` 70 - ` return not file.startswith('_') and file.endswith('.scss')` 147 + ` .. code-block:: python` 71 148 72 - ` for root, subdirs, files in os.walk(input_dir):` 149 + ` >>> import qtsass` 150 + ` >>> qtsass.compile_dirname("./scss", "./css")` 151 + 152 + ` :param input_dir: Directory containing QtSass files.` 153 + ` :param output_dir: Directory to write compiled Qt compliant CSS files to.` 154 + ` :param kwargs: Keyword arguments to pass to sass.compile` 155 + ` """` 156 + ` kwargs.setdefault('include_paths', [input_dir])` 157 + 158 + ` def is_valid(file_name):` 159 + ` return not file_name.startswith('_') and file_name.endswith('.scss')` 160 + 161 + ` for root, _, files in os.walk(input_dir):` 73 162 ` relative_root = os.path.relpath(root, input_dir)` 74 163 ` output_root = os.path.join(output_dir, relative_root)` 164 + ` fkwargs = dict(kwargs)` 165 + ` fkwargs['include_paths'] = fkwargs['include_paths'] + [root]` 75 166 76 - ` for file in [f for f in files if is_valid(f)]:` 77 - ` scss_path = os.path.join(root, file)` 78 - ` css_file = os.path.splitext(file)[0] + '.css'` 167 + ` for file_name in [f for f in files if is_valid(f)]:` 168 + ` scss_path = os.path.join(root, file_name)` 169 + ` css_file = os.path.splitext(file_name)[0] + '.css'` 79 170 ` css_path = os.path.join(output_root, css_file)` 171 + 80 172 ` if not os.path.isdir(output_root):` 81 173 ` os.makedirs(output_root)` 82 - ` compile_filename(scss_path, css_path)` 83 174 175 + ` compile_filename(scss_path, css_path, **fkwargs)` 84 176 85 - `def watch(source, destination, compiler=None, recursive=True):` 177 + 178 + `def enable_logging(level=None, handler=None):` 179 + ` """Enable logging for qtsass.` 180 + 181 + ` Sets the qtsass logger's level to:` 182 + ` 1. the provided logging level` 183 + ` 2. logging.DEBUG if the QTSASS_DEBUG envvar is a True value` 184 + ` 3. logging.WARNING` 185 + 186 + ` .. code-block:: python` 187 + ` >>> import logging` 188 + ` >>> import qtsass` 189 + ` >>> handler = logging.StreamHandler()` 190 + ` >>> formatter = logging.Formatter('%(level)-8s: %(name)s> %(message)s')` 191 + ` >>> handler.setFormatter(formatter)` 192 + ` >>> qtsass.enable_logging(level=logging.DEBUG, handler=handler)` 193 + 194 + ` :param level: Optional logging level` 195 + ` :param handler: Optional handler to add` 196 + ` """` 197 + ` if level is None:` 198 + ` debug = os.environ.get('QTSASS_DEBUG', False)` 199 + ` if debug in ('1', 'true', 'True', 'TRUE', 'on', 'On', 'ON'):` 200 + ` level = logging.DEBUG` 201 + ` else:` 202 + ` level = logging.WARNING` 203 + 204 + ` logger = logging.getLogger('qtsass')` 205 + ` logger.setLevel(level)` 206 + ` if handler:` 207 + ` logger.addHandler(handler)` 208 + ` _log.debug('logging level set to {}.'.format(level))` 209 + 210 + 211 + `def watch(source, destination, compiler=None, Watcher=None):` 86 212 ` """` 87 213 ` Watches a source file or directory, compiling QtSass files when modified.` 88 214
@@ -92,19 +218,20 @@
 92 218 ` :param source: Path to source QtSass file or directory.` 93 219 ` :param destination: Path to output css file or directory.` 94 220 ` :param compiler: Compile function (optional)` 95 - ` :param recursive: If True, watch subdirectories (default: True).` 96 - ` :returns: watchdog.Observer` 221 + ` :param Watcher: Defaults to qtsass.watchers.Watcher (optional)` 222 + ` :returns: qtsass.watchers.Watcher instance` 97 223 ` """` 98 - 99 224 ` if os.path.isfile(source):` 100 225 ` watch_dir = os.path.dirname(source)` 101 226 ` compiler = compiler or compile_filename` 102 - ` else:` 227 + ` elif os.path.isdir(source):` 103 228 ` watch_dir = source` 104 229 ` compiler = compiler or compile_dirname` 230 + ` else:` 231 + ` raise ValueError('source arg must be a dirname or filename...')` 105 232 106 - ` event_handler = SourceEventHandler(source, destination, compiler)` 233 + ` if Watcher is None:` 234 + ` from qtsass.watchers import Watcher` 107 235 108 - ` observer = Observer()` 109 - ` observer.schedule(event_handler, watch_dir, recursive=recursive)` 110 - ` return observer` 236 + ` watcher = Watcher(watch_dir, compiler, (source, destination))` 237 + ` return watcher`

@@ -6,13 +6,56 @@
 6 6 `# Licensed under the terms of the MIT License` 7 7 `# (See LICENSE.txt for details)` 8 8 `# -----------------------------------------------------------------------------` 9 + `"""` 10 + `The SASS language brings countless amazing features to CSS.` 11 + 12 + `Besides being used in web development, CSS is also the way to stylize Qt-based` 13 + `desktop applications. However, Qt's CSS has a few variations that prevent the` 14 + `direct use of SASS compiler.` 15 + 16 + `The purpose of qtsass is to fill the gap between SASS and Qt-CSS by handling` 17 + `those variations.` 18 + `"""` 19 + 20 + `# yapf: disable` 9 21 10 - `# Standard library imports` 11 22 `from __future__ import absolute_import` 12 23 24 + `# Standard library imports` 25 + `import logging` 26 + 13 27 `# Local imports` 14 - `from qtsass.api import compile, compile_filename, compile_dirname, watch` 28 + `from qtsass.api import (` 29 + ` compile,` 30 + ` compile_dirname,` 31 + ` compile_filename,` 32 + ` enable_logging,` 33 + ` watch,` 34 + `)` 35 + 36 + 37 + `# yapf: enable` 38 + 39 + `# Setup Logging` 40 + `logging.getLogger(__name__).addHandler(logging.NullHandler())` 41 + `enable_logging()` 42 + 43 + `# Constants` 44 + `__version__ = '0.3.0.dev0'` 45 + 46 + 47 + `def _to_version_info(version):` 48 + ` """Convert a version string to a number and string tuple."""` 49 + ` parts = []` 50 + ` for part in version.split('.'):` 51 + ` try:` 52 + ` part = int(part)` 53 + ` except ValueError:` 54 + ` pass` 55 + 56 + ` parts.append(part)` 57 + 58 + ` return tuple(parts)` 15 59 16 60 17 - `VERSION_INFO = (0, 1, 0)` 18 - `__version__ = '.'.join(map(str, VERSION_INFO))` 61 + `VERSION_INFO = _to_version_info(__version__)`

@@ -0,0 +1,64 @@
 1 + `# -*- coding: utf-8 -*-` 2 + `# -----------------------------------------------------------------------------` 3 + `# Copyright (c) 2015 Yann Lanthony` 4 + `# Copyright (c) 2017-2018 Spyder Project Contributors` 5 + `#` 6 + `# Licensed under the terms of the MIT License` 7 + `# (See LICENSE.txt for details)` 8 + `# -----------------------------------------------------------------------------` 9 + `"""Contains the fallback implementation of the Watcher api."""` 10 + 11 + `# yapf: disable` 12 + 13 + `from __future__ import absolute_import, print_function` 14 + 15 + `# Standard library imports` 16 + `import os` 17 + 18 + `# Local imports` 19 + `from qtsass.importers import norm_path` 20 + 21 + 22 + `# yapf: enable` 23 + 24 + 25 + `def take(dir_or_file, depth=3):` 26 + ` """Return a dict mapping files and folders to their mtimes."""` 27 + ` if os.path.isfile(dir_or_file):` 28 + ` path = norm_path(dir_or_file)` 29 + ` return {path: os.path.getmtime(path)}` 30 + 31 + ` if not os.path.isdir(dir_or_file):` 32 + ` return {}` 33 + 34 + ` snapshot = {}` 35 + ` base_depth = len(norm_path(dir_or_file).split('/'))` 36 + 37 + ` for root, subdirs, files in os.walk(dir_or_file):` 38 + 39 + ` path = norm_path(root)` 40 + ` if len(path.split('/')) - base_depth == depth:` 41 + ` subdirs[:] = []` 42 + 43 + ` snapshot[path] = os.path.getmtime(path)` 44 + ` for f in files:` 45 + ` path = norm_path(root, f)` 46 + ` snapshot[path] = os.path.getmtime(path)` 47 + 48 + ` return snapshot` 49 + 50 + 51 + `def diff(prev_snapshot, next_snapshot):` 52 + ` """Return a dict containing changes between two snapshots."""` 53 + ` changes = {}` 54 + ` for path in set(prev_snapshot.keys()) | set(next_snapshot.keys()):` 55 + ` if path in prev_snapshot and path not in next_snapshot:` 56 + ` changes[path] = 'Deleted'` 57 + ` elif path not in prev_snapshot and path in next_snapshot:` 58 + ` changes[path] = 'Created'` 59 + ` else:` 60 + ` prev_mtime = prev_snapshot[path]` 61 + ` next_mtime = next_snapshot[path]` 62 + ` if next_mtime > prev_mtime:` 63 + ` changes[path] = 'Changed'` 64 + ` return changes`

@@ -0,0 +1,146 @@
 1 + `# -*- coding: utf-8 -*-` 2 + `# -----------------------------------------------------------------------------` 3 + `# Copyright (c) 2015 Yann Lanthony` 4 + `# Copyright (c) 2017-2018 Spyder Project Contributors` 5 + `#` 6 + `# Licensed under the terms of the MIT License` 7 + `# (See LICENSE.txt for details)` 8 + `# -----------------------------------------------------------------------------` 9 + `"""The filesystem watcher api."""` 10 + 11 + `# yapf: disable` 12 + 13 + `from __future__ import absolute_import` 14 + 15 + `# Standard library imports` 16 + `import functools` 17 + `import logging` 18 + `import time` 19 + 20 + 21 + `_log = logging.getLogger(__name__)` 22 + 23 + 24 + `def retry(n, interval=0.1):` 25 + ` """Retry a function or method n times before raising an exception.` 26 + 27 + ` :param n: Number of times to retry` 28 + ` :param interval: Time to sleep before attempts` 29 + ` """` 30 + ` def decorate(fn):` 31 + ` @functools.wraps(fn)` 32 + ` def attempt(*args, **kwargs):` 33 + ` attempts = 0` 34 + ` while True:` 35 + ` try:` 36 + ` return fn(*args, **kwargs)` 37 + ` except Exception:` 38 + ` attempts += 1` 39 + ` if n <= attempts:` 40 + ` raise` 41 + ` time.sleep(interval)` 42 + 43 + ` return attempt` 44 + 45 + ` return decorate` 46 + 47 + `# yapf: enable` 48 + 49 + 50 + `class Watcher(object):` 51 + ` """Watcher base class.` 52 + 53 + ` Watchers monitor a file or directory and call the on_change method when a` 54 + ` change occurs. The on_change method should trigger the compiler function` 55 + ` passed in during construction and dispatch the result to all connected` 56 + ` callbacks.` 57 + 58 + ` Watcher implementations must inherit from this base class. Subclasses` 59 + ` should perform any setup required in the setup method, rather than` 60 + ` overriding __init__.` 61 + ` """` 62 + 63 + ` def __init__(self, watch_dir, compiler, args=None, kwargs=None):` 64 + ` """Store initialization values and call Watcher.setup."""` 65 + ` self._watch_dir = watch_dir` 66 + ` self._compiler = compiler` 67 + ` self._args = args or ()` 68 + ` self._kwargs = kwargs or {}` 69 + ` self._callbacks = set()` 70 + ` self._log = _log` 71 + ` self.setup()` 72 + 73 + ` def setup(self):` 74 + ` """Perform any setup required here.` 75 + 76 + ` Rather than implement __init__, subclasses can perform any setup in` 77 + ` this method.` 78 + ` """` 79 + ` return NotImplemented` 80 + 81 + ` def start(self):` 82 + ` """Start this Watcher."""` 83 + ` return NotImplemented` 84 + 85 + ` def stop(self):` 86 + ` """Stop this Watcher."""` 87 + ` return NotImplemented` 88 + 89 + ` def join(self):` 90 + ` """Wait for this Watcher to finish."""` 91 + ` return NotImplemented` 92 + 93 + ` @retry(5)` 94 + ` def compile(self):` 95 + ` """Call the Watcher's compiler."""` 96 + ` self._log.debug(` 97 + ` 'Compiling sass...%s(*%s, **%s)',` 98 + ` self._compiler,` 99 + ` self._args,` 100 + ` self._kwargs,` 101 + ` )` 102 + ` return self._compiler(*self._args, **self._kwargs)` 103 + 104 + ` def compile_and_dispatch(self):` 105 + ` """Compile and dispatch the resulting css to connected callbacks."""` 106 + ` self._log.debug('Compiling and dispatching....')` 107 + 108 + ` try:` 109 + ` css = self.compile()` 110 + ` except Exception:` 111 + ` self._log.exception('Failed to compile...')` 112 + ` return` 113 + 114 + ` self.dispatch(css)` 115 + 116 + ` def dispatch(self, css):` 117 + ` """Dispatch css to connected callbacks."""` 118 + ` self._log.debug('Dispatching callbacks...')` 119 + ` for callback in self._callbacks:` 120 + ` callback(css)` 121 + 122 + ` def on_change(self):` 123 + ` """Call when a change is detected.` 124 + 125 + ` Subclasses must call this method when they detect a change. Subclasses` 126 + ` may also override this method in order to manually compile and dispatch` 127 + ` callbacks. For example, a Qt implementation may use signals and slots` 128 + ` to ensure that compiling and executing callbacks happens in the main` 129 + ` GUI thread.` 130 + ` """` 131 + ` self._log.debug('Change detected...')` 132 + ` self.compile_and_dispatch()` 133 + 134 + ` def connect(self, fn):` 135 + ` """Connect a callback to this Watcher.` 136 + 137 + ` All callbacks are called when a change is detected. Callbacks are` 138 + ` passed the compiled css.` 139 + ` """` 140 + ` self._log.debug('Connecting callback: %s', fn)` 141 + ` self._callbacks.add(fn)` 142 + 143 + ` def disconnect(self, fn):` 144 + ` """Disconnect a callback from this Watcher."""` 145 + ` self._log.debug('Disconnecting callback: %s', fn)` 146 + ` self._callbacks.discard(fn)`

@@ -9,13 +9,15 @@
 9 9 `# -----------------------------------------------------------------------------` 10 10 `"""qtsass command line interface."""` 11 11 12 - `# Standard library imports` 12 + `# yapf: disable` 13 + 13 14 `from __future__ import absolute_import` 14 - `import sys` 15 15 16 16 `# Local imports` 17 17 `from qtsass import cli` 18 18 19 19 20 + `# yapf: enable` 21 + 20 22 `if __name__ == '__main__':` 21 - ` cli.main(sys.argv[1:])` 23 + ` cli.main()`