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
|
|
|
|