Merge lp:~danilo/maas/django-upgrade-legacy-migrations into lp:~maas-committers/maas/trunk

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 6098
Proposed branch: lp:~danilo/maas/django-upgrade-legacy-migrations
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~danilo/maas/django-upgrade-method-fixes
Diff against target: 373 lines (+95/-97)
8 files modified
src/maasserver/djangosettings/development.py (+10/-1)
src/maasserver/fields.py (+0/-17)
src/maasserver/middleware.py (+54/-12)
src/maasserver/tests/__init__.py (+9/-1)
src/maasserver/tests/test_fields.py (+0/-7)
src/maasserver/tests/test_middleware.py (+6/-6)
src/maastesting/djangotestcase.py (+2/-53)
src/metadataserver/tests/__init__.py (+14/-0)
To merge this branch: bzr merge lp:~danilo/maas/django-upgrade-legacy-migrations
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+325898@code.launchpad.net

Commit message

1. Remove legacy region test setup and instead make it part of the development settings to avoid problems due to differences in import order between Django 1.8 and 1.11.
2. Replace use of build_request_repr for the DebuggingLoggerMiddleware.
3. Stop restricting lookup types for JSONField and XMLField fields because this was of limited use and entirely changed in 1.10.

Description of the change

We also stop restricting lookup types for JSONField and XMLField fields: get_prep_lookup and get_db_prep_lookup methods are gone in 1.10, and lookups are defined in a different way since, and it's not worth it to override all existing built-in lookups for these two fields and catching weird comparisons like >= on JSON fields.

Most of the legacy test setup is now simplified, though there's still cleanup rogue DB connections that fails in 1.8 but now again works in 1.11 that we may want to re-enable.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I'm confused about how appending to INSTALLED_APPS is going to work. I see the following code in the file, when I look at the full diff context:

    # We expect the following settings to be overridden. They are mentioned here
    # to silence lint warnings.
    INSTALLED_APPS = None

    # Extend base settings.
    import_settings(settings)

    prevent_migrations = StringBool().to_python(
    os.environ.get("MAAS_PREVENT_MIGRATIONS", 0))

    if prevent_migrations:
        INSTALLED_APPS += ("maasserver.tests", "metadataserver.tests")

So how is a tuple going to be appended to None?

review: Needs Information
Revision history for this message
Данило Шеган (danilo) wrote :

import_settings(settings) will actually set it. I am not too happy with how the code works and looks, but import_settings() imports the stuff from a passed-in module into the global namespace: that's where we get INSTALLED_APPS from.

FWIW, as the comment indicates, INSTALLED_APPS is set to None just to avoid lint warnings when it's modified or accessed later on in the file.

Also, note that I have run the tests in both 1.8 and 1.11 about a gazillion times in each of my django 1.11 branches, and I never submit them before they are an improvement in 1.11 (as in a reduced count of test failures), and pass completely with 1.8.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Sounds good; thanks for the explanation.

I'd be happier with this branch if you leave a comment around the INSTALLED_APPS foolishness (Django's, not yours). ;-) But I won't block you on it.

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

I was going to complain about Django too, but it's our code that does that in maasserver.djangosettings.import_settings() — it may be idiomatic for Django, but we've partaken in the foolishness :)

Thanks for the re-review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/djangosettings/development.py'
--- src/maasserver/djangosettings/development.py 2017-05-22 13:29:04 +0000
+++ src/maasserver/djangosettings/development.py 2017-06-21 15:20:56 +0000
@@ -14,12 +14,19 @@
14)14)
1515
16# We expect the following settings to be overridden. They are mentioned here16# We expect the following settings to be overridden. They are mentioned here
17# to silence lint warnings.17# to silence lint warnings: import_settings() below will actually re-set it
18# to a tuple as set in settings.INSTALLED_APPS.
18INSTALLED_APPS = None19INSTALLED_APPS = None
1920
20# Extend base settings.21# Extend base settings.
21import_settings(settings)22import_settings(settings)
2223
24prevent_migrations = StringBool().to_python(
25 os.environ.get("MAAS_PREVENT_MIGRATIONS", 0))
26
27if prevent_migrations:
28 INSTALLED_APPS += ("maasserver.tests", "metadataserver.tests")
29
23# Use our custom test runner, which makes sure that a local database30# Use our custom test runner, which makes sure that a local database
24# cluster is running in the branch.31# cluster is running in the branch.
25TEST_RUNNER = 'maastesting.djangoloader.MAASDjangoTestRunner'32TEST_RUNNER = 'maastesting.djangoloader.MAASDjangoTestRunner'
@@ -97,7 +104,9 @@
97 'sites': 'maastesting.empty',104 'sites': 'maastesting.empty',
98 'piston3': 'maastesting.empty',105 'piston3': 'maastesting.empty',
99 'maasserver': 'maastesting.empty',106 'maasserver': 'maastesting.empty',
107 'maasserver.tests': 'maastesting.empty',
100 'metadataserver': 'maastesting.empty',108 'metadataserver': 'maastesting.empty',
109 'metadataserver.tests': 'maastesting.empty',
101 }110 }
102111
103PASSWORD_HASHERS = (112PASSWORD_HASHERS = (
104113
=== modified file 'src/maasserver/fields.py'
--- src/maasserver/fields.py 2017-06-02 11:01:43 +0000
+++ src/maasserver/fields.py 2017-06-21 15:20:56 +0000
@@ -320,12 +320,6 @@
320 def get_internal_type(self):320 def get_internal_type(self):
321 return 'TextField'321 return 'TextField'
322322
323 def get_prep_lookup(self, lookup_type, value):
324 if lookup_type not in ['exact', 'isnull']:
325 raise TypeError("Lookup type %s is not supported." % lookup_type)
326 return super(JSONObjectField, self).get_prep_lookup(
327 lookup_type, value)
328
329 def formfield(self, form_class=None, **kwargs):323 def formfield(self, form_class=None, **kwargs):
330 """Return a plain `forms.Field` here to avoid "helpful" conversions.324 """Return a plain `forms.Field` here to avoid "helpful" conversions.
331325
@@ -355,17 +349,6 @@
355 def db_type(self, connection):349 def db_type(self, connection):
356 return "xml"350 return "xml"
357351
358 def get_db_prep_lookup(self, lookup_type, value, **kwargs):
359 """Limit lookup types to those that work on xml.
360
361 Unlike character fields the xml type is non-comparible, see:
362 <http://www.postgresql.org/docs/devel/static/datatype-xml.html>
363 """
364 if lookup_type != 'isnull':
365 raise TypeError("Lookup type %s is not supported." % lookup_type)
366 return super(XMLField, self).get_db_prep_lookup(
367 lookup_type, value, **kwargs)
368
369352
370class EditableBinaryField(BinaryField):353class EditableBinaryField(BinaryField):
371 """An editable binary field.354 """An editable binary field.
372355
=== modified file 'src/maasserver/middleware.py'
--- src/maasserver/middleware.py 2017-03-14 20:50:31 +0000
+++ src/maasserver/middleware.py 2017-06-21 15:20:56 +0000
@@ -16,6 +16,7 @@
16import http.client16import http.client
17import json17import json
18import logging18import logging
19from pprint import pformat
19import re20import re
20import sys21import sys
21import traceback22import traceback
@@ -34,6 +35,8 @@
34 HttpResponseForbidden,35 HttpResponseForbidden,
35 HttpResponseRedirect,36 HttpResponseRedirect,
36)37)
38from django.utils import six
39from django.utils.encoding import force_str
37from django.utils.http import urlquote_plus40from django.utils.http import urlquote_plus
38from maasserver import logger41from maasserver import logger
39from maasserver.bootresources import SIMPLESTREAMS_URL_REGEXP42from maasserver.bootresources import SIMPLESTREAMS_URL_REGEXP
@@ -55,16 +58,6 @@
55)58)
56from provisioningserver.utils.shell import ExternalProcessError59from provisioningserver.utils.shell import ExternalProcessError
5760
58
59try:
60 from django.http.request import build_request_repr
61except ImportError:
62 # build_request_repr is only used for debugging: use
63 # a degraded version if build_request_repr is not
64 # available (i.e. if Django version < 1.5).
65 build_request_repr = repr
66
67
68# 'Retry-After' header sent for httplib.SERVICE_UNAVAILABLE61# 'Retry-After' header sent for httplib.SERVICE_UNAVAILABLE
69# responses.62# responses.
70RETRY_AFTER_SERVICE_UNAVAILABLE = 1063RETRY_AFTER_SERVICE_UNAVAILABLE = 10
@@ -272,12 +265,61 @@
272265
273 log_level = logging.DEBUG266 log_level = logging.DEBUG
274267
268 # Taken straight out of Django 1.8 django.http.request module to improve
269 # our debug output on requests (dropped in Django 1.9).
270 @classmethod
271 def _build_request_repr(
272 self, request, path_override=None, GET_override=None,
273 POST_override=None, COOKIES_override=None, META_override=None):
274 """
275 Builds and returns the request's representation string. The request's
276 attributes may be overridden by pre-processed values.
277 """
278 # Since this is called as part of error handling, we need to be very
279 # robust against potentially malformed input.
280 try:
281 get = (pformat(GET_override)
282 if GET_override is not None
283 else pformat(request.GET))
284 except Exception:
285 get = '<could not parse>'
286 if request._post_parse_error:
287 post = '<could not parse>'
288 else:
289 try:
290 post = (pformat(POST_override)
291 if POST_override is not None
292 else pformat(request.POST))
293 except Exception:
294 post = '<could not parse>'
295 try:
296 cookies = (pformat(COOKIES_override)
297 if COOKIES_override is not None
298 else pformat(request.COOKIES))
299 except Exception:
300 cookies = '<could not parse>'
301 try:
302 meta = (pformat(META_override)
303 if META_override is not None
304 else pformat(request.META))
305 except Exception:
306 meta = '<could not parse>'
307 path = path_override if path_override is not None else request.path
308 return force_str(
309 '<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' %
310 (request.__class__.__name__,
311 path,
312 six.text_type(get),
313 six.text_type(post),
314 six.text_type(cookies),
315 six.text_type(meta)))
316
275 def process_request(self, request):317 def process_request(self, request):
276 if logger.isEnabledFor(self.log_level):318 if logger.isEnabledFor(self.log_level):
277 header = " Request dump ".center(79, "#")319 header = " Request dump ".center(79, "#")
278 logger.log(320 logger.log(
279 self.log_level,321 self.log_level, "%s\n%s", header,
280 "%s\n%s", header, build_request_repr(request))322 self._build_request_repr(request))
281 return None # Allow request processing to continue unabated.323 return None # Allow request processing to continue unabated.
282324
283 def process_response(self, request, response):325 def process_response(self, request, response):
284326
=== modified file 'src/maasserver/tests/__init__.py'
--- src/maasserver/tests/__init__.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/tests/__init__.py 2017-06-21 15:20:56 +0000
@@ -3,4 +3,12 @@
33
4"""Tests for `maasserver`."""4"""Tests for `maasserver`."""
55
6__all__ = []6from django.apps import AppConfig
7
8
9class MAASServerTestsConfig(AppConfig):
10 name = 'maasserver.tests'
11 label = 'maasserver_tests'
12
13
14default_app_config = 'maasserver.tests.MAASServerTestsConfig'
715
=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py 2017-06-02 14:37:33 +0000
+++ src/maasserver/tests/test_fields.py 2017-06-21 15:20:56 +0000
@@ -306,10 +306,6 @@
306 test_instance = JSONFieldModel.objects.get(value__isnull=True)306 test_instance = JSONFieldModel.objects.get(value__isnull=True)
307 self.assertIsNone(test_instance.value)307 self.assertIsNone(test_instance.value)
308308
309 def test_field_another_lookup_fails(self):
310 # Others lookups are not allowed.
311 self.assertRaises(TypeError, JSONFieldModel.objects.get, value__gte=3)
312
313 def test_form_field_is_a_plain_field(self):309 def test_form_field_is_a_plain_field(self):
314 self.assertThat(310 self.assertThat(
315 JSONObjectField().formfield(),311 JSONObjectField().formfield(),
@@ -350,9 +346,6 @@
350 test_instance = XMLFieldModel.objects.get(value__isnull=True)346 test_instance = XMLFieldModel.objects.get(value__isnull=True)
351 self.assertIsNone(test_instance.value)347 self.assertIsNone(test_instance.value)
352348
353 def test_lookup_exact_unsupported(self):
354 self.assertRaises(TypeError, XMLFieldModel.objects.get, value="")
355
356349
357class TestEditableBinaryField(MAASServerTestCase):350class TestEditableBinaryField(MAASServerTestCase):
358351
359352
=== modified file 'src/maasserver/tests/test_middleware.py'
--- src/maasserver/tests/test_middleware.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/tests/test_middleware.py 2017-06-21 15:20:56 +0000
@@ -20,7 +20,6 @@
20)20)
21from django.core.urlresolvers import reverse21from django.core.urlresolvers import reverse
22from django.http import HttpResponse22from django.http import HttpResponse
23from django.http.request import build_request_repr
24from fixtures import FakeLogger23from fixtures import FakeLogger
25from maasserver import middleware as middleware_module24from maasserver import middleware as middleware_module
26from maasserver.components import (25from maasserver.components import (
@@ -239,9 +238,8 @@
239 logger = self.useFixture(FakeLogger('maasserver', logging.INFO))238 logger = self.useFixture(FakeLogger('maasserver', logging.INFO))
240 request = factory.make_fake_request("/api/2.0/nodes/")239 request = factory.make_fake_request("/api/2.0/nodes/")
241 DebuggingLoggerMiddleware().process_request(request)240 DebuggingLoggerMiddleware().process_request(request)
242 self.assertThat(241 debug_output = DebuggingLoggerMiddleware._build_request_repr(request)
243 logger.output,242 self.assertThat(logger.output, Not(Contains(debug_output)))
244 Not(Contains(build_request_repr(request))))
245243
246 def test_debugging_logger_does_not_log_response_if_info_level(self):244 def test_debugging_logger_does_not_log_response_if_info_level(self):
247 logger = self.useFixture(FakeLogger('maasserver', logging.INFO))245 logger = self.useFixture(FakeLogger('maasserver', logging.INFO))
@@ -250,15 +248,17 @@
250 content="test content",248 content="test content",
251 content_type=b"text/plain; charset=utf-8")249 content_type=b"text/plain; charset=utf-8")
252 DebuggingLoggerMiddleware().process_response(request, response)250 DebuggingLoggerMiddleware().process_response(request, response)
251 debug_output = DebuggingLoggerMiddleware._build_request_repr(request)
253 self.assertThat(252 self.assertThat(
254 logger.output, Not(Contains(build_request_repr(request))))253 logger.output, Not(Contains(debug_output)))
255254
256 def test_debugging_logger_logs_request(self):255 def test_debugging_logger_logs_request(self):
257 logger = self.useFixture(FakeLogger('maasserver', logging.DEBUG))256 logger = self.useFixture(FakeLogger('maasserver', logging.DEBUG))
258 request = factory.make_fake_request("/api/2.0/nodes/")257 request = factory.make_fake_request("/api/2.0/nodes/")
259 request.content = "test content"258 request.content = "test content"
260 DebuggingLoggerMiddleware().process_request(request)259 DebuggingLoggerMiddleware().process_request(request)
261 self.assertThat(logger.output, Contains(build_request_repr(request)))260 debug_output = DebuggingLoggerMiddleware._build_request_repr(request)
261 self.assertThat(logger.output, Contains(debug_output))
262262
263 def test_debugging_logger_logs_response(self):263 def test_debugging_logger_logs_response(self):
264 logger = self.useFixture(FakeLogger('maasserver', logging.DEBUG))264 logger = self.useFixture(FakeLogger('maasserver', logging.DEBUG))
265265
=== modified file 'src/maastesting/djangotestcase.py'
--- src/maastesting/djangotestcase.py 2017-01-28 00:51:47 +0000
+++ src/maastesting/djangotestcase.py 2017-06-21 15:20:56 +0000
@@ -14,8 +14,6 @@
14 time,14 time,
15)15)
1616
17from django.apps import apps as django_apps
18from django.core.management import call_command
19from django.core.signals import request_started17from django.core.signals import request_started
20from django.db import (18from django.db import (
21 connection,19 connection,
@@ -170,45 +168,7 @@
170 )168 )
171169
172170
173class InstallDjangoAppsMixin:171class DjangoTestCase(django.test.TestCase, MAASTestCase):
174 """Install extra applications into the Django ``INSTALLED_APPS`` setting.
175
176 :deprecated: Do NOT use in new tests.
177 """
178
179 def _setup_apps(self, apps):
180 self._did_set_apps = False
181 if len(apps) > 0:
182 # Inject the apps into django now that the fixture is setup.
183 self._did_set_apps = True
184 current_apps = [
185 current_app.name
186 for current_app in django_apps.get_app_configs()
187 ]
188 need_to_add = [
189 new_app
190 for new_app in apps
191 if new_app not in current_apps
192 ]
193 for app in need_to_add:
194 current_apps.append(app)
195 # Set the installed applications in django. This requires another
196 # call to unset_installed_apps to reset to the previous value.
197 django_apps.set_installed_apps(current_apps)
198 # Call migrate that will actual perform the migrations for this
199 # applications and if no migrations exists then it will fallback
200 # to performing 'syncdb'.
201 call_command("migrate")
202
203 def _teardown_apps(self):
204 # Check that the internal __set_apps is set so that the required
205 # unset_installed_apps can be called.
206 if self._did_set_apps:
207 django_apps.unset_installed_apps()
208
209
210class DjangoTestCase(
211 django.test.TestCase, MAASTestCase, InstallDjangoAppsMixin):
212 """A Django `TestCase` for MAAS.172 """A Django `TestCase` for MAAS.
213173
214 Generally you should NOT directly subclass this for tests; use an174 Generally you should NOT directly subclass this for tests; use an
@@ -232,12 +192,7 @@
232 # This attribute is used as a tag with Nose.192 # This attribute is used as a tag with Nose.
233 legacy = True193 legacy = True
234194
235 def _fixture_setup(self):
236 super(DjangoTestCase, self)._fixture_setup()
237 self._setup_apps(self.apps)
238
239 def _fixture_teardown(self):195 def _fixture_teardown(self):
240 self._teardown_apps()
241 super(DjangoTestCase, self)._fixture_teardown()196 super(DjangoTestCase, self)._fixture_teardown()
242 # TODO blake_r: Fix so this is not disabled. Currently not197 # TODO blake_r: Fix so this is not disabled. Currently not
243 # working with Django 1.8.198 # working with Django 1.8.
@@ -245,8 +200,7 @@
245 # check_for_rogue_database_activity(self)200 # check_for_rogue_database_activity(self)
246201
247202
248class DjangoTransactionTestCase(203class DjangoTransactionTestCase(django.test.TransactionTestCase, MAASTestCase):
249 django.test.TransactionTestCase, MAASTestCase, InstallDjangoAppsMixin):
250 """A Django `TransactionTestCase` for MAAS.204 """A Django `TransactionTestCase` for MAAS.
251205
252 A version of `MAASTestCase` that supports transactions.206 A version of `MAASTestCase` that supports transactions.
@@ -273,12 +227,7 @@
273 # This attribute is used as a tag with Nose.227 # This attribute is used as a tag with Nose.
274 legacy = True228 legacy = True
275229
276 def _fixture_setup(self):
277 super(DjangoTransactionTestCase, self)._fixture_setup()
278 self._setup_apps(self.apps)
279
280 def _fixture_teardown(self):230 def _fixture_teardown(self):
281 self._teardown_apps()
282 super(DjangoTransactionTestCase, self)._fixture_teardown()231 super(DjangoTransactionTestCase, self)._fixture_teardown()
283 # TODO blake_r: Fix so this is not disabled. Currently not232 # TODO blake_r: Fix so this is not disabled. Currently not
284 # working with Django 1.8.233 # working with Django 1.8.
285234
=== modified file 'src/metadataserver/tests/__init__.py'
--- src/metadataserver/tests/__init__.py 2013-08-23 12:54:28 +0000
+++ src/metadataserver/tests/__init__.py 2017-06-21 15:20:56 +0000
@@ -0,0 +1,14 @@
1# Copyright 2012-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `metadataserver`."""
5
6from django.apps import AppConfig
7
8
9class MetadataServerTestsConfig(AppConfig):
10 name = 'metadataserver.tests'
11 label = 'metadataserver_tests'
12
13
14default_app_config = 'metadataserver.tests.MetadataServerTestsConfig'