diff options
| author | Jannis Leidel <jannis@leidel.info> | 2011-04-16 19:33:31 +0200 |
|---|---|---|
| committer | Jannis Leidel <jannis@leidel.info> | 2011-04-16 19:33:31 +0200 |
| commit | 700172c5db423c9b9d359839ea2b0475cce1022f (patch) | |
| tree | eee6dbedead7eb80c7bfd7a73bd598bcf6eaa69a /compressor/utils | |
| parent | 929c2807909bb8cb9ee84f0a4f9847fe85c656b1 (diff) | |
| parent | fbc3030463f9dc798fff7e4230ca977ff0dfceef (diff) | |
| download | django-compressor-700172c5db423c9b9d359839ea2b0475cce1022f.tar.gz | |
Merge branch 'staticfiles_compat' of https://github.com/jaap3/django_compressor into jaap3-staticfiles_compat
Diffstat (limited to 'compressor/utils')
| -rw-r--r-- | compressor/utils/__init__.py | 533 | ||||
| -rw-r--r-- | compressor/utils/staticfiles.py | 27 |
2 files changed, 560 insertions, 0 deletions
diff --git a/compressor/utils/__init__.py b/compressor/utils/__init__.py new file mode 100644 index 0000000..e81c980 --- /dev/null +++ b/compressor/utils/__init__.py @@ -0,0 +1,533 @@ +# -*- coding: utf-8 -*- +import os +import re +import sys +from inspect import getmembers +from shlex import split as cmd_split + +from django.conf import settings + +from compressor.exceptions import FilterError + +try: + any = any + +except NameError: + + def any(seq): + for item in seq: + if item: + return True + return False + + +def get_class(class_string, exception=FilterError): + """ + Convert a string version of a function name to the callable object. + """ + if not hasattr(class_string, '__bases__'): + try: + class_string = class_string.encode('ascii') + mod_name, class_name = get_mod_func(class_string) + if class_name != '': + cls = getattr(__import__(mod_name, {}, {}, ['']), class_name) + except (ImportError, AttributeError): + pass + else: + return cls + raise exception('Failed to import %s' % class_string) + + +def get_mod_func(callback): + """ + Converts 'django.views.news.stories.story_detail' to + ('django.views.news.stories', 'story_detail') + """ + try: + dot = callback.rindex('.') + except ValueError: + return callback, '' + return callback[:dot], callback[dot + 1:] + + +def walk(root, topdown=True, onerror=None, followlinks=False): + """ + A version of os.walk that can follow symlinks for Python < 2.6 + """ + for dirpath, dirnames, filenames in os.walk(root, topdown, onerror): + yield (dirpath, dirnames, filenames) + if followlinks: + for d in dirnames: + p = os.path.join(dirpath, d) + if os.path.islink(p): + for link_dirpath, link_dirnames, link_filenames in walk(p): + yield (link_dirpath, link_dirnames, link_filenames) + + +# Taken from Django 1.3 and before that from Python 2.7 +# with permission from the original author. +def _resolve_name(name, package, level): + """Return the absolute name of the module to be imported.""" + if not hasattr(package, 'rindex'): + raise ValueError("'package' not set to a string") + dot = len(package) + for x in xrange(level, 1, -1): + try: + dot = package.rindex('.', 0, dot) + except ValueError: + raise ValueError("attempted relative import beyond top-level " + "package") + return "%s.%s" % (package[:dot], name) + + +def import_module(name, package=None): + """Import a module. + + The 'package' argument is required when performing a relative import. It + specifies the package to use as the anchor point from which to resolve the + relative import to an absolute import. + + """ + if name.startswith('.'): + if not package: + raise TypeError("relative imports require the 'package' argument") + level = 0 + for character in name: + if character != '.': + break + level += 1 + name = _resolve_name(name[level:], package, level) + __import__(name) + return sys.modules[name] + + +class AppSettings(object): + """ + An app setting object to be used for handling app setting defaults + gracefully and providing a nice API for them. Say you have an app + called ``myapp`` and want to define a few defaults, and refer to the + defaults easily in the apps code. Add a ``settings.py`` to your app:: + + from path.to.utils import AppSettings + + class MyAppSettings(AppSettings): + SETTING_1 = "one" + SETTING_2 = ( + "two", + ) + + Then initialize the setting with the correct prefix in the location of + of your choice, e.g. ``conf.py`` of the app module:: + + settings = MyAppSettings(prefix="MYAPP") + + The ``MyAppSettings`` instance will automatically look at Django's + global setting to determine each of the settings and respect the + provided ``prefix``. E.g. adding this to your site's ``settings.py`` + will set the ``SETTING_1`` setting accordingly:: + + MYAPP_SETTING_1 = "uno" + + Usage + ----- + + Instead of using ``from django.conf import settings`` as you would + usually do, you can switch to using your apps own settings module + to access the app settings:: + + from myapp.conf import settings + + print myapp_settings.MYAPP_SETTING_1 + + ``AppSettings`` instances also work as pass-throughs for other + global settings that aren't related to the app. For example the + following code is perfectly valid:: + + from myapp.conf import settings + + if "myapp" in settings.INSTALLED_APPS: + print "yay, myapp is installed!" + + Custom handling + --------------- + + Each of the settings can be individually configured with callbacks. + For example, in case a value of a setting depends on other settings + or other dependencies. The following example sets one setting to a + different value depending on a global setting:: + + from django.conf import settings + + class MyCustomAppSettings(AppSettings): + ENABLED = True + + def configure_enabled(self, value): + return value and not self.DEBUG + + custom_settings = MyCustomAppSettings("MYAPP") + + The value of ``custom_settings.MYAPP_ENABLED`` will vary depending on the + value of the global ``DEBUG`` setting. + + Each of the app settings can be customized by providing + a method ``configure_<lower_setting_name>`` that takes the default + value as defined in the class attributes as the only parameter. + The method needs to return the value to be use for the setting in + question. + """ + def __dir__(self): + return sorted(list(set(self.__dict__.keys() + dir(settings)))) + + __members__ = lambda self: self.__dir__() + + def __getattr__(self, name): + if name.startswith(self._prefix): + raise AttributeError("%r object has no attribute %r" % + (self.__class__.__name__, name)) + return getattr(settings, name) + + def __setattr__(self, name, value): + super(AppSettings, self).__setattr__(name, value) + if name in dir(settings): + setattr(settings, name, value) + + def __init__(self, prefix): + super(AppSettings, self).__setattr__('_prefix', prefix) + for name, value in filter(self.issetting, getmembers(self.__class__)): + prefixed_name = "%s_%s" % (prefix.upper(), name.upper()) + value = getattr(settings, prefixed_name, value) + callback = getattr(self, "configure_%s" % name.lower(), None) + if callable(callback): + value = callback(value) + delattr(self.__class__, name) + setattr(self, prefixed_name, value) + + def issetting(self, (name, value)): + return name == name.upper() + + +class cached_property(object): + """Property descriptor that caches the return value + of the get function. + + *Examples* + + .. code-block:: python + + @cached_property + def connection(self): + return Connection() + + @connection.setter # Prepares stored value + def connection(self, value): + if value is None: + raise TypeError("Connection must be a connection") + return value + + @connection.deleter + def connection(self, value): + # Additional action to do at del(self.attr) + if value is not None: + print("Connection %r deleted" % (value, )) + """ + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + self.__get = fget + self.__set = fset + self.__del = fdel + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + self.__module__ = fget.__module__ + + def __get__(self, obj, type=None): + if obj is None: + return self + try: + return obj.__dict__[self.__name__] + except KeyError: + value = obj.__dict__[self.__name__] = self.__get(obj) + return value + + def __set__(self, obj, value): + if obj is None: + return self + if self.__set is not None: + value = self.__set(obj, value) + obj.__dict__[self.__name__] = value + + def __delete__(self, obj): + if obj is None: + return self + try: + value = obj.__dict__.pop(self.__name__) + except KeyError: + pass + else: + if self.__del is not None: + self.__del(obj, value) + + def setter(self, fset): + return self.__class__(self.__get, fset, self.__del) + + def deleter(self, fdel): + return self.__class__(self.__get, self.__set, fdel) + + +"""Advanced string formatting for Python >= 2.4. + +An implementation of the advanced string formatting (PEP 3101). + +Author: Florent Xicluna +""" + +if hasattr(str, 'partition'): + def partition(s, sep): + return s.partition(sep) +else: # Python 2.4 + def partition(s, sep): + try: + left, right = s.split(sep, 1) + except ValueError: + return s, '', '' + return left, sep, right + +_format_str_re = re.compile( + r'((?<!{)(?:{{)+' # '{{' + r'|(?:}})+(?!})' # '}} + r'|{(?:[^{](?:[^{}]+|{[^{}]*})*)?})' # replacement field +) +_format_sub_re = re.compile(r'({[^{}]*})') # nested replacement field +_format_spec_re = re.compile( + r'((?:[^{}]?[<>=^])?)' # alignment + r'([-+ ]?)' # sign + r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep + r'((?:\.\d+)?)' # precision + r'(.?)$' # type +) +_field_part_re = re.compile( + r'(?:(\[)|\.|^)' # start or '.' or '[' + r'((?(1)[^]]*|[^.[]*))' # part + r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail +) + +if hasattr(re, '__version__'): + _format_str_sub = _format_str_re.sub +else: + # Python 2.4 fails to preserve the Unicode type + def _format_str_sub(repl, s): + if isinstance(s, unicode): + return unicode(_format_str_re.sub(repl, s)) + return _format_str_re.sub(repl, s) + +if hasattr(int, '__index__'): + def _is_integer(value): + return hasattr(value, '__index__') +else: # Python 2.4 + def _is_integer(value): + return isinstance(value, (int, long)) + + +def _strformat(value, format_spec=""): + """Internal string formatter. + + It implements the Format Specification Mini-Language. + """ + m = _format_spec_re.match(str(format_spec)) + if not m: + raise ValueError('Invalid conversion specification') + align, sign, prefix, width, comma, precision, conversion = m.groups() + is_numeric = hasattr(value, '__float__') + is_integer = is_numeric and _is_integer(value) + if prefix and not is_integer: + raise ValueError('Alternate form (#) not allowed in %s format ' + 'specifier' % (is_numeric and 'float' or 'string')) + if is_numeric and conversion == 'n': + # Default to 'd' for ints and 'g' for floats + conversion = is_integer and 'd' or 'g' + elif sign: + if not is_numeric: + raise ValueError("Sign not allowed in string format specifier") + if conversion == 'c': + raise ValueError("Sign not allowed with integer " + "format specifier 'c'") + if comma: + # TODO: thousand separator + pass + try: + if ((is_numeric and conversion == 's') or + (not is_integer and conversion in set('cdoxX'))): + raise ValueError + if conversion == 'c': + conversion = 's' + value = chr(value % 256) + rv = ('%' + prefix + precision + (conversion or 's')) % (value,) + except ValueError: + raise ValueError("Unknown format code %r for object of type %r" % + (conversion, value.__class__.__name__)) + if sign not in '-' and value >= 0: + # sign in (' ', '+') + rv = sign + rv + if width: + zero = (width[0] == '0') + width = int(width) + else: + zero = False + width = 0 + # Fastpath when alignment is not required + if width <= len(rv): + if not is_numeric and (align == '=' or (zero and not align)): + raise ValueError("'=' alignment not allowed in string format " + "specifier") + return rv + fill, align = align[:-1], align[-1:] + if not fill: + fill = zero and '0' or ' ' + if align == '^': + padding = width - len(rv) + # tweak the formatting if the padding is odd + if padding % 2: + rv += fill + rv = rv.center(width, fill) + elif align == '=' or (zero and not align): + if not is_numeric: + raise ValueError("'=' alignment not allowed in string format " + "specifier") + if value < 0 or sign not in '-': + rv = rv[0] + rv[1:].rjust(width - 1, fill) + else: + rv = rv.rjust(width, fill) + elif align in ('>', '=') or (is_numeric and not align): + # numeric value right aligned by default + rv = rv.rjust(width, fill) + else: + rv = rv.ljust(width, fill) + return rv + + +def _format_field(value, parts, conv, spec, want_bytes=False): + """Format a replacement field.""" + for k, part, _ in parts: + if k: + if part.isdigit(): + value = value[int(part)] + else: + value = value[part] + else: + value = getattr(value, part) + if conv: + value = ((conv == 'r') and '%r' or '%s') % (value,) + if hasattr(value, '__format__'): + value = value.__format__(spec) + elif hasattr(value, 'strftime') and spec: + value = value.strftime(str(spec)) + else: + value = _strformat(value, spec) + if want_bytes and isinstance(value, unicode): + return str(value) + return value + + +class FormattableString(object): + """Class which implements method format(). + + The method format() behaves like str.format() in python 2.6+. + + >>> FormattableString(u'{a:5}').format(a=42) + ... # Same as u'{a:5}'.format(a=42) + u' 42' + + """ + + __slots__ = '_index', '_kwords', '_nested', '_string', 'format_string' + + def __init__(self, format_string): + self._index = 0 + self._kwords = {} + self._nested = {} + + self.format_string = format_string + self._string = _format_str_sub(self._prepare, format_string) + + def __eq__(self, other): + if isinstance(other, FormattableString): + return self.format_string == other.format_string + # Compare equal with the original string. + return self.format_string == other + + def _prepare(self, match): + # Called for each replacement field. + part = match.group(0) + if part[0] == part[-1]: + # '{{' or '}}' + assert part == part[0] * len(part) + return part[:len(part) // 2] + repl = part[1:-1] + field, _, format_spec = partition(repl, ':') + literal, sep, conversion = partition(field, '!') + if sep and not conversion: + raise ValueError("end of format while looking for " + "conversion specifier") + if len(conversion) > 1: + raise ValueError("expected ':' after format specifier") + if conversion not in 'rsa': + raise ValueError("Unknown conversion specifier %s" % + str(conversion)) + name_parts = _field_part_re.findall(literal) + if literal[:1] in '.[': + # Auto-numbering + if self._index is None: + raise ValueError("cannot switch from manual field " + "specification to automatic field numbering") + name = str(self._index) + self._index += 1 + if not literal: + del name_parts[0] + else: + name = name_parts.pop(0)[1] + if name.isdigit() and self._index is not None: + # Manual specification + if self._index: + raise ValueError("cannot switch from automatic field " + "numbering to manual field specification") + self._index = None + empty_attribute = False + for k, v, tail in name_parts: + if not v: + empty_attribute = True + if tail: + raise ValueError("Only '.' or '[' may follow ']' " + "in format field specifier") + if name_parts and k == '[' and not literal[-1] == ']': + raise ValueError("Missing ']' in format string") + if empty_attribute: + raise ValueError("Empty attribute in format string") + if '{' in format_spec: + format_spec = _format_sub_re.sub(self._prepare, format_spec) + rv = (name_parts, conversion, format_spec) + self._nested.setdefault(name, []).append(rv) + else: + rv = (name_parts, conversion, format_spec) + self._kwords.setdefault(name, []).append(rv) + return r'%%(%s)s' % id(rv) + + def format(self, *args, **kwargs): + """Same as str.format() and unicode.format() in Python 2.6+.""" + if args: + kwargs.update(dict((str(i), value) + for (i, value) in enumerate(args))) + # Encode arguments to ASCII, if format string is bytes + want_bytes = isinstance(self._string, str) + params = {} + for name, items in self._kwords.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + for name, items in self._nested.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + spec = spec % params + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + return self._string % params diff --git a/compressor/utils/staticfiles.py b/compressor/utils/staticfiles.py new file mode 100644 index 0000000..78756b3 --- /dev/null +++ b/compressor/utils/staticfiles.py @@ -0,0 +1,27 @@ +from django.conf import settings as django_settings +from django.core.exceptions import ImproperlyConfigured + +INSTALLED = "staticfiles" in django_settings.INSTALLED_APPS or \ + "django.contrib.staticfiles" in django_settings.INSTALLED_APPS + +finders = None +settings = None + +if INSTALLED: + if "django.contrib.staticfiles" in django_settings.INSTALLED_APPS: + from django.contrib.staticfiles import finders + settings = django_settings + else: + try: + from staticfiles import finders + from staticfiles.conf import settings + except ImportError: + # Old (pre 1.0) and incompatible version of staticfiles + INSTALLED = False + + if INSTALLED and "compressor.finders.CompressorFinder" \ + not in settings.STATICFILES_FINDERS: + raise ImproperlyConfigured( + "When using Django Compressor together with staticfiles, " + "please add 'compressor.finders.CompressorFinder' to the " + "STATICFILES_FINDERS setting.") |
