Code

Ticket #14861: 14861-4.diff

File 14861-4.diff, 5.5 KB (added by claudep, 22 months ago)
Line 
1diff --git a/django/conf/__init__.py b/django/conf/__init__.py
2index 6272f4e..d636ff0 100644
3--- a/django/conf/__init__.py
4+++ b/django/conf/__init__.py
5@@ -43,13 +43,28 @@ class LazySettings(LazyObject):
6                 % (name, ENVIRONMENT_VARIABLE))
7 
8         self._wrapped = Settings(settings_module)
9-
10+        self._configure_logging()
11 
12     def __getattr__(self, name):
13         if self._wrapped is empty:
14             self._setup(name)
15         return getattr(self._wrapped, name)
16 
17+    def _configure_logging(self):
18+        """
19+        Setup logging from LOGGING_CONFIG and LOGGING settings.
20+        """
21+        if self.LOGGING_CONFIG:
22+            # First find the logging configuration function ...
23+            logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1)
24+            logging_config_module = importlib.import_module(logging_config_path)
25+            logging_config_func = getattr(logging_config_module, logging_config_func_name)
26+
27+            # Backwards-compatibility shim for #16288 fix
28+            compat_patch_logging_config(self.LOGGING)
29+
30+            # ... then invoke it with the logging settings
31+            logging_config_func(self.LOGGING)
32 
33     def configure(self, default_settings=global_settings, **options):
34         """
35@@ -133,19 +148,6 @@ class Settings(BaseSettings):
36             os.environ['TZ'] = self.TIME_ZONE
37             time.tzset()
38 
39-        # Settings are configured, so we can set up the logger if required
40-        if self.LOGGING_CONFIG:
41-            # First find the logging configuration function ...
42-            logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1)
43-            logging_config_module = importlib.import_module(logging_config_path)
44-            logging_config_func = getattr(logging_config_module, logging_config_func_name)
45-
46-            # Backwards-compatibility shim for #16288 fix
47-            compat_patch_logging_config(self.LOGGING)
48-
49-            # ... then invoke it with the logging settings
50-            logging_config_func(self.LOGGING)
51-
52 
53 class UserSettingsHolder(BaseSettings):
54     """
55diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
56index 94236ba..5fc229e 100644
57--- a/docs/topics/logging.txt
58+++ b/docs/topics/logging.txt
59@@ -347,33 +347,10 @@ This logging configuration does the following things:
60 
61 .. admonition:: Custom handlers and circular imports
62 
63-    If your ``settings.py`` specifies a custom handler class and the file
64-    defining that class also imports ``settings.py`` a circular import will
65-    occur.
66-
67-    For example, if ``settings.py`` contains the following config for
68-    :setting:`LOGGING`::
69-
70-        LOGGING = {
71-          'version': 1,
72-          'handlers': {
73-            'custom_handler': {
74-              'level': 'INFO',
75-              'class': 'myproject.logconfig.MyHandler',
76-            }
77-          }
78-        }
79-
80-    and ``myproject/logconfig.py`` has the following line before the
81-    ``MyHandler`` definition::
82-
83-        from django.conf import settings
84-
85-    then the ``dictconfig`` module will raise an exception like the following::
86-
87-        ValueError: Unable to configure handler 'custom_handler':
88-        Unable to configure handler 'custom_handler':
89-        'module' object has no attribute 'logconfig'
90+    Logging configuration happens soon in the Django setup machinery. If your
91+    ``settings.py`` specifies a custom handler class, you should be careful
92+    about the modules you import in the file defining this class, unless a
93+    circular import may occur.
94 
95 .. _formatter documentation: http://docs.python.org/library/logging.html#formatter-objects
96 
97diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py
98new file mode 100644
99index 0000000..8524aa2
100--- /dev/null
101+++ b/tests/regressiontests/logging_tests/logconfig.py
102@@ -0,0 +1,7 @@
103+import logging
104+
105+from django.conf import settings
106+
107+class MyHandler(logging.Handler):
108+    def __init__(self, *args, **kwargs):
109+        self.config = settings.LOGGING
110diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py
111index f444e0f..a54b425 100644
112--- a/tests/regressiontests/logging_tests/tests.py
113+++ b/tests/regressiontests/logging_tests/tests.py
114@@ -10,6 +10,8 @@ from django.test import TestCase, RequestFactory
115 from django.test.utils import override_settings
116 from django.utils.log import CallbackFilter, RequireDebugFalse
117 
118+from ..admin_scripts.tests import AdminScriptTestCase
119+
120 
121 # logging config prior to using filter with mail_admins
122 OLD_LOGGING = {
123@@ -253,3 +255,30 @@ class AdminEmailHandlerTest(TestCase):
124 
125         self.assertEqual(len(mail.outbox), 1)
126         self.assertEqual(mail.outbox[0].subject, expected_subject)
127+
128+
129+class SettingsConfigTest(AdminScriptTestCase):
130+    """
131+    Test that accessing settings in a custom logging handler does not trigger
132+    a circular import error.
133+    """
134+    def setUp(self):
135+        log_config = """{
136+    'version': 1,
137+    'handlers': {
138+        'custom_handler': {
139+            'level': 'INFO',
140+            'class': 'logging_tests.logconfig.MyHandler',
141+        }
142+    }
143+}"""
144+        self.write_settings('settings.py', sdict={'LOGGING': log_config})
145+
146+    def tearDown(self):
147+        self.remove_settings('settings.py')
148+
149+    def test_circular_dependency(self):
150+        # validate is just an example command to trigger settings configuration
151+        out, err = self.run_manage(['validate'])
152+        self.assertNoOutput(err)
153+        self.assertOutput(out, "0 errors found")