Merge lp:~lukasz-czyzykowski/django-configglue/django-1.4 into lp:django-configglue

Proposed by Łukasz Czyżykowski
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: 108
Merged at revision: 71
Proposed branch: lp:~lukasz-czyzykowski/django-configglue/django-1.4
Merge into: lp:django-configglue
Diff against target: 1248 lines (+536/-212)
12 files modified
django_configglue/management/__init__.py (+9/-9)
django_configglue/schema.py (+349/-43)
django_configglue/tests/helpers.py (+32/-26)
django_configglue/tests/settings.py (+3/-3)
django_configglue/tests/test_configglue.py (+42/-25)
django_configglue/tests/test_settings.py (+36/-28)
django_configglue/utils.py (+15/-0)
setup.py (+1/-1)
testproject/main-12.cfg (+0/-12)
testproject/main-14.cfg (+10/-0)
testproject/main.cfg (+0/-2)
tox.ini (+39/-63)
To merge this branch: bzr merge lp:~lukasz-czyzykowski/django-configglue/django-1.4
Reviewer Review Type Date Requested Status
Ricardo Kirkner Approve
Review via email: mp+149555@code.launchpad.net

Commit message

Django 1.4 compatibility.

Description of the change

Overview
========
Updated code to work against Django 1.4. On the way there dropped support for anything earlier than Django 1.3. Schemas for them still exist in the codebase, but are not registered.

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

l. 515,581: this should probably be at the start of the setUp method
l. 551,562: instead you could use the addCleanup idiom which is nicer than having to store this
l. 573,693: why mixing stdout and stderr?
l. 989: remove commented out code

Also tests are currently not passing when running them like 'python setup.py test'. I have django 1.4.1 as a system package. If tests require a specific django version (like 1.4.5, it should be specified in setup.py).

review: Needs Fixing
96. By Łukasz Czyżykowski

MP reviews comment updates.

97. By Łukasz Czyżykowski

Updated setup.py to point to the earliest supported version.

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

l. 515,581: done
l. 551,562: it has to be done around the settings reload, and before clean up, as otherwise tearDown will fail
l. 573,693: because django 1.3 prints the help message to stderr, 1.4 to stdout so to test on both, we need to mix them
l. 989: done

The 1.4.5 is specified in tox.ini, but updated setup.py to point to the 1.3.7 as the version officially supported.

98. By Łukasz Czyżykowski

Moved super call to the top of setUp method.

99. By Łukasz Czyżykowski

Added code to handle django versions before 1.4.5

100. By Łukasz Czyżykowski

Added testenv for django 1.4.1

101. By Łukasz Czyżykowski

Fixed super call in setUp.

102. By Łukasz Czyżykowski

Fixed small issue in assert_schema_structure.

103. By Łukasz Czyżykowski

Properly rearranged schemas to include base one for Django 1.3 and 1.4.

104. By Łukasz Czyżykowski

Fixed test for schma by generating new versions.

105. By Łukasz Czyżykowski

Fixed test generation test.

106. By Łukasz Czyżykowski

Fixed test for Django 1.3.*

107. By Łukasz Czyżykowski

Fixed tests on all version.

108. By Łukasz Czyżykowski

essage=Changed the way in which version is used in schema building.

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Great stuff! Thanks

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_configglue/management/__init__.py'
2--- django_configglue/management/__init__.py 2012-10-15 14:30:11 +0000
3+++ django_configglue/management/__init__.py 2013-03-22 14:01:30 +0000
4@@ -4,16 +4,16 @@
5 # GNU Lesser General Public License version 3 (see the file LICENSE)
6 # except where third-party/django/LICENSE applies.
7
8-
9 from optparse import BadOptionError
10-import sys
11-
12-import django
13-import django.core.management
14+
15+from django.core import management
16+from django.core.management import (
17+ ManagementUtility,
18+ LaxOptionParser as _LaxOptionParser,
19+)
20+from django.conf import settings
21+
22 from configglue.glue import schemaconfigglue
23-from django.core.management import ManagementUtility, LaxOptionParser as _LaxOptionParser
24-from django.core.management.base import BaseCommand, handle_default_options
25-from django.conf import settings
26 from django_configglue import utils
27
28
29@@ -86,4 +86,4 @@
30
31
32 # We're going to go ahead and use our own ManagementUtility here, thank you.
33-django.core.management.ManagementUtility = GlueManagementUtility
34+management.ManagementUtility = GlueManagementUtility
35
36=== modified file 'django_configglue/schema.py'
37--- django_configglue/schema.py 2013-02-20 08:51:07 +0000
38+++ django_configglue/schema.py 2013-03-22 14:01:30 +0000
39@@ -13,10 +13,10 @@
40 StringOption,
41 TupleOption,
42 )
43-from django import get_version
44+from django import get_version, VERSION
45 from django.conf import global_settings
46-from django.conf.project_template import settings as project_settings
47
48+from django_configglue.utils import get_project_settings
49
50 # As in django.conf.global_settings:
51 # This is defined here as a do-nothing function because we can't import
52@@ -446,23 +446,6 @@
53 help="The profanities that will trigger a validation error in the "
54 "'hasNoProfanities' validator. All of these should be in "
55 "lowercase")
56- comments_banned_users_group = StringOption(null=True,
57- help="The group ID that designates which users are banned. "
58- "Set to None if you're not using it")
59- comments_moderators_group = StringOption(null=True,
60- help="The group ID that designates which users can moderate "
61- "comments. Set to None if you're not using it")
62- comments_sketchy_users_group = StringOption(null=True,
63- help="The group ID that designates the users whose comments "
64- "should be e-mailed to MANAGERS. Set to None if you're not "
65- "using it")
66- comments_first_few = IntOption(default=0,
67- help="The system will e-mail MANAGERS the first "
68- "COMMENTS_FIRST_FEW comments by each user. Set this to 0 if "
69- "you want to disable it")
70- banned_ips = TupleOption(
71- help="A tuple of IP addresses that have been banned from "
72- "participating in various Django-powered features")
73
74 ##################
75 # AUTHENTICATION #
76@@ -873,19 +856,16 @@
77
78
79 Django13Base = derivate_django_schema(
80- Django12Schema, exclude=['cache_backend'])
81+ Django12Schema,
82+ exclude=[
83+ 'cache_backend',
84+ ])
85
86
87 class Django13Schema(Django13Base):
88 version = '1.3'
89
90- # sections
91 class django(Django13Base.django):
92-
93- ################
94- # CORE #
95- ################
96-
97 # update default value
98 languages = ListOption(
99 item=TupleOption(length=2),
100@@ -1025,6 +1005,27 @@
101 help="The profanities that will trigger a validation error in the "
102 "'hasNoProfanities' validator. All of these should be in "
103 "lowercase")
104+ comments_banned_users_group = StringOption(
105+ null=True,
106+ help="The group ID that designates which users are banned. "
107+ "Set to None if you're not using it")
108+ comments_moderators_group = StringOption(
109+ null=True,
110+ help="The group ID that designates which users can moderate "
111+ "comments. Set to None if you're not using it")
112+ comments_sketchy_users_group = StringOption(
113+ null=True,
114+ help="The group ID that designates the users whose comments "
115+ "should be e-mailed to MANAGERS. Set to None if you're not "
116+ "using it")
117+ comments_first_few = IntOption(
118+ default=0,
119+ help="The system will e-mail MANAGERS the first "
120+ "COMMENTS_FIRST_FEW comments by each user. Set this to 0 if "
121+ "you want to disable it")
122+ banned_ips = TupleOption(
123+ help="A tuple of IP addresses that have been banned from "
124+ "participating in various Django-powered features")
125
126 ###########
127 # LOGGING #
128@@ -1115,11 +1116,50 @@
129 "Examples: 'htttp://foo.com/media/', '/media/'")
130
131
132-class Django14Schema(Django13Schema):
133+class Django136Schema(Django13Schema):
134+ version = '1.3.6'
135+
136+ class django(Django13Schema.django):
137+
138+ allowed_hosts = ListOption(
139+ item=StringOption(),
140+ help="A list of strings representing the host/domain names "
141+ "that this Django site can serve. This is a security "
142+ "measure to prevent an attacker from poisoning caches and "
143+ "password reset emails with links to malicious hosts by "
144+ "submitting requests with a fake HTTP Host header, which is "
145+ "possible even under many seemingly-safe webserver "
146+ "configurations.")
147+
148+
149+Django14Base = derivate_django_schema(
150+ Django13Schema,
151+ exclude=[
152+ 'admin_media_prefix',
153+ 'ignorable_404_starts',
154+ 'ignorable_404_ends',
155+ 'banned_ips',
156+ 'comments_banned_users_group',
157+ 'comments_moderators_group',
158+ 'comments_sketchy_users_group',
159+ 'comments_first_few',
160+ 'database_engine',
161+ 'database_host',
162+ 'database_name',
163+ 'database_options',
164+ 'database_password',
165+ 'database_port',
166+ 'database_user',
167+ 'test_database_charset',
168+ 'test_database_collation',
169+ 'test_database_name',
170+ ])
171+
172+
173+class Django14Schema(Django14Base):
174 version = '1.4'
175
176- #sections
177- class django(Django13Schema.django):
178+ class django(Django14Base.django):
179
180 wsgi_application = StringOption(
181 help="The full Python path of the WSGI application object"
182@@ -1140,6 +1180,7 @@
183
184 secure_proxy_ssl_header = TupleOption(
185 length=2,
186+ default=None,
187 help="A tuple representing a HTTP header/value combination "
188 "that signifies a request is secure. This controls the "
189 "behavior of the request object's is_secure() method.")
190@@ -1153,6 +1194,252 @@
191 "as favicon.ico or robots.txt, or if it gets hammered by "
192 "script kiddies.")
193
194+ password_hashers = ListOption(
195+ item=StringOption(),
196+ help="This is a list of hashing algorithm classes that this "
197+ "Django installation supports. The first entry in this list "
198+ "(that is, settings.PASSWORD_HASHERS[0]) will be used to "
199+ "store passwords, and all the other entries are valid "
200+ "hashers that can be used to check existing passwords.",
201+ default=[
202+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
203+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
204+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
205+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
206+ 'django.contrib.auth.hashers.MD5PasswordHasher',
207+ 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
208+ 'django.contrib.auth.hashers.CryptPasswordHasher',
209+ ]
210+ )
211+
212+ x_frame_options = StringOption(
213+ default='SAMEORIGIN',
214+ help="The default value for the X-Frame-Options header used "
215+ "by XFrameOptionsMiddleware.")
216+
217+ use_tz = BoolOption(
218+ default=True,
219+ help="A boolean that specifies if datetimes will be timezone-aware"
220+ " by default or not. If this is set to True, Django will use "
221+ "timezone-aware datetimes internally. Otherwise, Django will "
222+ "use naive datetimes in local time.")
223+
224+ default_exception_reporter_filter = StringOption(
225+ default='django.views.debug.SafeExceptionReporterFilter',
226+ help="Default exception reporter filter class to be used if none "
227+ "has been assigned to the HttpRequest instance yet.")
228+
229+ signing_backend = StringOption(
230+ default='django.core.signing.TimestampSigner',
231+ help="The backend used for signing cookies and other data.")
232+
233+ url_validator_user_agent = StringOption(
234+ default=("Django/%s (https://www.djangoproject.com)" %
235+ get_version()),
236+ help="The User-Agent string to use when checking for URL validity "
237+ "through the isExistingURL validator")
238+
239+ message_storage = StringOption(
240+ default='django.contrib.messages.storage.fallback.'
241+ 'FallbackStorage',
242+ help="Class to be used as messages backend")
243+
244+
245+ logging = DictOption(
246+ spec={
247+ 'version': IntOption(default=1),
248+ 'formatters': DictOption(
249+ item=DictOption(
250+ spec={
251+ 'format': StringOption(null=True),
252+ 'datefmt': StringOption(null=True)})),
253+ 'filters': DictOption(
254+ item=DictOption(
255+ spec={'name': StringOption()})),
256+ 'handlers': DictOption(
257+ item=DictOption(
258+ spec={
259+ 'class': StringOption(fatal=True),
260+ 'level': StringOption(),
261+ 'formatter': StringOption(),
262+ 'filters': StringOption()})),
263+ 'loggers': DictOption(
264+ item=DictOption(
265+ spec={
266+ 'level': StringOption(),
267+ 'propagate': BoolOption(),
268+ 'filters': ListOption(item=StringOption()),
269+ 'handlers': ListOption(item=StringOption()),
270+ })),
271+ 'root': DictOption(
272+ spec={
273+ 'level': StringOption(),
274+ 'filters': ListOption(item=StringOption()),
275+ 'handlers': ListOption(item=StringOption()),
276+ }),
277+ 'incremental': BoolOption(default=False),
278+ 'disable_existing_loggers': BoolOption(default=False),
279+ },
280+ default={
281+ 'version': 1,
282+ 'disable_existing_loggers': False,
283+ 'filters': {
284+ 'require_debug_false': {
285+ '()': 'django.utils.log.RequireDebugFalse',
286+ }
287+ },
288+ 'handlers': {
289+ 'mail_admins': {
290+ 'level': 'ERROR',
291+ 'filters': ['require_debug_false'],
292+ 'class': 'django.utils.log.AdminEmailHandler'
293+ }
294+ },
295+ 'loggers': {
296+ 'django.request': {
297+ 'handlers': ['mail_admins'],
298+ 'level': 'ERROR',
299+ 'propagate': True,
300+ },
301+ }
302+ },
303+ help="The default logging configuration. This sends an email to "
304+ "the site admins on every HTTP 500 error. All other records "
305+ "are sent to the bit bucket.")
306+
307+ template_context_processors = ListOption(
308+ item=StringOption(),
309+ default=['django.contrib.auth.context_processors.auth',
310+ 'django.core.context_processors.debug',
311+ 'django.core.context_processors.i18n',
312+ 'django.core.context_processors.media',
313+ 'django.core.context_processors.static',
314+ 'django.core.context_processors.tz',
315+ 'django.contrib.messages.context_processors.messages',
316+ ],
317+ help="List of processors used by RequestContext to populate the "
318+ "context. Each one should be a callable that takes the "
319+ "request object as its only parameter and returns a "
320+ "dictionary to add to the context")
321+
322+ languages = ListOption(
323+ item=TupleOption(length=2),
324+ default=[
325+ ('ar', gettext_noop('Arabic')),
326+ ('az', gettext_noop('Azerbaijani')),
327+ ('bg', gettext_noop('Bulgarian')),
328+ ('bn', gettext_noop('Bengali')),
329+ ('bs', gettext_noop('Bosnian')),
330+ ('ca', gettext_noop('Catalan')),
331+ ('cs', gettext_noop('Czech')),
332+ ('cy', gettext_noop('Welsh')),
333+ ('da', gettext_noop('Danish')),
334+ ('de', gettext_noop('German')),
335+ ('el', gettext_noop('Greek')),
336+ ('en', gettext_noop('English')),
337+ ('en-gb', gettext_noop('British English')),
338+ ('eo', gettext_noop('Esperanto')),
339+ ('es', gettext_noop('Spanish')),
340+ ('es-ar', gettext_noop('Argentinian Spanish')),
341+ ('es-mx', gettext_noop('Mexican Spanish')),
342+ ('es-ni', gettext_noop('Nicaraguan Spanish')),
343+ ('et', gettext_noop('Estonian')),
344+ ('eu', gettext_noop('Basque')),
345+ ('fa', gettext_noop('Persian')),
346+ ('fi', gettext_noop('Finnish')),
347+ ('fr', gettext_noop('French')),
348+ ('fy-nl', gettext_noop('Frisian')),
349+ ('ga', gettext_noop('Irish')),
350+ ('gl', gettext_noop('Galician')),
351+ ('he', gettext_noop('Hebrew')),
352+ ('hi', gettext_noop('Hindi')),
353+ ('hr', gettext_noop('Croatian')),
354+ ('hu', gettext_noop('Hungarian')),
355+ ('id', gettext_noop('Indonesian')),
356+ ('is', gettext_noop('Icelandic')),
357+ ('it', gettext_noop('Italian')),
358+ ('ja', gettext_noop('Japanese')),
359+ ('ka', gettext_noop('Georgian')),
360+ ('kk', gettext_noop('Kazakh')),
361+ ('km', gettext_noop('Khmer')),
362+ ('kn', gettext_noop('Kannada')),
363+ ('ko', gettext_noop('Korean')),
364+ ('lt', gettext_noop('Lithuanian')),
365+ ('lv', gettext_noop('Latvian')),
366+ ('mk', gettext_noop('Macedonian')),
367+ ('ml', gettext_noop('Malayalam')),
368+ ('mn', gettext_noop('Mongolian')),
369+ ('nb', gettext_noop('Norwegian Bokmal')),
370+ ('ne', gettext_noop('Nepali')),
371+ ('nl', gettext_noop('Dutch')),
372+ ('nn', gettext_noop('Norwegian Nynorsk')),
373+ ('pa', gettext_noop('Punjabi')),
374+ ('pl', gettext_noop('Polish')),
375+ ('pt', gettext_noop('Portuguese')),
376+ ('pt-br', gettext_noop('Brazilian Portuguese')),
377+ ('ro', gettext_noop('Romanian')),
378+ ('ru', gettext_noop('Russian')),
379+ ('sk', gettext_noop('Slovak')),
380+ ('sl', gettext_noop('Slovenian')),
381+ ('sq', gettext_noop('Albanian')),
382+ ('sr', gettext_noop('Serbian')),
383+ ('sr-latn', gettext_noop('Serbian Latin')),
384+ ('sv', gettext_noop('Swedish')),
385+ ('sw', gettext_noop('Swahili')),
386+ ('ta', gettext_noop('Tamil')),
387+ ('te', gettext_noop('Telugu')),
388+ ('th', gettext_noop('Thai')),
389+ ('tr', gettext_noop('Turkish')),
390+ ('tt', gettext_noop('Tatar')),
391+ ('uk', gettext_noop('Ukrainian')),
392+ ('ur', gettext_noop('Urdu')),
393+ ('vi', gettext_noop('Vietnamese')),
394+ ('zh-cn', gettext_noop('Simplified Chinese')),
395+ ('zh-tw', gettext_noop('Traditional Chinese'))],
396+ help="Languages we provide translations for, out of the box. "
397+ "The language name should be the utf-8 encoded local name "
398+ "for the language")
399+
400+ session_cookie_httponly = BoolOption(
401+ default=True,
402+ help="Whether to use the non-RFC standard htt pOnly flag (IE, "
403+ "FF3+, others)")
404+
405+ datetime_input_formats = ListOption(
406+ item=StringOption(),
407+ default=[
408+ '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
409+ '%Y-%m-%d %H:%M:%S.%f', # '2006-10-25 14:30:59.000200'
410+ '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
411+ '%Y-%m-%d', # '2006-10-25'
412+ '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
413+ '%m/%d/%Y %H:%M:%S.%f', # '10/25/2006 14:30:59.000200'
414+ '%m/%d/%Y %H:%M', # '10/25/2006 14:30'
415+ '%m/%d/%Y', # '10/25/2006'
416+ '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59'
417+ '%m/%d/%y %H:%M:%S.%f', # '10/25/06 14:30:59.000200'
418+ '%m/%d/%y %H:%M', # '10/25/06 14:30'
419+ '%m/%d/%y', # '10/25/06'
420+ ],
421+ help="Default formats to be used when parsing dates and times "
422+ "from input boxes, in order")
423+
424+
425+class Django144Schema(Django14Schema):
426+ version = '1.4.4'
427+
428+ class django(Django14Schema.django):
429+
430+ allowed_hosts = ListOption(
431+ item=StringOption(),
432+ help="A list of strings representing the host/domain names "
433+ "that this Django site can serve. This is a security "
434+ "measure to prevent an attacker from poisoning caches and "
435+ "password reset emails with links to malicious hosts by "
436+ "submitting requests with a fake HTTP Host header, which is "
437+ "possible even under many seemingly-safe webserver "
438+ "configurations.")
439+
440
441 class DjangoSchemaFactory(object):
442 def __init__(self):
443@@ -1172,6 +1459,7 @@
444 return self._schemas[version]
445
446 msg = "No schema registered for version %r" % version
447+
448 if strict:
449 raise ValueError(msg)
450 else:
451@@ -1181,10 +1469,14 @@
452 schema = self.build(version)
453 return schema
454
455- def build(self, version_string=None, options=None):
456+ def build(self, version_string=None, options=None,
457+ BaseSchema=BaseDjangoSchema):
458 if version_string is None:
459 version_string = get_version()
460+
461 if options is None:
462+ project_settings = get_project_settings()
463+
464 options = dict([(name.lower(), value) for (name, value) in
465 inspect.getmembers(global_settings) if name.isupper()])
466 project_options = dict([(name.lower(), value) for (name, value) in
467@@ -1197,10 +1489,18 @@
468
469 options.update(project_options)
470
471- class DjangoSchema(Schema):
472+ try:
473+ base_version = '{0}.{1}'.format(*VERSION[:2])
474+ BaseSchema = self.get(base_version)
475+ except ValueError:
476+ pass
477+
478+ section_base_class = getattr(BaseSchema, 'django', Section)
479+
480+ class DjangoSchema(BaseSchema):
481 version = version_string
482
483- class django(Section):
484+ class django(section_base_class):
485 pass
486
487 def get_option_type(name, value):
488@@ -1214,8 +1514,18 @@
489 unicode: StringOption,
490 }
491 if value is None:
492+ # Special casing strange value, which by default is None but
493+ # should be set to tuple.
494+ if name == 'secure_proxy_ssl_header':
495+ return TupleOption(name=name, default=None)
496+
497 return StringOption(name=name, default=value, null=True)
498 else:
499+ # Clean up values comming from the project template and having
500+ # {{ }} substitutions in them.
501+ if name in ('secret_key', 'wsgi_application'):
502+ value = ''
503+
504 option_type = type_mappings[type(value)]
505 kwargs = {'name': name, 'default': value}
506
507@@ -1246,8 +1556,9 @@
508 for name, value in options.items():
509 if name == '__CONFIGGLUE_PARSER__':
510 continue
511- option = get_option_type(name, value)
512- setattr(DjangoSchema.django, name, option)
513+ if not hasattr(DjangoSchema.django, name):
514+ option = get_option_type(name, value)
515+ setattr(DjangoSchema.django, name, option)
516
517 # register schema for it to be available during next query
518 self.register(DjangoSchema, version_string)
519@@ -1256,14 +1567,9 @@
520
521 schemas = DjangoSchemaFactory()
522 schemas.register(BaseDjangoSchema)
523-schemas.register(BaseDjangoSchema, '1.0.4')
524-schemas.register(Django11Schema)
525-schemas.register(Django11Schema, '1.1.2')
526-schemas.register(Django11Schema, '1.1.4')
527-schemas.register(Django12Schema)
528-schemas.register(Django12Schema, '1.2.7')
529 schemas.register(Django13Schema)
530-for i in range(1, 6):
531- schemas.register(Django13Schema, '1.3.%d' % i)
532+schemas.register(Django136Schema)
533+schemas.register(Django136Schema, '1.3.7')
534 schemas.register(Django14Schema)
535-schemas.register(Django14Schema, '1.4.3')
536+schemas.register(Django144Schema)
537+schemas.register(Django144Schema, '1.4.5')
538
539=== modified file 'django_configglue/tests/helpers.py'
540--- django_configglue/tests/helpers.py 2011-08-05 16:12:53 +0000
541+++ django_configglue/tests/helpers.py 2013-03-22 14:01:30 +0000
542@@ -11,6 +11,10 @@
543 from django.core import management
544 from django.conf import settings
545 from django.test import TestCase
546+try:
547+ from django.utils.functional import empty
548+except ImportError:
549+ empty = None
550
551 from configglue.schema import (
552 ListOption,
553@@ -23,31 +27,25 @@
554 COMMAND = ''
555
556 def setUp(self):
557+ super(ConfigGlueDjangoCommandTestCase, self).setUp()
558 # disable logging during tests
559 self.level = logging.getLogger().level
560 logging.disable(logging.ERROR)
561
562 config = textwrap.dedent("""
563 [django]
564- database_engine = sqlite3
565- database_name = :memory:
566 installed_apps = django_configglue
567 time_zone = Europe/London
568+ databases = databases
569+
570+ [databases]
571+ default = db_default
572+
573+ [db_default]
574+ engine = django.db.backends.sqlite3
575+ name = :memory:
576 """)
577
578- if django.VERSION[:2] > (1, 1):
579- # since 1.2 use multi database settings format
580- config += textwrap.dedent("""
581- databases = databases
582-
583- [databases]
584- default = db_default
585-
586- [db_default]
587- engine = sqlite3
588- name = :memory:
589- """)
590-
591 self.set_config(config)
592 self._DJANGO_SETTINGS_MODULE = self.load_settings()
593
594@@ -60,6 +58,7 @@
595 self._DJANGO_SETTINGS_MODULE)
596
597 os.remove('test.cfg')
598+ super(ConfigGlueDjangoCommandTestCase, self).tearDown()
599
600 def set_config(self, config):
601 config_file = open('test.cfg', 'w')
602@@ -68,14 +67,11 @@
603
604 @property
605 def wrapped_settings(self):
606- wrapped = '_target'
607- if django.VERSION[:3] > (1, 0, 2):
608- wrapped = '_wrapped'
609 # make sure the wrapped object is not None
610 # by just querying it for a setting
611 getattr(settings, 'DEBUG', False)
612- assert(getattr(settings, wrapped) != None)
613- return wrapped
614+ assert(getattr(settings, '_wrapped') is not empty)
615+ return '_wrapped'
616
617 def load_settings(self, module='django_configglue.tests.settings'):
618 old_module = os.environ['DJANGO_SETTINGS_MODULE']
619@@ -90,8 +86,12 @@
620 'DATABASE_SUPPORTS_TRANSACTIONS': getattr(
621 settings, 'DATABASE_SUPPORTS_TRANSACTIONS'),
622 }
623+ # save _original_allowed_hosts so that the teardown will work
624+ # properly
625+ _original_allowed_hosts = getattr(
626+ settings, '_original_allowed_hosts', None)
627 # force django to reload its settings
628- setattr(settings, self.wrapped_settings, None)
629+ setattr(settings, self.wrapped_settings, empty)
630 # update settings module for next reload
631 os.environ['DJANGO_SETTINGS_MODULE'] = module
632
633@@ -99,6 +99,8 @@
634 for key, value in extra_settings.items():
635 setattr(settings, key, value)
636
637+ settings._original_allowed_hosts = _original_allowed_hosts
638+
639 if hasattr(self, 'extra_settings'):
640 for key, value in self.extra_settings.items():
641 setattr(settings, key, value)
642@@ -119,8 +121,7 @@
643 sys.stdout.seek(0)
644 sys.stderr.seek(0)
645
646- self.capture = {'stdout': sys.stdout.read(),
647- 'stderr': sys.stderr.read()}
648+ self.output = sys.stdout.read() + sys.stderr.read()
649
650 sys.stdout = self._stdout
651 sys.stderr = self._stderr
652@@ -135,6 +136,7 @@
653
654 class SchemaHelperTestCase(TestCase):
655 def setUp(self):
656+ super(SchemaHelperTestCase, self).setUp()
657 # disable logging during tests
658 self.level = logging.getLogger().level
659 logging.disable(logging.ERROR)
660@@ -142,10 +144,11 @@
661 def tearDown(self):
662 # re-enable logging
663 logging.getLogger().setLevel(self.level)
664+ super(SchemaHelperTestCase, self).tearDown()
665
666 def assert_schema_structure(self, schema_cls, version, options):
667 self.assertTrue(issubclass(schema_cls, Schema))
668- self.assertEqual(schema_cls.version, 'bogus')
669+ self.assertEqual(schema_cls.version, version)
670
671 schema = schema_cls()
672
673@@ -187,8 +190,11 @@
674 # django defaults to tuples for options it defines as lists
675 if (isinstance(option, (ListOption, TupleOption)) or
676 isinstance(expected_option, (ListOption, TupleOption))):
677- self.assertEqual(list(option.default),
678- list(expected_option.default))
679+ if option.default is None:
680+ self.assertIsNone(expected_option.default)
681+ else:
682+ self.assertEqual(list(option.default),
683+ list(expected_option.default))
684 else:
685 self.assertEqual(option.default,
686 expected_option.default)
687
688=== modified file 'django_configglue/tests/settings.py'
689--- django_configglue/tests/settings.py 2011-07-19 01:24:06 +0000
690+++ django_configglue/tests/settings.py 2013-03-22 14:01:30 +0000
691@@ -10,9 +10,9 @@
692
693 version = DjangoSchema.version
694 main_cfg = 'main.cfg'
695-if version >= '1.3':
696+if version >= '1.4':
697+ main_cfg = 'main-14.cfg'
698+elif version >= '1.3':
699 main_cfg = 'main-13.cfg'
700-elif version >= '1.2':
701- main_cfg = 'main-12.cfg'
702
703 configglue(DjangoSchema, [main_cfg, 'test.cfg'], __name__)
704
705=== modified file 'django_configglue/tests/test_configglue.py'
706--- django_configglue/tests/test_configglue.py 2013-02-20 08:51:07 +0000
707+++ django_configglue/tests/test_configglue.py 2013-03-22 14:01:30 +0000
708@@ -25,7 +25,6 @@
709 )
710 from django.conf import settings
711 from django.conf import global_settings
712-from django.conf.project_template import settings as project_settings
713 from mock import patch
714
715 from django_configglue.management import GlueManagementUtility, LaxOptionParser
716@@ -34,6 +33,7 @@
717 configglue,
718 get_django_settings,
719 update_settings,
720+ get_project_settings,
721 )
722 from django_configglue.schema import (
723 BaseDjangoSchema,
724@@ -100,11 +100,11 @@
725 self.assertRaises(ValueError, schemas.get, '1.1.1', strict=True)
726
727 def test_schema_versions(self):
728- django_10 = schemas.get('1.0')()
729- django_11 = schemas.get('1.1')()
730- self.assertEqual(django_10.version, '1.0')
731- self.assertEqual(django_11.version, '1.1')
732- self.assertFalse(django_10 is django_11)
733+ django_13 = schemas.get('1.3')()
734+ django_14 = schemas.get('1.4')()
735+ self.assertEqual(django_13.version, '1.3')
736+ self.assertEqual(django_14.version, '1.4')
737+ self.assertFalse(django_13 is django_14)
738
739 def test_register_without_version(self):
740 class MySchema(Schema):
741@@ -156,10 +156,10 @@
742 (StringOption, 'foo'),
743 ]
744
745- for opt_type, default in data:
746- schema_cls = schemas.build('bogus', {'foo': default})
747+ for i, (opt_type, default) in enumerate(data):
748+ schema_cls = schemas.build('bogus.%d' % i, {'foo': default}, Schema)
749 # do common checks
750- self.assert_schema_structure(schema_cls, 'bogus',
751+ self.assert_schema_structure(schema_cls, 'bogus.%d' % i,
752 {'foo': opt_type(name='foo', default=default)})
753
754 def test_schemafactory_build_django(self):
755@@ -231,7 +231,7 @@
756 options = dict([(name.lower(), value) for (name, value) in
757 inspect.getmembers(global_settings) if name.isupper()])
758 project_options = dict([(name.lower(), value) for (name, value) in
759- inspect.getmembers(project_settings) if name.isupper()])
760+ inspect.getmembers(get_project_settings()) if name.isupper()])
761 # handle special case of ROOT_URLCONF which depends on the
762 # project name
763 root_urlconf = project_options['root_urlconf'].replace(
764@@ -353,38 +353,52 @@
765
766 def test_execute_no_args(self):
767 self.util.argv = ['']
768- self.assertRaises(SystemExit, self.execute)
769+ try:
770+ self.execute()
771+ except SystemExit:
772+ pass
773 self.assertTrue(
774 "Type '%s help <subcommand>' for help" % (self.util.prog_name,) in
775- self.capture['stderr'])
776+ self.output)
777
778- def test_execute_help(self):
779+ @patch('sys.stdout')
780+ def test_execute_help(self, mock_stdout):
781+ mock_stdout.isatty.return_value = False
782 self.util.argv = ['', 'help']
783- self.assertRaises(SystemExit, self.execute)
784- self.assertTrue(self.util.main_help_text() in self.capture['stderr'])
785+ try:
786+ self.execute()
787+ except SystemExit:
788+ # In earlier versions than 1.4, help was raising SystemExit
789+ pass
790+ self.assertTrue(self.util.main_help_text() in self.output)
791
792- def test_execute_help_option(self):
793+ @patch('sys.stdout')
794+ def test_execute_help_option(self, mock_stdout):
795+ mock_stdout.isatty.return_value = False
796 self.util.argv = ['', '--help']
797 self.execute()
798- self.assertTrue(self.util.main_help_text() in self.capture['stderr'])
799+ self.assertTrue(self.util.main_help_text() in self.output)
800
801 def test_execute_help_for_command(self):
802 self.util.argv = ['', 'help', 'settings']
803 self.execute()
804- self.assertTrue('Show settings attributes' in self.capture['stdout'])
805+ self.assertTrue('Show settings attributes' in self.output)
806
807 def test_execute_version(self):
808 from django import get_version
809 self.util.argv = ['', '--version']
810 self.execute()
811- self.assertTrue(get_version() in self.capture['stdout'])
812+ self.assertTrue(get_version() in self.output)
813
814 def test_execute(self):
815 self.util.argv = ['', 'settings']
816 self.execute()
817- self.assertTrue('Show settings attributes' in self.capture['stdout'])
818-
819- def test_execute_settings_exception(self):
820+ self.assertTrue('Show settings attributes' in self.output)
821+
822+ @patch('sys.stdout')
823+ def test_execute_settings_exception(self, mock_stdout):
824+ mock_stdout.isatty.return_value = False
825+
826 from django.conf import settings
827 wrapped = getattr(settings, self.wrapped_settings)
828 old_CONFIGGLUE_PARSER = wrapped.__CONFIGGLUE_PARSER__
829@@ -392,16 +406,19 @@
830
831 try:
832 self.util.argv = ['', 'help']
833- self.assertRaises(SystemExit, self.execute)
834+ try:
835+ self.execute()
836+ except SystemExit:
837+ pass
838 self.assertTrue(
839- self.util.main_help_text() in self.capture['stderr'])
840+ self.util.main_help_text() in self.output)
841 finally:
842 wrapped.__CONFIGGLUE_PARSER__ = old_CONFIGGLUE_PARSER
843
844 def test_execute_with_schema_options(self):
845 self.util.argv = ['', '--django_debug=False', 'help', 'settings']
846 self.execute()
847- self.assertTrue('Show settings attributes' in self.capture['stdout'])
848+ self.assertTrue('Show settings attributes' in self.output)
849
850 def test_verbosity_is_preserved(self):
851 self.util.argv = ['', 'settings', '--verbosity=2']
852
853=== modified file 'django_configglue/tests/test_settings.py'
854--- django_configglue/tests/test_settings.py 2012-10-15 14:21:03 +0000
855+++ django_configglue/tests/test_settings.py 2013-03-22 14:01:30 +0000
856@@ -23,30 +23,27 @@
857
858 def test_no_args(self):
859 self.call_command()
860- self.assertTrue(self.capture['stdout'].startswith('Usage: '))
861+ self.assertTrue(self.output.startswith('Usage: '))
862
863 def test_get(self):
864 self.call_command('installed_apps')
865 expected_output = "INSTALLED_APPS = ['django_configglue']"
866- self.assertEqual(self.capture['stdout'].strip(), expected_output)
867+ self.assertEqual(self.output.strip(), expected_output)
868
869 def test_get_not_found(self):
870 self.call_command('bogus')
871 expected_output = "setting BOGUS not found"
872- self.assertEqual(self.capture['stdout'].strip(), expected_output)
873+ self.assertEqual(self.output.strip(), expected_output)
874
875 def test_show(self):
876 expected_values = [
877+ 'SITE_ID = 1',
878+ "SETTINGS_MODULE = 'django_configglue.tests.settings'",
879 "ROOT_URLCONF = 'urls'",
880- "SITE_ID = 1",
881- "SETTINGS_MODULE = 'django_configglue.tests.settings'",
882 "SETTINGS_ENCODING = '%s'" % SETTINGS_ENCODING,
883 ]
884- django_version = django.VERSION[:2]
885- if django_version == (1, 1):
886- expected_values.append("DATABASE_SUPPORTS_TRANSACTIONS = True")
887 self.call_command(show_current=True)
888- output_lines = self.capture['stdout'].strip().split('\n')
889+ output_lines = self.output.strip().split('\n')
890 self.assertEqual(set(expected_values), set(output_lines))
891
892 def test_show_global(self):
893@@ -55,7 +52,7 @@
894 dir(settings) if self.is_setting(key)])
895 # process output into dictionary
896 items = map(lambda x: x.split(' = '),
897- self.capture['stdout'].strip().split('\n'))
898+ self.output.strip().split('\n'))
899 items = map(lambda x: (x[0].strip(), eval(x[1].strip())),
900 (t for t in items if self.is_setting(t[0])))
901 output = dict(items)
902@@ -66,12 +63,12 @@
903 self.call_command('time_zone', locate=True)
904 location = os.path.join(os.path.realpath(os.path.curdir), 'test.cfg')
905 expected_output = "setting TIME_ZONE last defined in '%s'" % location
906- self.assertEqual(self.capture['stdout'].strip(), expected_output)
907+ self.assertEqual(self.output.strip(), expected_output)
908
909 def test_locate_setting_not_found(self):
910 self.call_command('bogus', locate=True)
911 expected_output = 'setting BOGUS not found'
912- self.assertEqual(self.capture['stdout'].strip(), expected_output)
913+ self.assertEqual(self.output.strip(), expected_output)
914
915 def test_locate_setting_no_configglue_parser(self):
916 wrapped = getattr(settings, self.wrapped_settings)
917@@ -84,7 +81,7 @@
918 locals(), [''])
919 location = os.path.realpath(mod.__file__)
920 expected_output = "setting TIME_ZONE last defined in %r" % location
921- self.assertEqual(self.capture['stdout'].strip(), expected_output)
922+ self.assertEqual(self.output.strip(), expected_output)
923 finally:
924 wrapped.__CONFIGGLUE_PARSER__ = old_CONFIGGLUE_PARSER
925
926@@ -96,7 +93,7 @@
927 try:
928 self.call_command('bogus', locate=True)
929 expected_output = 'setting BOGUS not found'
930- self.assertEqual(self.capture['stdout'].strip(), expected_output)
931+ self.assertEqual(self.output.strip(), expected_output)
932 finally:
933 wrapped.__CONFIGGLUE_PARSER__ = old_CONFIGGLUE_PARSER
934
935@@ -115,10 +112,11 @@
936 class GeneratedSettingsTestCase(ConfigGlueDjangoCommandTestCase,
937 SchemaHelperTestCase):
938 def setUp(self):
939+ super(GeneratedSettingsTestCase, self).setUp()
940 self.expected_schema = schemas.get(
941 django.get_version(), strict=True)()
942-
943- mock_get_version = Mock(return_value='foo')
944+ self.version = '.'.join(django.get_version().split('.')[:2]) + '.foo'
945+ mock_get_version = Mock(return_value=self.version)
946 self.patch_get_version = patch(
947 'django_configglue.tests.settings.django.get_version',
948 mock_get_version)
949@@ -127,7 +125,6 @@
950 self.patch_warn = patch(
951 'django_configglue.schema.logging.warn', self.mock_warn)
952 self.patch_warn.start()
953- super(GeneratedSettingsTestCase, self).setUp()
954
955 def tearDown(self):
956 self.patch_get_version.stop()
957@@ -137,22 +134,29 @@
958 def test_generated_schema(self):
959 # import here so that the necessary modules can be mocked before
960 # being required
961+ self.load_settings()
962+
963 from django.conf import settings
964 schema = settings.__CONFIGGLUE_PARSER__.schema
965
966 self.assert_schemas_equal(schema, self.expected_schema)
967 self.assertEqual(self.mock_warn.call_args_list,
968- [(("No schema registered for version 'foo'",), {}),
969- (("Dynamically creating schema for version 'foo'",), {})])
970+ [(("No schema registered for version '%s'" % self.version,),
971+ {}),
972+ (("Dynamically creating schema for version '%s'" % self.version,),
973+ {})])
974
975
976 class ValidateCommandTestCase(ConfigGlueDjangoCommandTestCase):
977 COMMAND = 'settings'
978
979 def test_valid_config(self):
980- self.call_command(validate=True)
981+ try:
982+ self.call_command(validate=True)
983+ except SystemExit:
984+ pass
985 expected_output = 'Settings appear to be fine.'
986- self.assertEqual(self.capture['stdout'].strip(), expected_output)
987+ self.assertEqual(self.output.strip(), expected_output)
988
989 def test_invalid_config(self):
990 config = """
991@@ -168,7 +172,7 @@
992 except SystemExit, e:
993 self.assertEqual(e.code, 1)
994 error_msg = 'Error: Settings did not validate against schema.'
995- self.assertTrue(self.capture['stderr'].strip().startswith(
996+ self.assertTrue(self.output.strip().startswith(
997 error_msg))
998
999 def test_no_configglue_parser(self):
1000@@ -180,7 +184,7 @@
1001 self.call_command(validate=True)
1002 expected_output = ('The settings module was not generated by '
1003 'configglue. Can only validate configglue generated settings.')
1004- self.assertEqual(self.capture['stdout'].strip(), expected_output)
1005+ self.assertEqual(self.output.strip(), expected_output)
1006 finally:
1007 wrapped.__CONFIGGLUE_PARSER__ = old_CONFIGGLUE_PARSER
1008
1009@@ -190,7 +194,7 @@
1010
1011 def test_help(self):
1012 self.call_command()
1013- self.assertTrue('--django_debug' in self.capture['stdout'])
1014+ self.assertTrue('--django_debug' in self.output)
1015
1016 def test_update_settings(self):
1017 self.assertTrue(settings.DEBUG)
1018@@ -201,7 +205,7 @@
1019 utility.execute()
1020 finally:
1021 self.end_capture()
1022- self.assertTrue('False' in self.capture['stdout'])
1023+ self.assertTrue('False' in self.output)
1024
1025 def test_version_is_printed_once(self):
1026 args = ['manage.py', '--version']
1027@@ -212,14 +216,18 @@
1028 finally:
1029 self.end_capture()
1030 expected = get_version()
1031- self.assertEqual(1, self.capture['stdout'].count(expected))
1032+ self.assertEqual(1, self.output.count(expected))
1033
1034 def test_noargs_doesnt_error(self):
1035 args = ['manage.py']
1036 utility = ManagementUtility(argv=args)
1037 self.begin_capture()
1038 try:
1039- self.assertRaises(SystemExit, utility.execute)
1040+ utility.execute()
1041+ except SystemExit:
1042+ # Django <= 1.3 uses SystemExit to terminate management
1043+ # command
1044+ pass
1045 finally:
1046 self.end_capture()
1047- self.assertFalse('Unknown command' in self.capture['stdout'])
1048+ self.assertFalse('Unknown command' in self.output)
1049
1050=== modified file 'django_configglue/utils.py'
1051--- django_configglue/utils.py 2011-07-27 00:14:34 +0000
1052+++ django_configglue/utils.py 2013-03-22 14:01:30 +0000
1053@@ -1,5 +1,7 @@
1054 # Copyright 2010-2011 Canonical Ltd. This software is licensed under the
1055 # GNU Lesser General Public License version 3 (see the file LICENSE).
1056+import imp
1057+import os
1058 import sys
1059
1060 from configglue.parser import SchemaConfigParser
1061@@ -56,3 +58,16 @@
1062 scp.read(configs)
1063 update_settings(scp, target)
1064 return scp
1065+
1066+
1067+def get_project_settings():
1068+ """Find project_template settings file, in 1.4 you can't just use import"""
1069+ from django import conf
1070+
1071+ project_template_dir = os.path.join(
1072+ os.path.dirname(conf.__file__),
1073+ 'project_template')
1074+ for root, dirs, files in os.walk(project_template_dir):
1075+ if 'settings.py' in files:
1076+ return imp.load_source('project_settings',
1077+ os.path.join(root, 'settings.py'))
1078
1079=== modified file 'setup.py'
1080--- setup.py 2011-07-27 00:14:34 +0000
1081+++ setup.py 2013-03-22 14:01:30 +0000
1082@@ -38,7 +38,7 @@
1083
1084 # content
1085 packages=find_packages(exclude=['testproject*']),
1086- install_requires=['django >= 1.0.2-final', 'configglue >= 1.0'],
1087+ install_requires=['django >= 1.3.7', 'configglue >= 1.0'],
1088
1089 # tests
1090 test_suite='testproject.testrunner.runtests',
1091
1092=== removed file 'testproject/main-12.cfg'
1093--- testproject/main-12.cfg 2011-04-11 01:06:16 +0000
1094+++ testproject/main-12.cfg 1970-01-01 00:00:00 +0000
1095@@ -1,12 +0,0 @@
1096-[django]
1097-database_engine = sqlite3
1098-database_name = :memory:
1099-installed_apps = django_configglue
1100-databases = databases
1101-
1102-[databases]
1103-default = db_default
1104-
1105-[db_default]
1106-engine = sqlite3
1107-name = :memory:
1108
1109=== added file 'testproject/main-14.cfg'
1110--- testproject/main-14.cfg 1970-01-01 00:00:00 +0000
1111+++ testproject/main-14.cfg 2013-03-22 14:01:30 +0000
1112@@ -0,0 +1,10 @@
1113+[django]
1114+installed_apps = django_configglue
1115+databases = databases
1116+
1117+[databases]
1118+default = db_default
1119+
1120+[db_default]
1121+engine = django.db.backends.sqlite3
1122+name = :memory:
1123
1124=== modified file 'testproject/main.cfg'
1125--- testproject/main.cfg 2011-04-11 01:06:16 +0000
1126+++ testproject/main.cfg 2013-03-22 14:01:30 +0000
1127@@ -1,4 +1,2 @@
1128 [django]
1129-database_engine = sqlite3
1130-database_name = :memory:
1131 installed_apps = django_configglue
1132
1133=== modified file 'tox.ini'
1134--- tox.ini 2013-02-20 08:48:09 +0000
1135+++ tox.ini 2013-03-22 14:01:30 +0000
1136@@ -1,73 +1,49 @@
1137 [tox]
1138 envlist =
1139- py26-django114,
1140- py26-django127,
1141- py26-django135,
1142- py26-django143,
1143- py27-django114,
1144- py27-django127,
1145- py27-django135,
1146- py27-django143,
1147+ py26-django137,
1148+ py26-django141,
1149+ py26-django145,
1150+ py27-django137,
1151+ py27-django145,
1152 configglue-dev
1153
1154 [testenv]
1155 commands = python setup.py test
1156
1157-[testenv:py26-django114]
1158-basepython = python2.6
1159-deps =
1160- configglue
1161- mock
1162- django==1.1.4
1163-
1164-[testenv:py26-django127]
1165-basepython = python2.6
1166-deps =
1167- configglue
1168- mock
1169- django==1.2.7
1170-
1171-[testenv:py26-django135]
1172-basepython = python2.6
1173-deps =
1174- configglue
1175- mock
1176- django==1.3.5
1177-
1178-[testenv:py26-django143]
1179-basepython = python2.6
1180-deps =
1181- configglue
1182- mock
1183- django==1.4.3
1184-
1185-[testenv:py27-django114]
1186-basepython = python2.7
1187-deps =
1188- configglue
1189- mock
1190- django==1.1.4
1191-
1192-[testenv:py27-django127]
1193-basepython = python2.7
1194-deps =
1195- configglue
1196- mock
1197- django==1.2.7
1198-
1199-[testenv:py27-django135]
1200-basepython = python2.7
1201-deps =
1202- configglue
1203- mock
1204- django==1.3.5
1205-
1206-[testenv:py27-django143]
1207-basepython = python2.7
1208-deps =
1209- configglue
1210- mock
1211- django==1.4.3
1212+[testenv:py26-django137]
1213+basepython = python2.6
1214+deps =
1215+ configglue
1216+ mock
1217+ django==1.3.7
1218+
1219+[testenv:py26-django145]
1220+basepython = python2.6
1221+deps =
1222+ configglue
1223+ mock
1224+ django==1.4.5
1225+
1226+[testenv:py26-django141]
1227+basepython = python2.6
1228+deps =
1229+ configglue
1230+ mock
1231+ django==1.4.1
1232+
1233+[testenv:py27-django137]
1234+basepython = python2.7
1235+deps =
1236+ configglue
1237+ mock
1238+ django==1.3.7
1239+
1240+[testenv:py27-django145]
1241+basepython = python2.7
1242+deps =
1243+ configglue
1244+ mock
1245+ django==1.4.5
1246
1247 [testenv:configglue-dev]
1248 deps =

Subscribers

People subscribed via source and target branches