public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/python-django-configurations] rawhide: Fix Python 3.15 compatibility: move to PEP-451 style loader
@ 2026-06-06 10:53 
  0 siblings, 0 replies; only message in thread
From:  @ 2026-06-06 10:53 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/python-django-configurations
            Branch : rawhide
            Commit : 3ae4328864864183022b6fc0b8bcb6c511e1a378
            Author : Tomáš Hrnčiar <thrnciar@redhat.com>
            Date   : 2026-06-06T10:33:59+00:00
            Stats  : +281/-0 in 2 file(s)
            URL    : https://src.fedoraproject.org/rpms/python-django-configurations/c/3ae4328864864183022b6fc0b8bcb6c511e1a378?branch=rawhide

            Log:
            Fix Python 3.15 compatibility: move to PEP-451 style loader

Backport upstream PR#386 to replace the deprecated load_module() API
with exec_module(), which Python 3.15 requires.

https: //github.com/jazzband/django-configurations/pull/386
Co-authored-by: Cursor <cursoragent@cursor.com>

---
diff --git a/fix-pep451-exec-module.patch b/fix-pep451-exec-module.patch
new file mode 100644
index 0000000..db4b284
--- /dev/null
+++ b/fix-pep451-exec-module.patch
@@ -0,0 +1,278 @@
+diff --git a/configurations/importer.py b/configurations/importer.py
+index 499eab6..01469c6 100644
+--- a/configurations/importer.py
++++ b/configurations/importer.py
+@@ -1,4 +1,3 @@
+-import importlib.util
+ from importlib.machinery import PathFinder
+ import logging
+ import os
+@@ -47,12 +46,12 @@ def install(check_options=False):
+             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, "
+@@ -71,7 +70,7 @@ class ConfigurationImporter:
+             self.announce()
+ 
+     def __repr__(self):
+-        return "<ConfigurationImporter for '{0}.{1}'>".format(self.module,
++        return "<ConfigurationFinder for '{0}.{1}'>".format(self.module,
+                                                               self.name)
+ 
+     @property
+@@ -129,56 +128,51 @@ class ConfigurationImporter:
+ 
+     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 = '{0}.{1}'.format(mod.__name__, self.name)
+-
+-        try:
+-            cls = getattr(mod, self.name)
+-        except AttributeError as err:  # pragma: no cover
+-            reraise(err, "Couldn't find configuration '{0}' "
+-                         "in module '{1}'".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', '{0}.{1}'.format(fullname,
+-                                                           self.name))
+-            cls.post_setup()
+-
+-        except Exception as err:
+-            reraise(err, "Couldn't setup configuration '{0}'".format(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 = '{0}.{1}'.format(mod.__name__, class_name)
++
++            try:
++                cls = getattr(mod, class_name)
++            except AttributeError as err:  # pragma: no cover
++                reraise(
++                    err,
++                    "Couldn't find configuration '{0}' in "
++                    "module '{1}'".format(class_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', '{0}.{1}'.format(module.__name__,
++                                                               class_name))
++                cls.post_setup()
++
++            except Exception as err:
++                reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))
++
++    loader.__class__ = ConfigurationLoader
+diff --git a/tests/settings/dot_env.py b/tests/settings/dot_env.py
+index eab4237..7c9d3e6 100644
+--- a/tests/settings/dot_env.py
++++ b/tests/settings/dot_env.py
+@@ -6,3 +6,6 @@ class DotEnvConfiguration(Configuration):
+     DOTENV = 'test_project/.env'
+ 
+     DOTENV_VALUE = values.Value()
++
++    def DOTENV_VALUE_METHOD(self):
++        return values.Value(environ_name="DOTENV_VALUE")
+diff --git a/tests/settings/error.py b/tests/settings/error.py
+new file mode 100644
+index 0000000..a356910
+--- /dev/null
++++ b/tests/settings/error.py
+@@ -0,0 +1,8 @@
++from configurations import Configuration
++
++
++class ErrorConfiguration(Configuration):
++
++    @classmethod
++    def pre_setup(cls):
++        raise ValueError("Error in pre_setup")
+diff --git a/tests/test_env.py b/tests/test_env.py
+index 8066eea..50b7f66 100644
+--- a/tests/test_env.py
++++ b/tests/test_env.py
+@@ -11,4 +11,5 @@ class DotEnvLoadingTests(TestCase):
+     def test_env_loaded(self):
+         from tests.settings import dot_env
+         self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
++        self.assertEqual(dot_env.DOTENV_VALUE_METHOD, 'is set')
+         self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)
+diff --git a/tests/test_error.py b/tests/test_error.py
+new file mode 100644
+index 0000000..85e87da
+--- /dev/null
++++ b/tests/test_error.py
+@@ -0,0 +1,22 @@
++import os
++from django.test import TestCase
++from unittest.mock import patch
++
++
++class ErrorTests(TestCase):
++
++    @patch.dict(os.environ, clear=True,
++                DJANGO_CONFIGURATION='ErrorConfiguration',
++                DJANGO_SETTINGS_MODULE='tests.settings.error')
++    def test_env_loaded(self):
++        with self.assertRaises(ValueError) as cm:
++            from tests.settings import error  # noqa: F401
++
++        self.assertIsInstance(cm.exception, ValueError)
++        self.assertEqual(
++            cm.exception.args,
++            (
++                "Couldn't setup configuration "
++                "'tests.settings.error.ErrorConfiguration':  Error in pre_setup ",
++            )
++        )
+diff --git a/tests/test_main.py b/tests/test_main.py
+index ff9ad54..47a9eb6 100644
+--- a/tests/test_main.py
++++ b/tests/test_main.py
+@@ -7,7 +7,7 @@ from django.core.exceptions import ImproperlyConfigured
+ 
+ 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')
+@@ -42,12 +42,14 @@ class MainTests(TestCase):
+ 
+     @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
+@@ -70,21 +72,21 @@ class MainTests(TestCase):
+                 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',
+@@ -93,12 +95,12 @@ class MainTests(TestCase):
+                                 '--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):
+         """

diff --git a/python-django-configurations.spec b/python-django-configurations.spec
index 3fbfb07..f66d7f8 100644
--- a/python-django-configurations.spec
+++ b/python-django-configurations.spec
@@ -17,6 +17,9 @@ URL:            https://django-configurations.readthedocs.io/
 Source:         %{pypi_source}
 Patch:          %{pypi_name}-adjust_test_cases.diff
 Patch:          django-redis.diff
+# Fix Python 3.15 compatibility: move to PEP-451 style loader (exec_module)
+# https://github.com/jazzband/django-configurations/pull/386
+Patch:          fix-pep451-exec-module.patch
 
 BuildArch:      noarch
 

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-06-06 10:53 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-06 10:53 [rpms/python-django-configurations] rawhide: Fix Python 3.15 compatibility: move to PEP-451 style loader 

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox