Skip to content

Commit 57b6827

Browse files
authored
Merge pull request #386 from adamchainz/pep_451
Move to PEP-451 style loader
2 parents fcd03ad + b8f66f7 commit 57b6827

File tree

6 files changed

+104
-72
lines changed

6 files changed

+104
-72
lines changed

configurations/importer.py

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib.util
21
from importlib.machinery import PathFinder
32
import logging
43
import os
@@ -47,12 +46,12 @@ def create_parser(self, prog_name, subcommand):
4746
return parser
4847

4948
base.BaseCommand.create_parser = create_parser
50-
importer = ConfigurationImporter(check_options=check_options)
49+
importer = ConfigurationFinder(check_options=check_options)
5150
sys.meta_path.insert(0, importer)
5251
installed = True
5352

5453

55-
class ConfigurationImporter:
54+
class ConfigurationFinder(PathFinder):
5655
modvar = SETTINGS_ENVIRONMENT_VARIABLE
5756
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
5857
error_msg = ("Configuration cannot be imported, "
@@ -71,7 +70,7 @@ def __init__(self, check_options=False):
7170
self.announce()
7271

7372
def __repr__(self):
74-
return "<ConfigurationImporter for '{}.{}'>".format(self.module,
73+
return "<ConfigurationFinder for '{}.{}'>".format(self.module,
7574
self.name)
7675

7776
@property
@@ -129,56 +128,53 @@ def stylize(text):
129128

130129
def find_spec(self, fullname, path=None, target=None):
131130
if fullname is not None and fullname == self.module:
132-
spec = PathFinder.find_spec(fullname, path)
131+
spec = super().find_spec(fullname, path, target)
133132
if spec is not None:
134-
return importlib.machinery.ModuleSpec(spec.name,
135-
ConfigurationLoader(self.name, spec),
136-
origin=spec.origin)
137-
return None
138-
139-
140-
class ConfigurationLoader:
141-
142-
def __init__(self, name, spec):
143-
self.name = name
144-
self.spec = spec
145-
146-
def load_module(self, fullname):
147-
if fullname in sys.modules:
148-
mod = sys.modules[fullname] # pragma: no cover
133+
wrap_loader(spec.loader, self.name)
134+
return spec
149135
else:
150-
mod = importlib.util.module_from_spec(self.spec)
151-
sys.modules[fullname] = mod
152-
self.spec.loader.exec_module(mod)
153-
154-
cls_path = f'{mod.__name__}.{self.name}'
155-
156-
try:
157-
cls = getattr(mod, self.name)
158-
except AttributeError as err: # pragma: no cover
159-
reraise(err, "Couldn't find configuration '{}' "
160-
"in module '{}'".format(self.name,
161-
mod.__package__))
162-
try:
163-
cls.pre_setup()
164-
cls.setup()
165-
obj = cls()
166-
attributes = uppercase_attributes(obj).items()
167-
for name, value in attributes:
168-
if callable(value) and not getattr(value, 'pristine', False):
169-
value = value()
170-
# in case a method returns a Value instance we have
171-
# to do the same as the Configuration.setup method
172-
if isinstance(value, Value):
173-
setup_value(mod, name, value)
174-
continue
175-
setattr(mod, name, value)
176-
177-
setattr(mod, 'CONFIGURATION', '{}.{}'.format(fullname,
178-
self.name))
179-
cls.post_setup()
180-
181-
except Exception as err:
182-
reraise(err, f"Couldn't setup configuration '{cls_path}'")
183-
184-
return mod
136+
return None
137+
138+
139+
def wrap_loader(loader, class_name):
140+
class ConfigurationLoader(loader.__class__):
141+
def exec_module(self, module):
142+
super().exec_module(module)
143+
144+
mod = module
145+
146+
cls_path = f'{mod.__name__}.{class_name}'
147+
148+
try:
149+
cls = getattr(mod, class_name)
150+
except AttributeError as err: # pragma: no cover
151+
reraise(
152+
err,
153+
(
154+
f"Couldn't find configuration '{class_name}' in "
155+
f"module '{mod.__package__}'"
156+
),
157+
)
158+
try:
159+
cls.pre_setup()
160+
cls.setup()
161+
obj = cls()
162+
attributes = uppercase_attributes(obj).items()
163+
for name, value in attributes:
164+
if callable(value) and not getattr(value, 'pristine', False):
165+
value = value()
166+
# in case a method returns a Value instance we have
167+
# to do the same as the Configuration.setup method
168+
if isinstance(value, Value):
169+
setup_value(mod, name, value)
170+
continue
171+
setattr(mod, name, value)
172+
173+
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__,
174+
class_name))
175+
cls.post_setup()
176+
177+
except Exception as err:
178+
reraise(err, f"Couldn't setup configuration '{cls_path}'")
179+
180+
loader.__class__ = ConfigurationLoader

tests/settings/dot_env.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ class DotEnvConfiguration(Configuration):
66
DOTENV = 'test_project/.env'
77

88
DOTENV_VALUE = values.Value()
9+
10+
def DOTENV_VALUE_METHOD(self):
11+
return values.Value(environ_name="DOTENV_VALUE")

tests/settings/error.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from configurations import Configuration
2+
3+
4+
class ErrorConfiguration(Configuration):
5+
6+
@classmethod
7+
def pre_setup(cls):
8+
raise ValueError("Error in pre_setup")

tests/test_env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ class DotEnvLoadingTests(TestCase):
1111
def test_env_loaded(self):
1212
from tests.settings import dot_env
1313
self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
14+
self.assertEqual(dot_env.DOTENV_VALUE_METHOD, 'is set')
1415
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)

tests/test_error.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
from django.test import TestCase
3+
from unittest.mock import patch
4+
5+
6+
class ErrorTests(TestCase):
7+
8+
@patch.dict(os.environ, clear=True,
9+
DJANGO_CONFIGURATION='ErrorConfiguration',
10+
DJANGO_SETTINGS_MODULE='tests.settings.error')
11+
def test_env_loaded(self):
12+
with self.assertRaises(ValueError) as cm:
13+
from tests.settings import error # noqa: F401
14+
15+
self.assertIsInstance(cm.exception, ValueError)
16+
self.assertEqual(
17+
cm.exception.args,
18+
(
19+
"Couldn't setup configuration "
20+
"'tests.settings.error.ErrorConfiguration': Error in pre_setup ",
21+
)
22+
)

tests/test_main.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from unittest.mock import patch
99

10-
from configurations.importer import ConfigurationImporter
10+
from configurations.importer import ConfigurationFinder
1111

1212
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
1313
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project')
@@ -42,12 +42,14 @@ def test_global_arrival(self):
4242

4343
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
4444
def test_empty_module_var(self):
45-
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
45+
with self.assertRaises(ImproperlyConfigured):
46+
ConfigurationFinder()
4647

4748
@patch.dict(os.environ, clear=True,
4849
DJANGO_SETTINGS_MODULE='tests.settings.main')
4950
def test_empty_class_var(self):
50-
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
51+
with self.assertRaises(ImproperlyConfigured):
52+
ConfigurationFinder()
5153

5254
def test_global_settings(self):
5355
from configurations.base import Configuration
@@ -70,21 +72,21 @@ def test_repr(self):
7072
DJANGO_SETTINGS_MODULE='tests.settings.main',
7173
DJANGO_CONFIGURATION='Test')
7274
def test_initialization(self):
73-
importer = ConfigurationImporter()
74-
self.assertEqual(importer.module, 'tests.settings.main')
75-
self.assertEqual(importer.name, 'Test')
75+
finder = ConfigurationFinder()
76+
self.assertEqual(finder.module, 'tests.settings.main')
77+
self.assertEqual(finder.name, 'Test')
7678
self.assertEqual(
77-
repr(importer),
78-
"<ConfigurationImporter for 'tests.settings.main.Test'>")
79+
repr(finder),
80+
"<ConfigurationFinder for 'tests.settings.main.Test'>")
7981

8082
@patch.dict(os.environ, clear=True,
8183
DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
8284
DJANGO_CONFIGURATION='Inheritance')
8385
def test_initialization_inheritance(self):
84-
importer = ConfigurationImporter()
85-
self.assertEqual(importer.module,
86+
finder = ConfigurationFinder()
87+
self.assertEqual(finder.module,
8688
'tests.settings.inheritance')
87-
self.assertEqual(importer.name, 'Inheritance')
89+
self.assertEqual(finder.name, 'Inheritance')
8890

8991
@patch.dict(os.environ, clear=True,
9092
DJANGO_SETTINGS_MODULE='tests.settings.main',
@@ -93,12 +95,12 @@ def test_initialization_inheritance(self):
9395
'--settings=tests.settings.main',
9496
'--configuration=Test'])
9597
def test_configuration_option(self):
96-
importer = ConfigurationImporter(check_options=False)
97-
self.assertEqual(importer.module, 'tests.settings.main')
98-
self.assertEqual(importer.name, 'NonExisting')
99-
importer = ConfigurationImporter(check_options=True)
100-
self.assertEqual(importer.module, 'tests.settings.main')
101-
self.assertEqual(importer.name, 'Test')
98+
finder = ConfigurationFinder(check_options=False)
99+
self.assertEqual(finder.module, 'tests.settings.main')
100+
self.assertEqual(finder.name, 'NonExisting')
101+
finder = ConfigurationFinder(check_options=True)
102+
self.assertEqual(finder.module, 'tests.settings.main')
103+
self.assertEqual(finder.name, 'Test')
102104

103105
def test_configuration_argument_in_cli(self):
104106
"""

0 commit comments

Comments
 (0)