libsass-python/sassutils/wsgi.py

176 lines
6.2 KiB
Python

""":mod:`sassutils.wsgi` --- WSGI middleware for development purpose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
import logging
import os
import os.path
from pkg_resources import resource_filename
from sass import CompileError
from sassutils._compat import collections_abc
from .builder import Manifest
__all__ = 'SassMiddleware',
class SassMiddleware(object):
r"""WSGI middleware for development purpose. Every time a CSS file has
requested it finds a matched Sass/SCSS source file and then compiled
it into CSS.
It shows syntax errors in three ways:
Heading comment
The result CSS includes detailed error message in the heading
CSS comment e.g.:
.. code-block:: css
/*
Error: invalid property name
*/
Red text in ``body:before``
The result CSS draws detailed error message in ``:before``
pseudo-class of ``body`` element e.g.:
.. code-block:: css
body:before {
content: 'Error: invalid property name';
color: maroon;
background-color: white;
}
In most cases you could be aware of syntax error by refreshing your
working document because it will removes all other styles and leaves
only a red text.
:mod:`logging`
It logs syntax errors if exist during compilation to
``sassutils.wsgi.SassMiddleware`` logger with level ``ERROR``.
To enable this::
from logging import Formatter, StreamHandler, getLogger
logger = getLogger('sassutils.wsgi.SassMiddleware')
handler = StreamHandler(level=logging.ERROR)
formatter = Formatter(fmt='*' * 80 + '\n%(message)s\n' + '*' * 80)
handler.setFormatter(formatter)
logger.addHandler(handler)
Or simply::
import logging
logging.basicConfig()
:param app: the WSGI application to wrap
:type app: :class:`collections.abc.Callable`
:param manifests: build settings. the same format to
:file:`setup.py` script's ``sass_manifests``
option
:type manifests: :class:`collections.abc.Mapping`
:param package_dir: optional mapping of package names to directories.
the same format to :file:`setup.py` script's
``package_dir`` option
:type package_dir: :class:`collections.abc.Mapping`
.. versionchanged:: 0.4.0
It creates also source map files with filenames followed by
:file:`.map` suffix.
.. versionadded:: 0.8.0
It logs syntax errors if exist during compilation to
``sassutils.wsgi.SassMiddleware`` logger with level ``ERROR``.
"""
def __init__(
self, app, manifests, package_dir={},
error_status='200 OK',
):
if not callable(app):
raise TypeError(
'app must be a WSGI-compliant callable object, '
'not ' + repr(app),
)
self.app = app
self.manifests = Manifest.normalize_manifests(manifests)
if not isinstance(package_dir, collections_abc.Mapping):
raise TypeError(
'package_dir must be a mapping object, not ' +
repr(package_dir),
)
self.error_status = error_status
self.package_dir = dict(package_dir)
for package_name in self.manifests:
if package_name in self.package_dir:
continue
path = resource_filename(package_name, '')
self.package_dir[package_name] = path
self.paths = []
for package_name, manifest in self.manifests.items():
wsgi_path = manifest.wsgi_path
if not wsgi_path.startswith('/'):
wsgi_path = '/' + wsgi_path
if not wsgi_path.endswith('/'):
wsgi_path += '/'
package_dir = self.package_dir[package_name]
self.paths.append((wsgi_path, package_dir, manifest))
def __call__(self, environ, start_response):
path = environ.get('PATH_INFO', '/')
if path.endswith('.css'):
for prefix, package_dir, manifest in self.paths:
if not path.startswith(prefix):
continue
css_filename = path[len(prefix):]
sass_filename = manifest.unresolve_filename(
package_dir, css_filename,
)
try:
result = manifest.build_one(
package_dir,
sass_filename,
source_map=True,
)
except (IOError, OSError):
break
except CompileError as e:
logger = logging.getLogger(__name__ + '.SassMiddleware')
logger.error(str(e))
start_response(
self.error_status,
[('Content-Type', 'text/css; charset=utf-8')],
)
return [
b'/*\n', str(e).encode('utf-8'), b'\n*/\n\n',
b'body:before { content: ',
self.quote_css_string(str(e)).encode('utf-8'),
b'; color: maroon; background-color: white',
b'; white-space: pre-wrap; display: block',
b'; font-family: "Courier New", monospace'
b'; user-select: text; }',
]
def read_file(path):
with open(path, 'rb') as in_:
while 1:
chunk = in_.read(4096)
if chunk:
yield chunk
else:
break
start_response('200 OK', [('Content-Type', 'text/css')])
return read_file(os.path.join(package_dir, result))
return self.app(environ, start_response)
@staticmethod
def quote_css_string(s):
"""Quotes a string as CSS string literal."""
return "'" + ''.join('\\%06x' % ord(c) for c in s) + "'"