Merge lp:~ricardokirkner/django-pgtools/support-django-1.6-to-1.10 into lp:django-pgtools
- support-django-1.6-to-1.10
- Merge into trunk
Proposed by
Ricardo Kirkner
Status: | Merged |
---|---|
Approved by: | Ricardo Kirkner |
Approved revision: | 20 |
Merged at revision: | 9 |
Proposed branch: | lp:~ricardokirkner/django-pgtools/support-django-1.6-to-1.10 |
Merge into: | lp:django-pgtools |
Diff against target: |
1099 lines (+471/-280) 17 files modified
.bzrignore (+4/-1) Makefile (+51/-0) Makefile.db (+44/-0) manage.py (+10/-0) pgtools/dbrole.py (+6/-14) pgtools/dbuser.py (+5/-9) pgtools/management/commands/createuser.py (+35/-10) pgtools/management/commands/deleteuser.py (+20/-3) pgtools/management/commands/grantuser.py (+19/-12) pgtools/management/commands/listusers.py (+18/-9) pgtools/tests.py (+128/-79) pgtools/utils.py (+64/-10) requirements.txt (+1/-1) setup.cfg (+2/-0) setup.py (+4/-53) test_project/settings.py (+29/-0) tox.ini (+31/-79) |
To merge this branch: | bzr merge lp:~ricardokirkner/django-pgtools/support-django-1.6-to-1.10 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Fabián Ezequiel Gallina (community) | Approve | ||
Review via email: mp+314917@code.launchpad.net |
Commit message
support django 1.6 to 1.10
- dropped support for python 2.5 and 2.6
- dropped support for django < 1.6
- improved bootstrapping and testing of project
Description of the change
To post a comment you must log in.
- 18. By Ricardo Kirkner
-
swapped distutils for setuptools
- 19. By Ricardo Kirkner
-
support building universal wheels
- 20. By Ricardo Kirkner
-
better version of get_models that supports django 1.6 to 1.10
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2011-05-30 14:30:10 +0000 |
3 | +++ .bzrignore 2017-01-17 15:08:22 +0000 |
4 | @@ -1,1 +1,4 @@ |
5 | -.tox |
6 | \ No newline at end of file |
7 | +.tox |
8 | +*.pyc |
9 | + |
10 | +env |
11 | |
12 | === added file 'Makefile' |
13 | --- Makefile 1970-01-01 00:00:00 +0000 |
14 | +++ Makefile 2017-01-17 15:08:22 +0000 |
15 | @@ -0,0 +1,51 @@ |
16 | +.PHONY: help |
17 | +.DEFAULT_GOAL := help |
18 | + |
19 | +DJANGO_SETTINGS_MODULE ?= test_project.settings |
20 | +ENV = $(CURDIR)/env |
21 | +ENV_CREATED = $(shell test -e $(PYTHON) && echo "yes" || echo "no") |
22 | +PIP = $(ENV)/bin/pip |
23 | +PYTHON ?= $(ENV)/bin/python |
24 | +export DJANGO_SETTINGS_MODULE |
25 | +export PYTHONPATH |
26 | + |
27 | +help: |
28 | + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ |
29 | + | cut -d ':' -f 2,3 \ |
30 | + | sort \ |
31 | + | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' |
32 | + |
33 | +# pre-process arguments |
34 | +# if the first argument is "special" (manage, test)... |
35 | +ifneq (,$(filter $(firstword $(MAKECMDGOALS)),manage test)) |
36 | +# use the rest as sub-arguments |
37 | +ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) |
38 | +# ...and turn them into do-nothing targets |
39 | +$(eval $(ARGS):;@:) |
40 | +endif |
41 | + |
42 | +bootstrap: ARGS=-r requirements.txt |
43 | +bootstrap: $(ENV) ## Bootstrap the project |
44 | + @$(PIP) install $(ARGS) |
45 | + |
46 | +$(ENV): |
47 | + @virtualenv --clear $(ENV) |
48 | + |
49 | +clean: ## Remove build artifacts |
50 | + @rm -rf $(WHEELS_DIR) |
51 | + @find -name '*.pyc' -delete |
52 | + @find -name '*.~*' -delete |
53 | + |
54 | +clean-env: ## Remove virtualenv |
55 | + @rm -rf $(ENV) |
56 | + |
57 | +manage: ## Run django management commands |
58 | + @$(PYTHON) manage.py $(ARGS) |
59 | + |
60 | +test: ## Run unit tests |
61 | + @$(PYTHON) manage.py test $(ARGS) |
62 | + |
63 | +# database related targets |
64 | +ifeq (yes,$(ENV_CREATED)) |
65 | +include Makefile.db |
66 | +endif |
67 | |
68 | === added file 'Makefile.db' |
69 | --- Makefile.db 1970-01-01 00:00:00 +0000 |
70 | +++ Makefile.db 2017-01-17 15:08:22 +0000 |
71 | @@ -0,0 +1,44 @@ |
72 | +PGNAME = $(shell DJANGO_SETTINGS_MODULE=$(DJANGO_SETTINGS_MODULE) PYTHONPATH=$(PYTHONPATH) $(PYTHON) -c "from django.conf import settings; print settings.DATABASES['default']['NAME']") |
73 | +PGHOST = $(shell DJANGO_SETTINGS_MODULE=$(DJANGO_SETTINGS_MODULE) PYTHONPATH=$(PYTHONPATH) $(PYTHON) -c "from django.conf import settings; print settings.DATABASES['default']['HOST']") |
74 | +PGUSER = $(shell DJANGO_SETTINGS_MODULE=$(DJANGO_SETTINGS_MODULE) PYTHONPATH=$(PYTHONPATH) $(PYTHON) -c "from django.conf import settings; print settings.DATABASES['default']['USER']") |
75 | +DATA_DIR = $(PGHOST)/data |
76 | +LOG_FILE = $(PGHOST)/postgresql.log |
77 | +CONF_FILE = $(PGHOST)/postgresql.conf |
78 | +PGBIN = /usr/lib/postgresql/9.3/bin |
79 | +PGCTL = $(PGBIN)/pg_ctl |
80 | +PGINIT = $(PGBIN)/initdb |
81 | +PGSOCKET = $(PGHOST)/.s.PGSQL.5432 |
82 | + |
83 | +# The weird setup-db/pgsocket dependency is so that other rules can depend |
84 | +# on setup-db which is nice, but the behavior is that if the socket exists, |
85 | +# the database won't be started again. |
86 | +setup-db: $(PGSOCKET) |
87 | + |
88 | +$(PGSOCKET): |
89 | + # If the socket didn't exist, means the db server was stopped. |
90 | + # In this case it should be safe to blast the directory as developers |
91 | + # don't usually stop the db server if they care about the (transient, |
92 | + # anyway) data. |
93 | + rm -rf $(DATA_DIR) |
94 | + mkdir -p $(DATA_DIR) |
95 | + $(PGINIT) -A trust -D $(DATA_DIR) |
96 | + echo "fsync = off" > $(CONF_FILE) |
97 | + echo "standard_conforming_strings = off" >> $(CONF_FILE) |
98 | + echo "escape_string_warning = off" >> $(CONF_FILE) |
99 | + $(PGCTL) start -w -D $(DATA_DIR) -l $(LOG_FILE) -o "-F -k $(PGHOST) -h ''" |
100 | + PGHOST=$(PGHOST) createdb $(PGNAME) |
101 | + PGHOST=$(PGHOST) createuser --superuser --createdb $(PGUSER) |
102 | + |
103 | +start-db: ## Start database server |
104 | + $(MAKE) setup-db |
105 | + $(MAKE) manage migrate |
106 | + |
107 | +stop-db: ## Stop database server |
108 | + PGHOST=$(PGHOST) dropdb $(PGNAME) |
109 | + $(PGCTL) stop -w -D $(DATA_DIR) -m smart |
110 | + rm -rf $(PGHOST) |
111 | + |
112 | +reset-db: ## Reset database state |
113 | + PGHOST=$(PGHOST) dropdb $(PGNAME) |
114 | + PGHOST=$(PGHOST) createdb $(PGNAME) |
115 | + $(MAKE) manage migrate |
116 | |
117 | === added file 'manage.py' |
118 | --- manage.py 1970-01-01 00:00:00 +0000 |
119 | +++ manage.py 2017-01-17 15:08:22 +0000 |
120 | @@ -0,0 +1,10 @@ |
121 | +#!/usr/bin/env python |
122 | +import os |
123 | +import sys |
124 | + |
125 | +if __name__ == "__main__": |
126 | + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") |
127 | + |
128 | + from django.core.management import execute_from_command_line |
129 | + |
130 | + execute_from_command_line(sys.argv) |
131 | |
132 | === modified file 'pgtools/dbrole.py' |
133 | --- pgtools/dbrole.py 2011-05-30 14:30:10 +0000 |
134 | +++ pgtools/dbrole.py 2017-01-17 15:08:22 +0000 |
135 | @@ -24,20 +24,16 @@ |
136 | |
137 | cursor = self.connection.cursor() |
138 | cursor.execute("CREATE ROLE %s" % rolename) |
139 | - if commit: |
140 | - self.transaction.commit_unless_managed() |
141 | - if not self.exists(rolename): |
142 | - raise CommandError("Role couldn't be created: %s" % rolename) |
143 | + if commit and not self.exists(rolename): |
144 | + raise CommandError("Role couldn't be created: %s" % rolename) |
145 | elif not exists: |
146 | raise CommandError("Role doesn't exist: %s" % rolename) |
147 | |
148 | def delete(self, commit=True): |
149 | cursor = self.connection.cursor() |
150 | cursor.execute("DROP ROLE %s" % self.rolename) |
151 | - if commit: |
152 | - self.transaction.commit_unless_managed() |
153 | - if self.exists(self.rolename): |
154 | - raise CommandError("Role cannot be deleted: %s" % self.rolename) |
155 | + if commit and self.exists(self.rolename): |
156 | + raise CommandError("Role cannot be deleted: %s" % self.rolename) |
157 | |
158 | @classmethod |
159 | def exists(cls, rolename, db_connection=None): |
160 | @@ -65,13 +61,9 @@ |
161 | def grant(self, user, commit=True): |
162 | cursor = self.connection.cursor() |
163 | cursor.execute("GRANT %s TO %s" % (self.rolename, user.username)) |
164 | - if commit: |
165 | - self.transaction.commit_unless_managed() |
166 | |
167 | def revoke(self, user, commit=True): |
168 | cursor = self.connection.cursor() |
169 | cursor.execute("REVOKE %s FROM %s" % (self.rolename, user.username)) |
170 | - if commit: |
171 | - self.transaction.commit_unless_managed() |
172 | - if user.has_role(self): |
173 | - raise CommandError("Role cannot be revoked: %s" % self.rolename) |
174 | + if commit and user.has_role(self): |
175 | + raise CommandError("Role cannot be revoked: %s" % self.rolename) |
176 | |
177 | === modified file 'pgtools/dbuser.py' |
178 | --- pgtools/dbuser.py 2011-05-30 14:30:10 +0000 |
179 | +++ pgtools/dbuser.py 2017-01-17 15:08:22 +0000 |
180 | @@ -27,21 +27,17 @@ |
181 | else: |
182 | cursor.execute("CREATE USER %s PASSWORD %%s" % username, |
183 | (password,)) |
184 | - if commit: |
185 | - self.transaction.commit_unless_managed() |
186 | - if not self.exists(username): |
187 | - raise CommandError( |
188 | - "User couldn't be created: %s" % username) |
189 | + if commit and not self.exists(username): |
190 | + raise CommandError( |
191 | + "User couldn't be created: %s" % username) |
192 | elif not exists: |
193 | raise CommandError("User doesn't exist: %s" % username) |
194 | |
195 | def delete(self, commit=True): |
196 | cursor = self.connection.cursor() |
197 | cursor.execute("DROP USER %s" % self.username) |
198 | - if commit: |
199 | - self.transaction.commit_unless_managed() |
200 | - if self.exists(self.username): |
201 | - raise CommandError("User cannot be deleted: %s" % self.username) |
202 | + if commit and self.exists(self.username): |
203 | + raise CommandError("User cannot be deleted: %s" % self.username) |
204 | |
205 | @classmethod |
206 | def exists(cls, username, db_connection=None): |
207 | |
208 | === modified file 'pgtools/management/commands/createuser.py' |
209 | --- pgtools/management/commands/createuser.py 2011-05-30 14:30:10 +0000 |
210 | +++ pgtools/management/commands/createuser.py 2017-01-17 15:08:22 +0000 |
211 | @@ -11,19 +11,44 @@ |
212 | from pgtools.dbrole import DatabaseRole |
213 | from pgtools.dbuser import DatabaseUser |
214 | from pgtools.decorators import graceful_db_errors |
215 | -from pgtools.utils import check_database_engine, parse_username_and_rolename |
216 | +from pgtools.utils import ( |
217 | + DJANGO_VERSION, |
218 | + check_database_engine, |
219 | + parse_username_and_rolename, |
220 | +) |
221 | |
222 | |
223 | class Command(BaseCommand): |
224 | - option_list = BaseCommand.option_list + ( |
225 | - make_option('-p', '--password', default=None, dest='password', |
226 | - help='Password of the user to be created.'), |
227 | - make_option('-P', '--no-password', default=False, dest='no_password', |
228 | - action='store_true', |
229 | - help='If given, then no password is set up for the user.'), |
230 | - ) |
231 | help = 'Create a new database user based on an existing role.' |
232 | - args = 'username [rolename]' |
233 | + OPTIONS = [ |
234 | + (('-p', '--password'), |
235 | + dict(default=None, dest='password', |
236 | + help='Password of the user to be created.')), |
237 | + (('-P', '--no-password'), |
238 | + dict(default=False, dest='no_password', |
239 | + help='If given, then no password is set up for the user.')), |
240 | + ] |
241 | + |
242 | + def __init__(self, *args, **kwargs): |
243 | + super(Command, self).__init__(*args, **kwargs) |
244 | + if DJANGO_VERSION < (1, 8): |
245 | + self.args = "username [rolename]" |
246 | + self.option_list = BaseCommand.option_list + tuple( |
247 | + make_option(*a, **kw) for (a, kw) in self.OPTIONS |
248 | + ) |
249 | + |
250 | + def add_arguments(self, parser): |
251 | + # positional arguments |
252 | + parser.add_argument( |
253 | + 'username', metavar='username', nargs=1, |
254 | + help='Username for the new user.') |
255 | + parser.add_argument( |
256 | + 'rolename', metavar='rolename', nargs='?', |
257 | + help='Role to use for creating the new user.') |
258 | + |
259 | + # named arguments |
260 | + for (args, kwargs) in self.OPTIONS: |
261 | + parser.add_argument(*args, **kwargs) |
262 | |
263 | def _ask_password(self): |
264 | password = None |
265 | @@ -50,7 +75,7 @@ |
266 | @graceful_db_errors |
267 | def handle(self, *args, **options): |
268 | check_database_engine() |
269 | - username, rolename = parse_username_and_rolename(args) |
270 | + username, rolename = parse_username_and_rolename(*args, **options) |
271 | |
272 | password = options.get('password') |
273 | no_password = options.get('no_password') |
274 | |
275 | === modified file 'pgtools/management/commands/deleteuser.py' |
276 | --- pgtools/management/commands/deleteuser.py 2011-05-30 14:30:10 +0000 |
277 | +++ pgtools/management/commands/deleteuser.py 2017-01-17 15:08:22 +0000 |
278 | @@ -7,17 +7,34 @@ |
279 | from pgtools.dbrole import DatabaseRole |
280 | from pgtools.dbuser import DatabaseUser |
281 | from pgtools.decorators import graceful_db_errors |
282 | -from pgtools.utils import check_database_engine, parse_username_and_rolename |
283 | +from pgtools.utils import ( |
284 | + DJANGO_VERSION, |
285 | + check_database_engine, |
286 | + parse_username_and_rolename, |
287 | +) |
288 | |
289 | |
290 | class Command(BaseCommand): |
291 | help = 'Delete an existing database user based on given role.' |
292 | - args = 'username [rolename]' |
293 | + |
294 | + def __init__(self, *args, **kwargs): |
295 | + super(Command, self).__init__(*args, **kwargs) |
296 | + if DJANGO_VERSION < (1, 8): |
297 | + self.args = "username [rolename]" |
298 | + |
299 | + def add_arguments(self, parser): |
300 | + # positional arguments |
301 | + parser.add_argument( |
302 | + 'username', metavar='username', nargs=1, |
303 | + help='Username for the new user.') |
304 | + parser.add_argument( |
305 | + 'rolename', metavar='rolename', nargs='?', |
306 | + help='Role to use for creating the new user.') |
307 | |
308 | @graceful_db_errors |
309 | def handle(self, *args, **options): |
310 | check_database_engine() |
311 | - username, rolename = parse_username_and_rolename(args) |
312 | + username, rolename = parse_username_and_rolename(*args, **options) |
313 | |
314 | role = DatabaseRole(rolename) |
315 | user = DatabaseUser(username, create=False) |
316 | |
317 | === modified file 'pgtools/management/commands/grantuser.py' |
318 | --- pgtools/management/commands/grantuser.py 2013-02-21 21:16:14 +0000 |
319 | +++ pgtools/management/commands/grantuser.py 2017-01-17 15:08:22 +0000 |
320 | @@ -2,12 +2,16 @@ |
321 | # Copyright 2011 Canonical Ltd. This software is licensed under the |
322 | # GNU Affero General Public License version 3 (see the file LICENSE). |
323 | |
324 | -from django.core.management.base import BaseCommand,CommandError |
325 | +from django.core.management.base import BaseCommand |
326 | from django.db import connection |
327 | -from django.db.models.loading import get_models |
328 | |
329 | from pgtools.decorators import graceful_db_errors |
330 | -from pgtools.utils import check_database_engine |
331 | +from pgtools.utils import ( |
332 | + DJANGO_VERSION, |
333 | + check_database_engine, |
334 | + get_models, |
335 | + parse_username, |
336 | +) |
337 | |
338 | |
339 | GRANT_SQL = "GRANT ALL ON %s TO %s;" |
340 | @@ -16,20 +20,23 @@ |
341 | |
342 | class Command(BaseCommand): |
343 | help = 'Grant access to user to all models.' |
344 | - args = 'username' |
345 | + |
346 | + def __init__(self, *args, **kwargs): |
347 | + super(Command, self).__init__(*args, **kwargs) |
348 | + if DJANGO_VERSION < (1, 8): |
349 | + self.args = "username" |
350 | + |
351 | + def add_arguments(self, parser): |
352 | + # positional arguments |
353 | + parser.add_argument( |
354 | + 'username', metavar='username', nargs=1, |
355 | + help='Username for the new user.') |
356 | |
357 | @graceful_db_errors |
358 | def handle(self, *args, **options): |
359 | check_database_engine() |
360 | |
361 | - arg_count = len(args) |
362 | - if arg_count > 1: |
363 | - raise CommandError('Too many arguments provided.') |
364 | - elif arg_count < 1: |
365 | - raise CommandError( |
366 | - 'Please provide a username.') |
367 | - |
368 | - self.username = args[0] |
369 | + self.username = parse_username(*args, **options) |
370 | self.cursor = connection.cursor() |
371 | |
372 | for model in get_models(): |
373 | |
374 | === modified file 'pgtools/management/commands/listusers.py' |
375 | --- pgtools/management/commands/listusers.py 2011-05-30 14:30:10 +0000 |
376 | +++ pgtools/management/commands/listusers.py 2017-01-17 15:08:22 +0000 |
377 | @@ -2,26 +2,35 @@ |
378 | # Copyright 2011 Canonical Ltd. This software is licensed under the |
379 | # GNU Affero General Public License version 3 (see the file LICENSE). |
380 | |
381 | -from django.core.management.base import BaseCommand, CommandError |
382 | +from django.core.management.base import BaseCommand |
383 | |
384 | from pgtools.dbrole import DatabaseRole |
385 | from pgtools.decorators import graceful_db_errors |
386 | -from pgtools.utils import check_database_engine, get_rolename_from_settings |
387 | +from pgtools.utils import ( |
388 | + DJANGO_VERSION, |
389 | + check_database_engine, |
390 | + parse_rolename, |
391 | +) |
392 | |
393 | |
394 | class Command(BaseCommand): |
395 | help = 'List database users with access to given role.' |
396 | - args = 'rolename' |
397 | + |
398 | + def __init__(self, *args, **kwargs): |
399 | + super(Command, self).__init__(*args, **kwargs) |
400 | + if DJANGO_VERSION < (1, 8): |
401 | + self.args = "[rolename]" |
402 | + |
403 | + def add_arguments(self, parser): |
404 | + # positional arguments |
405 | + parser.add_argument( |
406 | + 'rolename', metavar='rolename', nargs='?', |
407 | + help='Role to use for creating the new user.') |
408 | |
409 | @graceful_db_errors |
410 | def handle(self, *args, **options): |
411 | check_database_engine() |
412 | - if len(args) < 1: |
413 | - rolename = get_rolename_from_settings() |
414 | - elif len(args) == 1: |
415 | - rolename = args[0] |
416 | - else: |
417 | - raise CommandError('Too many arguments.') |
418 | + rolename = parse_rolename(*args, **options) |
419 | |
420 | role = DatabaseRole(rolename) |
421 | roles = role.get_users() |
422 | |
423 | === modified file 'pgtools/tests.py' |
424 | --- pgtools/tests.py 2013-07-11 20:09:14 +0000 |
425 | +++ pgtools/tests.py 2017-01-17 15:08:22 +0000 |
426 | @@ -7,30 +7,32 @@ |
427 | import sys |
428 | |
429 | from cStringIO import StringIO |
430 | -from contextlib import contextmanager |
431 | - |
432 | -from psycopg2 import ProgrammingError |
433 | - |
434 | -from mock import Mock, patch |
435 | - |
436 | -import django |
437 | +from unittest import skipIf |
438 | + |
439 | from django.conf import settings |
440 | from django.core.management import call_command |
441 | from django.core.management.base import CommandError |
442 | from django.db import connection, models |
443 | -from django.db.backends import postgresql_psycopg2 |
444 | -from django.db.models.loading import get_models |
445 | +from django.db.backends.postgresql_psycopg2.base import ( |
446 | + DatabaseWrapper as PGDatabaseWrapper, |
447 | +) |
448 | from django.db.utils import load_backend |
449 | -from django.test import TestCase |
450 | - |
451 | +from django.test import TransactionTestCase |
452 | +from django.test.utils import override_settings |
453 | +from mock import Mock, patch |
454 | +from psycopg2 import ProgrammingError |
455 | |
456 | from pgtools.dbrole import DatabaseRole |
457 | from pgtools.dbuser import DatabaseUser |
458 | from pgtools.decorators import graceful_db_errors |
459 | from pgtools.management.commands import grantuser |
460 | from pgtools.utils import ( |
461 | - check_database_engine, get_rolename_from_settings, |
462 | - parse_username_and_rolename |
463 | + DJANGO_VERSION, |
464 | + check_database_engine, |
465 | + get_rolename_from_settings, |
466 | + parse_rolename, |
467 | + parse_username, |
468 | + parse_username_and_rolename, |
469 | ) |
470 | |
471 | EMPTY_ROLE_NAME = 'test_empty_role' |
472 | @@ -39,35 +41,7 @@ |
473 | TEST_USER_NAME2 = 'test_user2' |
474 | |
475 | |
476 | -# Original snippet from http://djangosnippets.org/snippets/2156/ |
477 | -class SettingDoesNotExist: |
478 | - pass |
479 | - |
480 | - |
481 | -def switch_settings(**kwargs): |
482 | - """Helper method that updates settings and returns old settings.""" |
483 | - old_settings = {} |
484 | - for key, new_value in kwargs.items(): |
485 | - old_value = getattr(settings, key, SettingDoesNotExist) |
486 | - old_settings[key] = old_value |
487 | - |
488 | - if new_value is SettingDoesNotExist: |
489 | - delattr(settings, key) |
490 | - else: |
491 | - setattr(settings, key, new_value) |
492 | - |
493 | - return old_settings |
494 | - |
495 | - |
496 | -@contextmanager |
497 | -def patch_settings(**kwargs): |
498 | - old_settings = switch_settings(**kwargs) |
499 | - yield |
500 | - switch_settings(**old_settings) |
501 | -# end snippet |
502 | - |
503 | - |
504 | -class CommandTestCase(TestCase): |
505 | +class CommandBaseTestCase(TransactionTestCase): |
506 | |
507 | def setUp(self): |
508 | test_settings = { |
509 | @@ -80,7 +54,9 @@ |
510 | 'USER': 'postgres', |
511 | 'PASSWORD': '', |
512 | }) |
513 | - self.old_settings = switch_settings(**test_settings) |
514 | + overrides = self.settings(**test_settings) |
515 | + overrides.enable() |
516 | + self.addCleanup(overrides.disable) |
517 | |
518 | self._refresh_connection() |
519 | self._capture_stdout_stderr() |
520 | @@ -141,7 +117,6 @@ |
521 | def tearDown(self): |
522 | self._delete_roles_and_users() |
523 | self._release_stdout_stderr() |
524 | - switch_settings(**self.old_settings) |
525 | |
526 | def _delete_roles_and_users(self): |
527 | # clean up everything |
528 | @@ -170,7 +145,7 @@ |
529 | sys.stderr = sys.__stderr__ |
530 | |
531 | |
532 | -class CommandTest(CommandTestCase): |
533 | +class CommandTestCase(CommandBaseTestCase): |
534 | |
535 | def test_command_createuser_no_args(self): |
536 | # No argument |
537 | @@ -307,19 +282,17 @@ |
538 | except (CommandError, SystemExit): |
539 | pass |
540 | # No argument, but existing role predefined |
541 | - old_PG_BASE_ROLE = getattr(settings, 'PG_BASE_ROLE', None) |
542 | - settings.PG_BASE_ROLE = MASTER_ROLE_NAME |
543 | - call_command('listusers') |
544 | + with self.settings(PG_BASE_ROLE=MASTER_ROLE_NAME): |
545 | + call_command('listusers') |
546 | # No argument, non-existing role predefined |
547 | - settings.PG_BASE_ROLE = 'somefunkynonexistingrole' |
548 | - try: |
549 | - call_command('listusers') |
550 | - self.fail( |
551 | - 'Calling listusers without arguments and non-existing role ' |
552 | - 'predefined should fail.') |
553 | - except (CommandError, SystemExit): |
554 | - pass |
555 | - settings.PG_BASE_ROLE = old_PG_BASE_ROLE |
556 | + with self.settings(PG_BASE_ROLE='somefunkynonexistingrole'): |
557 | + try: |
558 | + call_command('listusers') |
559 | + self.fail( |
560 | + 'Calling listusers without arguments and non-existing role ' |
561 | + 'predefined should fail.') |
562 | + except (CommandError, SystemExit): |
563 | + pass |
564 | # Too many arguments |
565 | try: |
566 | call_command('listusers', 'foo', 'bar') |
567 | @@ -447,9 +420,8 @@ |
568 | "User cannot be deleted: %s" % TEST_USER_NAME1) |
569 | |
570 | def get_backends(self): |
571 | - django_version = django.VERSION[:2] |
572 | backends = ['django.db.backends.postgresql_psycopg2'] |
573 | - if django_version < (1,4): |
574 | + if DJANGO_VERSION < (1,4): |
575 | backends.insert(0, 'postgresql_psycopg2') |
576 | return backends |
577 | |
578 | @@ -484,7 +456,7 @@ |
579 | @patch('pgtools.utils.settings') |
580 | def test_utils_check_database_engine_subclass_13_style(self, |
581 | mock_settings): |
582 | - class CustomDatabaseWrapper(postgresql_psycopg2.base.DatabaseWrapper): |
583 | + class CustomDatabaseWrapper(PGDatabaseWrapper): |
584 | pass |
585 | |
586 | dbconfig = {'default': {'NAME': 'mydatabase', |
587 | @@ -505,11 +477,13 @@ |
588 | # Test with default (nothing set) |
589 | self.assertRaises(CommandError, get_rolename_from_settings) |
590 | # Test with the master role |
591 | - old_PG_BASE_ROLE = getattr(settings, 'PG_BASE_ROLE', None) |
592 | - settings.PG_BASE_ROLE = MASTER_ROLE_NAME |
593 | - master_role_name = get_rolename_from_settings() |
594 | - self.assertEqual(master_role_name, MASTER_ROLE_NAME) |
595 | - settings.PG_BASE_ROLE = old_PG_BASE_ROLE |
596 | + with self.settings(PG_BASE_ROLE=MASTER_ROLE_NAME): |
597 | + master_role_name = get_rolename_from_settings() |
598 | + self.assertEqual(master_role_name, MASTER_ROLE_NAME) |
599 | + |
600 | + |
601 | +@skipIf(DJANGO_VERSION >= (1, 8), 'Django 1.8 and later uses argparse') |
602 | +class Django17ParserTestCase(CommandBaseTestCase): |
603 | |
604 | def test_parse_username_and_rolename_no_args(self): |
605 | # Test with no args |
606 | @@ -519,31 +493,93 @@ |
607 | def test_parse_username_and_rolename_two_args(self): |
608 | # Test with two arguments |
609 | args = ['username', 'rolename'] |
610 | - username, rolename = parse_username_and_rolename(args) |
611 | + username, rolename = parse_username_and_rolename(*args) |
612 | self.assertEqual(username, 'username') |
613 | self.assertEqual(rolename, 'rolename') |
614 | |
615 | def test_parse_username_and_rolename_username_no_role(self): |
616 | # One argument, no predefined base role |
617 | args = ['username'] |
618 | - self.assertRaises(CommandError, parse_username_and_rolename, args) |
619 | + self.assertRaises(CommandError, parse_username_and_rolename, *args) |
620 | |
621 | + @override_settings(PG_BASE_ROLE=MASTER_ROLE_NAME) |
622 | def test_parse_username_and_rolename_username_with_role(self): |
623 | # One argument, predefined base role |
624 | - old_PG_BASE_ROLE = getattr(settings, 'PG_BASE_ROLE', None) |
625 | - settings.PG_BASE_ROLE = MASTER_ROLE_NAME |
626 | args = ['username', 'rolename'] |
627 | - username, rolename = parse_username_and_rolename(args) |
628 | + username, rolename = parse_username_and_rolename(*args) |
629 | self.assertEqual(username, 'username') |
630 | self.assertEqual(rolename, 'rolename') |
631 | - settings.PG_BASE_ROLE = old_PG_BASE_ROLE |
632 | |
633 | def test_parse_username_and_rolename_too_many_args(self): |
634 | args = ['username', 'rolename', 'foo'] |
635 | - self.assertRaises(CommandError, parse_username_and_rolename, args) |
636 | - |
637 | - |
638 | -class GrantUserCommandTestCase(CommandTestCase): |
639 | + self.assertRaises(CommandError, parse_username_and_rolename, *args) |
640 | + |
641 | + def test_parse_username_no_args(self): |
642 | + self.assertRaises(CommandError, parse_username) |
643 | + |
644 | + def test_parse_username_too_many_args(self): |
645 | + args = ['username', 'rolename'] |
646 | + self.assertRaises(CommandError, parse_username, *args) |
647 | + |
648 | + def test_parse_username_from_args(self): |
649 | + args = ['username'] |
650 | + username = parse_username(*args) |
651 | + self.assertEqual(username, 'username') |
652 | + |
653 | + @override_settings(PG_BASE_ROLE=MASTER_ROLE_NAME) |
654 | + def test_parse_rolename_no_args(self): |
655 | + rolename = parse_rolename() |
656 | + self.assertEqual(rolename, MASTER_ROLE_NAME) |
657 | + |
658 | + def test_parse_rolename_too_many_args(self): |
659 | + args = ['username', 'rolename'] |
660 | + self.assertRaises(CommandError, parse_rolename, *args) |
661 | + |
662 | + def test_parse_rolename_from_args(self): |
663 | + args = ['rolename'] |
664 | + rolename = parse_rolename(*args) |
665 | + self.assertEqual(rolename, 'rolename') |
666 | + |
667 | + |
668 | + |
669 | +@skipIf(DJANGO_VERSION < (1, 8), 'Django 1.7 and earlier uses optparse') |
670 | +class Django18ParserTestCase(CommandBaseTestCase): |
671 | + |
672 | + def test_parse_username_and_rolename_from_options(self): |
673 | + args = ['foo', 'bar'] |
674 | + options = {'username': 'username', 'rolename': 'rolename'} |
675 | + username, rolename = parse_username_and_rolename(*args, **options) |
676 | + self.assertEqual(username, 'username') |
677 | + self.assertEqual(rolename, 'rolename') |
678 | + |
679 | + @override_settings(PG_BASE_ROLE=MASTER_ROLE_NAME) |
680 | + def test_parse_username_and_rolename_from_options_with_default_role(self): |
681 | + args = ['foo', 'bar'] |
682 | + options = {'username': 'username'} |
683 | + username, rolename = parse_username_and_rolename(*args, **options) |
684 | + self.assertEqual(username, 'username') |
685 | + self.assertEqual(rolename, MASTER_ROLE_NAME) |
686 | + |
687 | + def test_parse_username_from_options(self): |
688 | + args = ['foo'] |
689 | + options = {'username': 'username'} |
690 | + username = parse_username(*args, **options) |
691 | + self.assertEqual(username, 'username') |
692 | + |
693 | + def test_parse_rolename_from_options(self): |
694 | + args = ['foo'] |
695 | + options = {'rolename': 'rolename'} |
696 | + rolename = parse_rolename(*args, **options) |
697 | + self.assertEqual(rolename, 'rolename') |
698 | + |
699 | + |
700 | +@skipIf(DJANGO_VERSION < (1, 7), "django.apps not available in Django 1.6") |
701 | +class GrantUserCommandTestCase(CommandBaseTestCase): |
702 | + |
703 | + def get_models(self, app_label): |
704 | + from django.apps import apps |
705 | + return apps.get_app_config(app_label).get_models() |
706 | + |
707 | def test_command_grantuser_no_args(self): |
708 | try: |
709 | call_command('grantuser') |
710 | @@ -553,8 +589,7 @@ |
711 | |
712 | @patch('pgtools.management.commands.grantuser.get_models') |
713 | def test_command_grantuser(self, mock_get_models): |
714 | - from django.contrib.admin import models as admin_models |
715 | - mock_get_models.return_value = get_models(admin_models) |
716 | + mock_get_models.return_value = self.get_models('admin') |
717 | command = grantuser.Command |
718 | with patch.object(command, '_grant_one') as mock_grant_one: |
719 | # force the cache to reload the apps |
720 | @@ -600,8 +635,7 @@ |
721 | @patch('pgtools.management.commands.grantuser.connection') |
722 | def test__grant_many_to_many(self, mock_connection, mock_get_models): |
723 | mock_connection.cursor.return_value.fetchone.return_value = ['foo'] |
724 | - from django.contrib.auth import models as auth_models |
725 | - mock_get_models.return_value = get_models(auth_models) |
726 | + mock_get_models.return_value = self.get_models('auth') |
727 | |
728 | call_command('grantuser', 'payments') |
729 | |
730 | @@ -612,3 +646,18 @@ |
731 | 'GRANT ALL ON foo TO payments;' |
732 | ]: |
733 | self.assertTrue(expected in calls) |
734 | + |
735 | + |
736 | +@skipIf(DJANGO_VERSION > (1, 6), "Django 1.7 and later use django.apps for getting models") |
737 | +class Django16GrantUserCommandTestCase(GrantUserCommandTestCase): |
738 | + |
739 | + def get_models(self, app_label): |
740 | + import django.contrib.admin.models |
741 | + import django.contrib.auth.models |
742 | + from django.db.models.loading import get_models |
743 | + |
744 | + if app_label == 'admin': |
745 | + models = django.contrib.admin.models |
746 | + elif app_label == 'auth': |
747 | + models = django.contrib.auth.models |
748 | + return get_models(models) |
749 | |
750 | === modified file 'pgtools/utils.py' |
751 | --- pgtools/utils.py 2013-07-11 19:14:08 +0000 |
752 | +++ pgtools/utils.py 2017-01-17 15:08:22 +0000 |
753 | @@ -2,12 +2,16 @@ |
754 | # Copyright 2011 Canonical Ltd. This software is licensed under the |
755 | # GNU Affero General Public License version 3 (see the file LICENSE). |
756 | |
757 | +import django |
758 | from django.conf import settings |
759 | from django.core.management.base import CommandError |
760 | from django.db import DEFAULT_DB_ALIAS |
761 | from django.db.utils import load_backend |
762 | |
763 | |
764 | +DJANGO_VERSION = django.VERSION[:2] |
765 | + |
766 | + |
767 | def check_database_engine(alias=None): |
768 | if alias is None: |
769 | alias = DEFAULT_DB_ALIAS |
770 | @@ -29,20 +33,70 @@ |
771 | return rolename |
772 | |
773 | |
774 | -def parse_username_and_rolename(args): |
775 | +def parse_username_and_rolename(*args, **options): |
776 | username = None |
777 | rolename = None |
778 | - arg_count = len(args) |
779 | + if DJANGO_VERSION < (1, 8): |
780 | + arg_count = len(args) |
781 | |
782 | - if arg_count == 2: |
783 | - username, rolename = args |
784 | + if arg_count == 2: |
785 | + username, rolename = args |
786 | + else: |
787 | + if arg_count > 2: |
788 | + raise CommandError('Too many arguments provided.') |
789 | + elif arg_count < 1: |
790 | + raise CommandError( |
791 | + 'Please provide both a username and a role name.') |
792 | + username = args[0] |
793 | + rolename = get_rolename_from_settings() |
794 | else: |
795 | - if arg_count > 2: |
796 | + username = options.get('username', None) |
797 | + rolename = options.get('rolename', None) |
798 | + |
799 | + if isinstance(username, list) and len(username) > 0: |
800 | + username = username[0] |
801 | + |
802 | + if rolename is None: |
803 | + rolename = get_rolename_from_settings() |
804 | + |
805 | + return (username, rolename) |
806 | + |
807 | + |
808 | +def parse_username(*args, **options): |
809 | + if DJANGO_VERSION < (1, 8): |
810 | + arg_count = len(args) |
811 | + if arg_count > 1: |
812 | raise CommandError('Too many arguments provided.') |
813 | elif arg_count < 1: |
814 | - raise CommandError( |
815 | - 'Please provide both a username and a role name.') |
816 | + raise CommandError('Please provide a username.') |
817 | username = args[0] |
818 | - rolename = get_rolename_from_settings() |
819 | - |
820 | - return (username, rolename) |
821 | + else: |
822 | + username = options['username'] |
823 | + if isinstance(username, list) and len(username) > 0: |
824 | + username = username[0] |
825 | + return username |
826 | + |
827 | + |
828 | +def parse_rolename(*args, **options): |
829 | + if DJANGO_VERSION < (1, 8): |
830 | + if len(args) < 1: |
831 | + rolename = get_rolename_from_settings() |
832 | + elif len(args) == 1: |
833 | + rolename = args[0] |
834 | + else: |
835 | + raise CommandError('Too many arguments.') |
836 | + else: |
837 | + rolename = options.get('rolename', None) |
838 | + if rolename is None: |
839 | + rolename = get_rolename_from_settings() |
840 | + return rolename |
841 | + |
842 | + |
843 | +def get_models(): |
844 | + try: |
845 | + from django.apps import apps |
846 | + models = apps.get_models() |
847 | + except ImportError: |
848 | + from django.db.models import loading |
849 | + models = loading.get_models() |
850 | + return models |
851 | |
852 | === modified file 'requirements.txt' |
853 | --- requirements.txt 2011-05-30 14:30:10 +0000 |
854 | +++ requirements.txt 2017-01-17 15:08:22 +0000 |
855 | @@ -1,3 +1,3 @@ |
856 | mock |
857 | -psycopg2 |
858 | +psycopg2==2.4.5 |
859 | django |
860 | |
861 | === added file 'setup.cfg' |
862 | --- setup.cfg 1970-01-01 00:00:00 +0000 |
863 | +++ setup.cfg 2017-01-17 15:08:22 +0000 |
864 | @@ -0,0 +1,2 @@ |
865 | +[wheel] |
866 | +universal = 1 |
867 | |
868 | === modified file 'setup.py' |
869 | --- setup.py 2013-07-11 19:14:08 +0000 |
870 | +++ setup.py 2017-01-17 15:08:22 +0000 |
871 | @@ -1,68 +1,19 @@ |
872 | #!/usr/bin/env python |
873 | # -*- encoding: utf-8 -*- |
874 | -# Copyright 2011 Canonical Ltd. This software is licensed under the |
875 | +# Copyright 2011-2017 Canonical Ltd. This software is licensed under the |
876 | # GNU Affero General Public License version 3 (see the file LICENSE). |
877 | |
878 | -from distutils.core import setup, Command |
879 | - |
880 | - |
881 | -class TestCommand(Command): |
882 | - user_options = [] |
883 | - |
884 | - def initialize_options(self): |
885 | - pass |
886 | - |
887 | - def finalize_options(self): |
888 | - pass |
889 | - |
890 | - def run(self): |
891 | - """Run the project tests.""" |
892 | - |
893 | - import django |
894 | - from django.conf import settings |
895 | - from django.core.management import call_command |
896 | - |
897 | - config = { |
898 | - 'SITE_ID': 1, |
899 | - 'ROOT_URLCONF': '', |
900 | - 'INSTALLED_APPS': [ |
901 | - 'django.contrib.admin', |
902 | - 'django.contrib.auth', |
903 | - 'django.contrib.contenttypes', |
904 | - 'django.contrib.sessions', |
905 | - 'pgtools', |
906 | - ], |
907 | - 'DATABASES': { |
908 | - 'default': { |
909 | - 'NAME': 'pgtools', |
910 | - 'ENGINE': 'django.db.backends.postgresql_psycopg2', |
911 | - 'USER': 'postgres', |
912 | - } |
913 | - } |
914 | - } |
915 | - |
916 | - django_version = django.VERSION[:2] |
917 | - if django_version < (1,4): |
918 | - config.update({ |
919 | - 'DATABASE_ENGINE': 'django.db.backends.postgresql_psycopg2', |
920 | - 'DATABASE_NAME': 'pgtools', |
921 | - 'DATABASE_USER': 'postgres', |
922 | - }) |
923 | - |
924 | - settings.configure(**config) |
925 | - |
926 | - call_command('test', interactive=False) |
927 | +from setuptools import setup |
928 | |
929 | |
930 | setup( |
931 | name='django-pgtools', |
932 | - version='0.2', |
933 | + version='0.3', |
934 | description="Set of Django management commands for handling various aspects of managing PostgreSQL database.", |
935 | url='https://launchpad.net/django-pgtools', |
936 | author='Canonical ISD', |
937 | author_email='canonical-isd@lists.launchpad.net', |
938 | packages=['pgtools', 'pgtools/management', 'pgtools/management/commands'], |
939 | license='AGPLv3', |
940 | - cmdclass = {'test': TestCommand}, |
941 | - requires=['django (>=1.2)'] |
942 | + requires=['django (>=1.6)'] |
943 | ) |
944 | |
945 | === added directory 'test_project' |
946 | === added file 'test_project/__init__.py' |
947 | === added file 'test_project/settings.py' |
948 | --- test_project/settings.py 1970-01-01 00:00:00 +0000 |
949 | +++ test_project/settings.py 2017-01-17 15:08:22 +0000 |
950 | @@ -0,0 +1,29 @@ |
951 | +DATABASES = { |
952 | + 'default': { |
953 | + 'ENGINE': 'django.db.backends.postgresql_psycopg2', |
954 | + 'NAME': 'pgtools', |
955 | + 'USER': 'postgres', |
956 | + 'HOST': '/dev/shm/pg_pgtools', |
957 | + } |
958 | +} |
959 | +INSTALLED_APPS = ( |
960 | + 'django.contrib.admin', |
961 | + 'django.contrib.auth', |
962 | + 'django.contrib.contenttypes', |
963 | + 'django.contrib.sessions', |
964 | + 'pgtools', |
965 | +) |
966 | +MIDDLEWARE_CLASSES = ( |
967 | + 'django.contrib.sessions.middleware.SessionMiddleware', |
968 | + 'django.middleware.common.CommonMiddleware', |
969 | + 'django.middleware.csrf.CsrfViewMiddleware', |
970 | + 'django.contrib.auth.middleware.AuthenticationMiddleware', |
971 | + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', |
972 | + 'django.middleware.locale.LocaleMiddleware', |
973 | + 'django.contrib.messages.middleware.MessageMiddleware', |
974 | + 'django.middleware.clickjacking.XFrameOptionsMiddleware', |
975 | + 'django.middleware.security.SecurityMiddleware', |
976 | +) |
977 | +ROOT_URLCONF = '' |
978 | +SECRET_KEY = 'secret' |
979 | +SITE_ID = 1 |
980 | |
981 | === modified file 'tox.ini' |
982 | --- tox.ini 2013-07-11 19:14:08 +0000 |
983 | +++ tox.ini 2017-01-17 15:08:22 +0000 |
984 | @@ -1,84 +1,36 @@ |
985 | [tox] |
986 | -envlist = |
987 | - py2.5-django1.2, py2.5-django1.3, py2.5-django1.4, py2.5-django1.5, |
988 | - py2.6-django1.2, py2.6-django1.3, py2.6-django1.4, py2.6-django1.5, |
989 | - py2.7-django1.2, py2.7-django1.3, py2.7-django1.4, py2.7-django1.5, |
990 | - |
991 | +envlist = py27-django1.6,py27-django1.7,py27-django1.8,py27-django1.9,py27-django1.10 |
992 | |
993 | [testenv] |
994 | -commands = python setup.py test |
995 | - |
996 | -# Python 2.5 |
997 | -[testenv:py2.5-django1.2] |
998 | -basepython = python2.5 |
999 | -deps = django >= 1.2, < 1.3 |
1000 | - psycopg2==2.4.1 |
1001 | - mock |
1002 | - |
1003 | -[testenv:py2.5-django1.3] |
1004 | -basepython = python2.5 |
1005 | -deps = django >= 1.3, < 1.4 |
1006 | - psycopg2==2.4.1 |
1007 | - mock |
1008 | - |
1009 | -[testenv:py2.5-django1.4] |
1010 | -basepython = python2.5 |
1011 | -deps = django >= 1.4, < 1.5 |
1012 | - psycopg2==2.4.1 |
1013 | - mock |
1014 | - |
1015 | -[testenv:py2.5-django1.5] |
1016 | -basepython = python2.5 |
1017 | -deps = django >= 1.5, < 1.6 |
1018 | - psycopg2==2.4.1 |
1019 | - mock |
1020 | - |
1021 | -# Python 2.6 |
1022 | -[testenv:py2.6-django1.2] |
1023 | -basepython = python2.6 |
1024 | -deps = django >= 1.2, < 1.3 |
1025 | - psycopg2==2.4.1 |
1026 | - mock |
1027 | - |
1028 | -[testenv:py2.6-django1.3] |
1029 | -basepython = python2.6 |
1030 | -deps = django >= 1.3, < 1.4 |
1031 | - psycopg2==2.4.1 |
1032 | - mock |
1033 | - |
1034 | -[testenv:py2.6-django1.4] |
1035 | -basepython = python2.6 |
1036 | -deps = django >= 1.4, < 1.5 |
1037 | - psycopg2==2.4.1 |
1038 | - mock |
1039 | - |
1040 | -[testenv:py2.6-django1.5] |
1041 | -basepython = python2.6 |
1042 | -deps = django >= 1.5, < 1.6 |
1043 | - psycopg2==2.4.1 |
1044 | - mock |
1045 | +commands = python manage.py test pgtools |
1046 | +basepython = |
1047 | + python2.7 |
1048 | +deps = |
1049 | + psycopg2 == 2.4.5 |
1050 | + mock |
1051 | |
1052 | # Python 2.7 |
1053 | -[testenv:py2.7-django1.2] |
1054 | -basepython = python2.7 |
1055 | -deps = django >= 1.2, < 1.3 |
1056 | - psycopg2==2.4.1 |
1057 | - mock |
1058 | - |
1059 | -[testenv:py2.7-django1.3] |
1060 | -basepython = python2.7 |
1061 | -deps = django >= 1.3, < 1.4 |
1062 | - psycopg2==2.4.1 |
1063 | - mock |
1064 | - |
1065 | -[testenv:py2.7-django1.4] |
1066 | -basepython = python2.7 |
1067 | -deps = django >= 1.4, < 1.5 |
1068 | - psycopg2==2.4.1 |
1069 | - mock |
1070 | - |
1071 | -[testenv:py2.7-django1.5] |
1072 | -basepython = python2.7 |
1073 | -deps = django >= 1.5, < 1.6 |
1074 | - psycopg2==2.4.1 |
1075 | - mock |
1076 | +[testenv:py27-django1.6] |
1077 | +deps = |
1078 | + django == 1.6.11 |
1079 | + {[testenv]deps} |
1080 | + |
1081 | +[testenv:py27-django1.7] |
1082 | +deps = |
1083 | + django == 1.7.11 |
1084 | + {[testenv]deps} |
1085 | + |
1086 | +[testenv:py27-django1.8] |
1087 | +deps = |
1088 | + django == 1.8.17 |
1089 | + {[testenv]deps} |
1090 | + |
1091 | +[testenv:py27-django1.9] |
1092 | +deps = |
1093 | + django == 1.9.12 |
1094 | + {[testenv]deps} |
1095 | + |
1096 | +[testenv:py27-django1.10] |
1097 | +deps = |
1098 | + django == 1.10.5 |
1099 | + {[testenv]deps} |
LGTM