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
|
|
|
import logging |
17
|
|
|
import os |
18
|
|
|
import sys |
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
|
|
|
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 |
42
|
|
|
_log = logging.getLogger(__name__) |
43
|
|
|
|
44
|
|
|
|
45
|
|
|
def compile(string, **kwargs): |
46
|
|
|
""" |
47
|
|
|
Conform and Compile QtSASS source code to CSS. |
48
|
|
|
|
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. |
52
|
|
|
|
53
|
|
|
.. code-block:: python |
54
|
|
|
|
55
|
|
|
>>> import qtsass |
56
|
|
|
>>> qtsass.compile("QWidget {background: rgb(0, 0, 0);}") |
57
|
|
|
QWidget {background:black;} |
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 |
104
|
|
|
try: |
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: |
138
|
|
|
css_file.write(css) |
139
|
|
|
_log.info('Created CSS file {}'.format(os.path.normpath(output_file))) |
140
|
|
|
|
141
|
|
|
return css |
142
|
|
|
|
143
|
|
|
|
144
|
|
|
def compile_dirname(input_dir, output_dir, **kwargs): |
145
|
|
|
"""Compiles QtSASS files in a directory including subdirectories. |
146
|
|
|
|
147
|
|
|
.. code-block:: python |
148
|
|
|
|
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): |
162
|
|
|
relative_root = os.path.relpath(root, input_dir) |
163
|
|
|
output_root = os.path.join(output_dir, relative_root) |
164
|
|
|
fkwargs = dict(kwargs) |
165
|
|
|
fkwargs['include_paths'] = fkwargs['include_paths'] + [root] |
166
|
|
|
|
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' |
170
|
|
|
css_path = os.path.join(output_root, css_file) |
171
|
|
|
|
172
|
|
|
if not os.path.isdir(output_root): |
173
|
|
|
os.makedirs(output_root) |
174
|
|
|
|
175
|
|
|
compile_filename(scss_path, css_path, **fkwargs) |
176
|
|
|
|
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): |
212
|
|
|
""" |
213
|
|
|
Watches a source file or directory, compiling QtSass files when modified. |
214
|
|
|
|
215
|
|
|
The compiler function defaults to compile_filename when source is a file |
216
|
|
|
and compile_dirname when source is a directory. |
217
|
|
|
|
218
|
|
|
:param source: Path to source QtSass file or directory. |
219
|
|
|
:param destination: Path to output css file or directory. |
220
|
|
|
:param compiler: Compile function (optional) |
221
|
|
|
:param Watcher: Defaults to qtsass.watchers.Watcher (optional) |
222
|
|
|
:returns: qtsass.watchers.Watcher instance |
223
|
|
|
""" |
224
|
|
|
if os.path.isfile(source): |
225
|
|
|
watch_dir = os.path.dirname(source) |
226
|
|
|
compiler = compiler or compile_filename |
227
|
|
|
elif os.path.isdir(source): |
228
|
|
|
watch_dir = source |
229
|
|
|
compiler = compiler or compile_dirname |
230
|
|
|
else: |
231
|
|
|
raise ValueError('source arg must be a dirname or filename...') |
232
|
|
|
|
233
|
|
|
if Watcher is None: |
234
|
|
|
from qtsass.watchers import Watcher |
235
|
|
|
|
236
|
|
|
watcher = Watcher(watch_dir, compiler, (source, destination)) |
237
|
|
|
return watcher |
238
|
|
|
|