From fc69fff9ab5bba8a6cff3eaf51f02a3204b1c015 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:11:42 +0200 Subject: [PATCH] Fixed #14861 -- Moved logging config outside of Settings.__init__ Thanks donspaulding for the report and simonpercivall for the initial patch. --- django/conf/__init__.py | 30 ++++++++++--------- docs/topics/logging.txt | 30 ------------------- .../logging_tests/logconfig.py | 7 +++++ tests/regressiontests/logging_tests/tests.py | 29 ++++++++++++++++++ 4 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 tests/regressiontests/logging_tests/logconfig.py diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 6272f4ed5d..d636ff0b6c 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -43,13 +43,28 @@ class LazySettings(LazyObject): % (name, ENVIRONMENT_VARIABLE)) self._wrapped = Settings(settings_module) - + self._configure_logging() def __getattr__(self, name): if self._wrapped is empty: self._setup(name) return getattr(self._wrapped, name) + def _configure_logging(self): + """ + Setup logging from LOGGING_CONFIG and LOGGING settings. + """ + if self.LOGGING_CONFIG: + # First find the logging configuration function ... + logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) + logging_config_module = importlib.import_module(logging_config_path) + logging_config_func = getattr(logging_config_module, logging_config_func_name) + + # Backwards-compatibility shim for #16288 fix + compat_patch_logging_config(self.LOGGING) + + # ... then invoke it with the logging settings + logging_config_func(self.LOGGING) def configure(self, default_settings=global_settings, **options): """ @@ -133,19 +148,6 @@ class Settings(BaseSettings): os.environ['TZ'] = self.TIME_ZONE time.tzset() - # Settings are configured, so we can set up the logger if required - if self.LOGGING_CONFIG: - # First find the logging configuration function ... - logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) - logging_config_module = importlib.import_module(logging_config_path) - logging_config_func = getattr(logging_config_module, logging_config_func_name) - - # Backwards-compatibility shim for #16288 fix - compat_patch_logging_config(self.LOGGING) - - # ... then invoke it with the logging settings - logging_config_func(self.LOGGING) - class UserSettingsHolder(BaseSettings): """ diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 94236babd6..a4aae0bc02 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -345,36 +345,6 @@ This logging configuration does the following things: printed to the console; ``ERROR`` and ``CRITICAL`` messages will also be output via email. -.. admonition:: Custom handlers and circular imports - - If your ``settings.py`` specifies a custom handler class and the file - defining that class also imports ``settings.py`` a circular import will - occur. - - For example, if ``settings.py`` contains the following config for - :setting:`LOGGING`:: - - LOGGING = { - 'version': 1, - 'handlers': { - 'custom_handler': { - 'level': 'INFO', - 'class': 'myproject.logconfig.MyHandler', - } - } - } - - and ``myproject/logconfig.py`` has the following line before the - ``MyHandler`` definition:: - - from django.conf import settings - - then the ``dictconfig`` module will raise an exception like the following:: - - ValueError: Unable to configure handler 'custom_handler': - Unable to configure handler 'custom_handler': - 'module' object has no attribute 'logconfig' - .. _formatter documentation: http://docs.python.org/library/logging.html#formatter-objects Custom logging configuration diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py new file mode 100644 index 0000000000..8524aa2c24 --- /dev/null +++ b/tests/regressiontests/logging_tests/logconfig.py @@ -0,0 +1,7 @@ +import logging + +from django.conf import settings + +class MyHandler(logging.Handler): + def __init__(self, *args, **kwargs): + self.config = settings.LOGGING diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index f444e0ff46..a54b425f67 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -10,6 +10,8 @@ from django.test import TestCase, RequestFactory from django.test.utils import override_settings from django.utils.log import CallbackFilter, RequireDebugFalse +from ..admin_scripts.tests import AdminScriptTestCase + # logging config prior to using filter with mail_admins OLD_LOGGING = { @@ -253,3 +255,30 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, expected_subject) + + +class SettingsConfigTest(AdminScriptTestCase): + """ + Test that accessing settings in a custom logging handler does not trigger + a circular import error. + """ + def setUp(self): + log_config = """{ + 'version': 1, + 'handlers': { + 'custom_handler': { + 'level': 'INFO', + 'class': 'logging_tests.logconfig.MyHandler', + } + } +}""" + self.write_settings('settings.py', sdict={'LOGGING': log_config}) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_circular_dependency(self): + # validate is just an example command to trigger settings configuration + out, err = self.run_manage(['validate']) + self.assertNoOutput(err) + self.assertOutput(out, "0 errors found")