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