Skip to content

Commit

Permalink
Move to PEP-451 style loader
Browse files Browse the repository at this point in the history
  • Loading branch information
adamchainz committed Nov 18, 2024
1 parent f37ed87 commit 8ab099b
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 72 deletions.
106 changes: 51 additions & 55 deletions configurations/importer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib.util
from importlib.machinery import PathFinder
import logging
import os
Expand Down Expand Up @@ -47,12 +46,12 @@ def create_parser(self, prog_name, subcommand):
return parser

base.BaseCommand.create_parser = create_parser
importer = ConfigurationImporter(check_options=check_options)
importer = ConfigurationFinder(check_options=check_options)
sys.meta_path.insert(0, importer)
installed = True


class ConfigurationImporter:
class ConfigurationFinder(PathFinder):
modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, "
Expand All @@ -71,7 +70,7 @@ def __init__(self, check_options=False):
self.announce()

def __repr__(self):
return "<ConfigurationImporter for '{}.{}'>".format(self.module,
return "<ConfigurationFinder for '{}.{}'>".format(self.module,
self.name)

@property
Expand Down Expand Up @@ -129,56 +128,53 @@ def stylize(text):

def find_spec(self, fullname, path=None, target=None):
if fullname is not None and fullname == self.module:
spec = PathFinder.find_spec(fullname, path)
spec = super().find_spec(fullname, path, target)
if spec is not None:
return importlib.machinery.ModuleSpec(spec.name,
ConfigurationLoader(self.name, spec),
origin=spec.origin)
return None


class ConfigurationLoader:

def __init__(self, name, spec):
self.name = name
self.spec = spec

def load_module(self, fullname):
if fullname in sys.modules:
mod = sys.modules[fullname] # pragma: no cover
wrap_loader(spec.loader, self.name)
return spec
else:
mod = importlib.util.module_from_spec(self.spec)
sys.modules[fullname] = mod
self.spec.loader.exec_module(mod)

cls_path = f'{mod.__name__}.{self.name}'

try:
cls = getattr(mod, self.name)
except AttributeError as err: # pragma: no cover
reraise(err, "Couldn't find configuration '{}' "
"in module '{}'".format(self.name,
mod.__package__))
try:
cls.pre_setup()
cls.setup()
obj = cls()
attributes = uppercase_attributes(obj).items()
for name, value in attributes:
if callable(value) and not getattr(value, 'pristine', False):
value = value()
# in case a method returns a Value instance we have
# to do the same as the Configuration.setup method
if isinstance(value, Value):
setup_value(mod, name, value)
continue
setattr(mod, name, value)

setattr(mod, 'CONFIGURATION', '{}.{}'.format(fullname,
self.name))
cls.post_setup()

except Exception as err:
reraise(err, f"Couldn't setup configuration '{cls_path}'")

return mod
return None


def wrap_loader(loader, class_name):
class ConfigurationLoader(loader.__class__):
def exec_module(self, module):
super().exec_module(module)

mod = module

cls_path = f'{mod.__name__}.{class_name}'

try:
cls = getattr(mod, class_name)
except AttributeError as err: # pragma: no cover
reraise(
err,
(
f"Couldn't find configuration '{class_name}' in "
f"module '{mod.__package__}'"
),
)
try:
cls.pre_setup()
cls.setup()
obj = cls()
attributes = uppercase_attributes(obj).items()
for name, value in attributes:
if callable(value) and not getattr(value, 'pristine', False):
value = value()
# in case a method returns a Value instance we have
# to do the same as the Configuration.setup method
if isinstance(value, Value):
setup_value(mod, name, value)
continue

Check warning on line 170 in configurations/importer.py

View check run for this annotation

Codecov / codecov/patch

configurations/importer.py#L169-L170

Added lines #L169 - L170 were not covered by tests
setattr(mod, name, value)

setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__,
class_name))
cls.post_setup()

except Exception as err:
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))

Check warning on line 178 in configurations/importer.py

View check run for this annotation

Codecov / codecov/patch

configurations/importer.py#L177-L178

Added lines #L177 - L178 were not covered by tests

loader.__class__ = ConfigurationLoader
36 changes: 19 additions & 17 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from unittest.mock import patch

from configurations.importer import ConfigurationImporter
from configurations.importer import ConfigurationFinder

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

@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()

@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main')
def test_empty_class_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()

def test_global_settings(self):
from configurations.base import Configuration
Expand All @@ -70,21 +72,21 @@ def test_repr(self):
DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='Test')
def test_initialization(self):
importer = ConfigurationImporter()
self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(importer.name, 'Test')
finder = ConfigurationFinder()
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test')
self.assertEqual(
repr(importer),
"<ConfigurationImporter for 'tests.settings.main.Test'>")
repr(finder),
"<ConfigurationFinder for 'tests.settings.main.Test'>")

@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
DJANGO_CONFIGURATION='Inheritance')
def test_initialization_inheritance(self):
importer = ConfigurationImporter()
self.assertEqual(importer.module,
finder = ConfigurationFinder()
self.assertEqual(finder.module,
'tests.settings.inheritance')
self.assertEqual(importer.name, 'Inheritance')
self.assertEqual(finder.name, 'Inheritance')

@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main',
Expand All @@ -93,12 +95,12 @@ def test_initialization_inheritance(self):
'--settings=tests.settings.main',
'--configuration=Test'])
def test_configuration_option(self):
importer = ConfigurationImporter(check_options=False)
self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(importer.name, 'NonExisting')
importer = ConfigurationImporter(check_options=True)
self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(importer.name, 'Test')
finder = ConfigurationFinder(check_options=False)
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'NonExisting')
finder = ConfigurationFinder(check_options=True)
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test')

def test_configuration_argument_in_cli(self):
"""
Expand Down

0 comments on commit 8ab099b

Please sign in to comment.