Skip to content

Commit 6be8369

Browse files
committed
Refactored the way the Environment class stores configuration. The
set_config/get_config methods are gone, instead there is a "config" property which exposes a dict-like object. The Environment subclass used by django-assets can now more cleanly implement a custom config backend, no longer needing to hack __getattribute__.
1 parent 49f028d commit 6be8369

File tree

7 files changed

+133
-83
lines changed

7 files changed

+133
-83
lines changed

docs/environment.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ Filter configuration
7272
====================
7373

7474
In addition to the standard options listed above, you can set custom
75-
configuration values using ``set_config``. This is so that you can
75+
configuration values using ``Environment.config``. This is so that you can
7676
configure filters through the environment:
7777

7878
.. code-block:: python
7979
80-
environment.set_config('sass_bin', '/opt/sass/bin/sass')
80+
environment.config['sass_bin'] = '/opt/sass/bin/sass')
8181
8282
This allows the :ref:`Sass filter <filters-sass>` to find the sass
8383
binary.

src/django_assets/env.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import imp
22
from django.conf import settings
3-
from webassets.env import Environment
3+
from webassets.env import Environment, ConfigStorage
44
from webassets.importlib import import_module
55

66

77
__all__ = ('register',)
88

99

10-
class DjangoEnvironment(Environment):
11-
"""For Django, we need to redirect all the configuration values this
12-
object holds to Django's own settings object.
1310

14-
We do this by hooking into __getattribute__ and __setattribute__,
15-
rather than reimplementing the get_foo/set_foo() methods, which means
16-
we won't have to reimplement any validation the parent class may do.
17-
"""
11+
class DjangoConfigStorage(ConfigStorage):
1812

1913
_mapping = {
2014
'debug': 'ASSETS_DEBUG',
@@ -26,37 +20,36 @@ class DjangoEnvironment(Environment):
2620
'url': 'MEDIA_URL',
2721
}
2822

29-
def __init__(self):
30-
# Have the parent initialize the default values
31-
super(DjangoEnvironment, self).__init__(None, None)
23+
def _transform_key(self, key):
24+
return self._mapping.get(key.lower(), key.upper())
3225

33-
# Then, from this point on, redirect various attributes
34-
# to the Django settings object.
35-
self.django_settings = settings
26+
def __getitem__(self, key):
27+
return getattr(settings, self._transform_key(key))
3628

37-
def get_config(self, key, default=None):
38-
return getattr(self.django_settings, key, default)
29+
def __setitem__(self, key, value):
30+
setattr(settings, self._transform_key(key), value)
3931

40-
def __getattribute__(self, name):
41-
if name == '_mapping':
42-
return super(DjangoEnvironment, self).__getattribute__(name)
43-
if not name in self._mapping:
44-
return super(DjangoEnvironment, self).__getattribute__(name)
32+
def __delitem__(self, key):
33+
# This isn't possible to implement in Django without relying
34+
# on internals of the settings object, so just set to None.
35+
self.__setitem__(key, None)
4536

46-
django_key = self._mapping[name]
47-
if not hasattr(self.django_settings, django_key):
48-
return super(DjangoEnvironment, self).__getattribute__(name)
4937

50-
return getattr(self.django_settings, django_key)
38+
class DjangoEnvironment(Environment):
39+
"""For Django, we need to redirect all the configuration values this
40+
object holds to Django's own settings object.
5141
52-
def __setattr__(self, name, value):
53-
if not hasattr(self, 'django_settings'):
54-
# The parent's __init__ is probably settings defaults
55-
return super(DjangoEnvironment, self).__setattr__(name, value)
56-
if not name in self._mapping:
57-
return super(DjangoEnvironment, self).__setattr__(name, value)
42+
We do this by hooking into __getattribute__ and __setattribute__,
43+
rather than reimplementing the get_foo/set_foo() methods, which means
44+
we won't have to reimplement any validation the parent class may do.
45+
"""
46+
47+
config_storage_class = DjangoConfigStorage
5848

59-
setattr(self.django_settings, self._mapping[name], value)
49+
def __init__(self):
50+
# Have the parent initialize the default values
51+
super(DjangoEnvironment, self).__init__(settings.MEDIA_ROOT,
52+
settings.MEDIA_URL)
6053

6154

6255
# Django has a global state, a global configuration, and so we need a

src/webassets/env.py

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,76 @@ class RegisterError(Exception):
1111
pass
1212

1313

14+
class ConfigStorage(object):
15+
"""This is the backend which :class:`Environment` uses to store
16+
it's configuration values.
17+
18+
Environment-subclasses like the one used by ``django-assets`` will
19+
often want to use a custom ``ConfigStorage`` as well, building upon
20+
whatever configuration the framework is using.
21+
22+
The goal in designing this class therefore is to make it easy for
23+
subclasses to change the place the data is stored: Only
24+
_meth:`__getitem__`, _meth:`__setitem__` and _meth:`__delitem__`
25+
need to be implemented.
26+
27+
One rule: The default storage is case-insensitive, and custom
28+
environments should maintain those semantics.
29+
30+
A related reason is why we don't inherit from ``dict``. It would
31+
require us to re-implement a whole bunch of methods, like pop() etc.
32+
"""
33+
# Don't inherit from ``dict``, as that would require us to implement
34+
# a whole bunch of methods, like pop() etc.
35+
def __init__(self, d={}):
36+
self._dict = {}
37+
self.update(d)
38+
39+
def __contains__(self, key):
40+
return self._dict.__contains__(key.lower())
41+
42+
def get(self, key, default=None):
43+
try:
44+
return self.__getitem__(key)
45+
except KeyError:
46+
return default
47+
48+
def update(self, d):
49+
for key in d:
50+
self._dict[key] = d[key]
51+
52+
def __getitem__(self, key):
53+
raise NotImplementedError()
54+
55+
def __setitem__(self, key, value):
56+
raise NotImplementedError()
57+
58+
def __delitem__(self, key):
59+
raise NotImplementedError()
60+
61+
62+
class DictConfigStorage(ConfigStorage):
63+
"""Using a lower-case dict for configuration values.
64+
"""
65+
def __getitem__(self, key):
66+
return self._dict.__getitem__(key.lower())
67+
def __setitem__(self, key, value):
68+
self._dict.__setitem__(key.lower(), value)
69+
def __delitem__(self, key):
70+
self._dict.__delitem__(key.lower())
71+
72+
1473
class Environment(object):
1574
"""Owns a collection of bundles, and a set of configuration values
1675
which will be used when processing these bundles.
1776
"""
1877

78+
config_storage_class = DictConfigStorage
79+
1980
def __init__(self, directory, url, **config):
2081
self._named_bundles = {}
2182
self._anon_bundles = []
22-
self._config = config
83+
self._config = self.config_storage_class(config)
2384

2485
self.directory = directory
2586
self.url = url
@@ -84,19 +145,18 @@ def add(self, *bundles):
84145
self._anon_bundles.append(bundle)
85146
bundle.env = self # take ownership
86147

87-
def get_config(self, key, default=None):
88-
"""This is a simple configuration area provided by the asset
89-
env which holds additional options used by the filters.
148+
@property
149+
def config(self):
150+
"""Key-value configuration. Keys are case-insensitive.
90151
"""
91-
return self._config.get(key.lower(), default)
92-
93-
def set_config(self, key, value):
94-
self._config[key.lower()] = value
152+
# This is a property so that user are not tempted to assign
153+
# a custom dictionary which won't uphold our caseless semantics.
154+
return self._config
95155

96156
def set_debug(self, debug):
97-
self._debug = debug
157+
self.config['debug'] = debug
98158
def get_debug(self):
99-
return self._debug
159+
return self.config['debug']
100160
debug = property(get_debug, set_debug, doc=
101161
"""Enable/disable debug mode. Possible values are:
102162
@@ -110,9 +170,9 @@ def get_debug(self):
110170
""")
111171

112172
def set_cache(self, enable):
113-
self._cache = enable
173+
self.config['cache'] = enable
114174
def get_cache(self):
115-
return self._cache
175+
return self.config['cache']
116176
cache = property(get_cache, set_cache, doc=
117177
"""Controls the behavior of the cache. The cache will speed up rebuilding
118178
of your bundles, by caching individual filter results. This can be
@@ -135,9 +195,9 @@ def get_cache(self):
135195
""")
136196

137197
def set_updater(self, updater):
138-
self._updater = updater
198+
self.config['updater'] = updater
139199
def get_updater(self):
140-
return self._updater
200+
return self.config['updater']
141201
updater = property(get_updater, set_updater, doc=
142202
"""Controls when and if bundles should be automatically rebuilt.
143203
Possible values are:
@@ -167,9 +227,9 @@ def get_updater(self):
167227
""")
168228

169229
def set_expire(self, expire):
170-
self._expire = expire
230+
self.config['expire'] = expire
171231
def get_expire(self):
172-
return self._expire
232+
return self.config['expire']
173233
expire = property(get_expire, set_expire, doc=
174234
"""If you send your assets to the client using a *far future expires*
175235
header (to minimize the 304 responses your server has to send), you
@@ -193,17 +253,17 @@ def get_expire(self):
193253
""")
194254

195255
def set_directory(self, directory):
196-
self._directory = directory
256+
self.config['directory'] = directory
197257
def get_directory(self):
198-
return self._directory
258+
return self.config['directory']
199259
directory = property(get_directory, set_directory, doc=
200260
"""The base directory to which all paths will be relative to.
201261
""")
202262

203263
def set_url(self, url):
204-
self._url = url
264+
self.config['url'] = url
205265
def get_url(self):
206-
return self._url
266+
return self.config['url']
207267
url = property(get_url, set_url, doc=
208268
"""The base used to construct urls under which :attr:`directory`
209269
should be exposed.

src/webassets/filter/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def get_config(self, setting=False, env=None, require=True,
9292

9393
value = None
9494
if not setting is False:
95-
value = self.env.get_config(setting, None)
95+
value = self.env.config.get(setting, None)
9696

9797
if value is None and not env is False:
9898
value = os.environ.get(env)

tests/test_environment.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ def setup(self):
7979
self.m = Environment(None, None)
8080

8181
def test_basic(self):
82-
assert self.m.get_config('foo') == None
83-
self.m.set_config('foo', 'bar')
84-
assert self.m.get_config('foo') == 'bar'
82+
assert self.m.config.get('foo') == None
83+
self.m.config['foo'] = 'bar'
84+
assert self.m.config.get('foo') == 'bar'
8585

8686
def test_case(self):
8787
"""get_config() is case-insensitive.
8888
"""
89-
self.m.set_config('FoO', 'bar')
90-
assert self.m.get_config('FOO') == 'bar'
91-
assert self.m.get_config('foo') == 'bar'
92-
assert self.m.get_config('fOO') == 'bar'
89+
self.m.config['FoO'] = 'bar'
90+
assert self.m.config.get('FOO') == 'bar'
91+
assert self.m.config.get('foo') == 'bar'
92+
assert self.m.config.get('fOO') == 'bar'

tests/test_ext/test_django.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,29 @@ def setup_module():
2323
AssetsNode = Node
2424

2525

26-
def test_options():
27-
"""Various Environment options are backed by the Django settings
26+
class TestConfig(object):
27+
"""The environment configuration is backed by the Django settings
2828
object.
2929
"""
30-
settings.MEDIA_ROOT = 'FOO'
31-
assert get_env().directory == 'FOO'
3230

33-
get_env().directory = 'BAR'
34-
assert settings.MEDIA_ROOT == 'BAR'
31+
def test_default_options(self):
32+
"""The builtin options have different names within the Django
33+
settings, to make it obvious they belong to django-assets.
34+
"""
35+
settings.ASSETS_EXPIRE = 'timestamp'
36+
assert get_env().config['expire'] == settings.ASSETS_EXPIRE
3537

36-
# We can also access values that are not represented by a original
37-
# Django setting. Specifically, we are able to read those values
38-
# and get the webassets-default without having to explicitly
39-
# initialize the corresponding Django setting.
40-
assert get_env().debug == False
41-
assert not hasattr(settings, 'ASSETS_DEBUG')
42-
get_env().debug = True
43-
assert settings.ASSETS_DEBUG == True
38+
settings.MEDIA_ROOT = 'FOO'
39+
assert get_env().directory == 'FOO'
4440

41+
get_env().directory = 'BAR'
42+
assert settings.MEDIA_ROOT == 'BAR'
4543

46-
def test_config():
47-
"""The Environment config storage is also backed by the Django
48-
settings object.
49-
"""
50-
settings.FOO = 42
51-
get_env().get_config('FOO') == 42
44+
def test_custom_options(self):
45+
settings.FOO = 42
46+
get_env().config['foo'] == 42
47+
# Also, we are caseless.
48+
get_env().config['foO'] == 42
5249

5350

5451
class TestTemplateTag():

tests/test_filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_get_config(self):
4747
assert_raises(EnvironmentError, get_config, setting=NAME, env=False)
4848

4949
# Set the value in the environment as well.
50-
m.set_config(NAME, 'foo')
50+
m.config[NAME] = 'foo'
5151
# Ensure that settings take precedence.
5252
assert_equals(get_config(NAME), 'foo')
5353
# Two different names can be supplied.

0 commit comments

Comments
 (0)