Package: src:python-repoze.who
Version: 3.1.0-1
User: [email protected]
Usertags: python3.15
Tags: patch, ftbfs, forky, sid

Hi!

While rebuilding the python related packages against the Python 3.15rc1
version we found that python-repoze.who fails to build from source [1].

The build errors when trying to use the deprecated pkg_resources. I
found that the upstream patch to modernize the build tooling takes care
of the problem [2].

I applied the upstream fix in the sandbox [3] to be able to build the
packages that depend on python-repoze.who, please consider applying the
patch to support the upcoming 3.15 version.

Happy hacking,

[1]: 
https://debusine.debian.net/debian/r-python-python3.15/work-request/1075339/
[2]: 
https://github.com/repoze/repoze.who/commit/8521c98925ac694b62d1c5c8ce1142df6384a3f8
[3]: https://debusine.debian.net/debian/r-python-python3.15/

--
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
From 8521c98925ac694b62d1c5c8ce1142df6384a3f8 Mon Sep 17 00:00:00 2001
From: Alessandro Molina <[email protected]>
Date: Tue, 12 May 2026 15:01:36 +0200
Subject: [PATCH] chore: modernize repoze.who

- Add support for Python 3.14.
- Drop support for Python < 3.10.
- Drop use of deprecated 'pkg_resources':
  - Drop use of entry points in favor of  local dotted name resolution.
  - Adopt "native" namespaces.
---
 pyproject.toml                            | 62 +++++++++++++++
 repoze/__init__.py                        |  2 -
 repoze/who/__init__.py                    |  2 -
 repoze/who/config.py                      |  8 +-
 repoze/who/middleware.py                  |  6 +-
 repoze/who/plugins/__init__.py            |  2 -
 repoze/who/restrict.py                    |  4 +-
 repoze/who/tests/test_namespace_compat.py | 92 +++++++++++++++++++++++
 repoze/who/tests/test_utils.py            | 38 ++++++++++
 repoze/who/utils.py                       | 14 +++-
 setup.cfg                                 |  5 --
 setup.py                                  | 80 +-------------------
 12 files changed, 213 insertions(+), 102 deletions(-)
 create mode 100644 pyproject.toml
 delete mode 100644 repoze/__init__.py
 delete mode 100644 repoze/who/__init__.py
 delete mode 100644 repoze/who/plugins/__init__.py
 create mode 100644 repoze/who/tests/test_namespace_compat.py
 create mode 100644 repoze/who/tests/test_utils.py
 delete mode 100644 setup.cfg

diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9c2abae
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,62 @@
+[build-system]
+requires = ["setuptools>=68", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "repoze.who"
+version = "3.1.0"
+description = "repoze.who is an identification and authentication framework for WSGI."
+dynamic = ["readme"]
+requires-python = ">=3.10"
+keywords = ["web", "application", "server", "wsgi", "zope"]
+license = { text = "BSD-derived (http://www.repoze.org/LICENSE.txt)" }
+authors = [
+  { name = "Agendaless Consulting", email = "[email protected]" },
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Intended Audience :: Developers",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+  "Programming Language :: Python :: 3.13",
+  "Programming Language :: Python :: 3.14",
+  "Programming Language :: Python :: Implementation :: CPython",
+  "Programming Language :: Python :: Implementation :: PyPy",
+  "Topic :: Internet :: WWW/HTTP",
+  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+  "Topic :: Internet :: WWW/HTTP :: WSGI",
+  "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+]
+dependencies = [
+  "WebOb",
+  "zope.interface",
+  "legacy-cgi; python_version > '3.12'",
+]
+
+[project.urls]
+Homepage = "http://www.repoze.org";
+
+[project.optional-dependencies]
+docs = ["Sphinx", "repoze.sphinx.autointerface"]
+
+[project.entry-points."paste.filter_app_factory"]
+test = "repoze.who.middleware:make_test_middleware"
+config = "repoze.who.config:make_middleware_with_config"
+predicate = "repoze.who.restrict:make_predicate_restriction"
+authenticated = "repoze.who.restrict:make_authenticated_restriction"
+
+[tool.setuptools]
+include-package-data = false
+zip-safe = false
+
+[tool.setuptools.packages.find]
+include = ["repoze*"]
+exclude = ["repoze.who.tests*", "repoze.who.plugins.tests*"]
+
+[tool.setuptools.dynamic]
+readme = { file = ["README.rst", "CHANGES.rst"], content-type = "text/x-rst" }
+
+[tool.pytest.ini_options]
+consider_namespace_packages = true
diff --git a/repoze/__init__.py b/repoze/__init__.py
deleted file mode 100644
index a958a99..0000000
--- a/repoze/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# repoze package
-__import__('pkg_resources').declare_namespace(__name__)
diff --git a/repoze/who/__init__.py b/repoze/who/__init__.py
deleted file mode 100644
index ccaa6c7..0000000
--- a/repoze/who/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# repoze.who package
-__import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE
diff --git a/repoze/who/config.py b/repoze/who/config.py
index fe368db..a66fa10 100644
--- a/repoze/who/config.py
+++ b/repoze/who/config.py
@@ -3,7 +3,6 @@
 import configparser
 from io import StringIO
 import logging
-from pkg_resources import EntryPoint
 import sys
 import warnings
 
@@ -16,10 +15,7 @@ from repoze.who.interfaces import IMetadataProvider
 from repoze.who.interfaces import IPlugin
 from repoze.who.interfaces import IRequestClassifier
 from repoze.who.middleware import PluggableAuthenticationMiddleware
-
-def _resolve(name):
-    if name:
-        return EntryPoint.parse('x=%s' % name).resolve()
+from repoze.who.utils import resolveDotted
 
 class WhoConfig:
     def __init__(self, here):
@@ -36,7 +32,7 @@ class WhoConfig:
     def _makePlugin(self, name, iface, options=None):
         if options is None:
             options = {}
-        obj = _resolve(name)
+        obj = resolveDotted(name)
         if not iface.providedBy(obj):
             obj = obj(**options)
         return obj
diff --git a/repoze/who/middleware.py b/repoze/who/middleware.py
index 1ae98a3..9e5526a 100644
--- a/repoze/who/middleware.py
+++ b/repoze/who/middleware.py
@@ -187,7 +187,7 @@ def make_test_middleware(app, global_conf):
     """ Functionally equivalent to
 
     [plugin:redirector]
-    use = repoze.who.plugins.redirector.RedirectorPlugin
+    use = repoze.who.plugins.redirector:RedirectorPlugin
     login_url = /login.html
 
     [plugin:auth_tkt]
@@ -196,11 +196,11 @@ def make_test_middleware(app, global_conf):
     cookie_name = oatmeal
 
     [plugin:basicauth]
-    use = repoze.who.plugins.basicauth.BasicAuthPlugin
+    use = repoze.who.plugins.basicauth:BasicAuthPlugin
     realm = repoze.who
 
     [plugin:htpasswd]
-    use = repoze.who.plugins.htpasswd.HTPasswdPlugin
+    use = repoze.who.plugins.htpasswd:HTPasswdPlugin
     filename = <...>
     check_fn = repoze.who.plugins.htpasswd:crypt_check
 
diff --git a/repoze/who/plugins/__init__.py b/repoze/who/plugins/__init__.py
deleted file mode 100644
index fc15384..0000000
--- a/repoze/who/plugins/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# repoze.who.plugins package
-__import__('pkg_resources').declare_namespace(__name__) #pragma NO COVERAGE
diff --git a/repoze/who/restrict.py b/repoze/who/restrict.py
index d74d34c..d2c18dc 100644
--- a/repoze/who/restrict.py
+++ b/repoze/who/restrict.py
@@ -1,5 +1,5 @@
 # Authorization middleware
-from pkg_resources import EntryPoint
+from repoze.who.utils import resolveDotted
 
 def authenticated_predicate():
     def _predicate(environ):
@@ -27,5 +27,5 @@ def make_authenticated_restriction(app, global_config, enabled=True):
 def make_predicate_restriction(app, global_config,
                                predicate, enabled=True, **kw):
     if isinstance(predicate, str):
-        predicate = EntryPoint.parse('x=%s' % predicate).resolve()
+        predicate = resolveDotted(predicate)
     return PredicateRestriction(app, predicate, enabled, **kw)
diff --git a/repoze/who/tests/test_namespace_compat.py b/repoze/who/tests/test_namespace_compat.py
new file mode 100644
index 0000000..c6239bb
--- /dev/null
+++ b/repoze/who/tests/test_namespace_compat.py
@@ -0,0 +1,92 @@
+import pathlib
+import shutil
+import sys
+import tempfile
+import unittest
+
+
+class NamespaceCompatibilityTests(unittest.TestCase):
+
+    def _create_sibling(self):
+        workspace = pathlib.Path(tempfile.mkdtemp())
+        sibling_root = workspace / "sibling"
+        plugin_pkg = sibling_root / "repoze" / "who" / "plugins"
+        plugin_pkg.mkdir(parents=True)
+
+        (plugin_pkg / "sibling_identifier.py").write_text(
+            "from zope.interface import implementer\n"
+            "from repoze.who.interfaces import IIdentifier\n"
+            "\n"
+            "@implementer(IIdentifier)\n"
+            "class SiblingIdentifier:\n"
+            "    def __init__(self, marker=None):\n"
+            "        self.marker = marker\n"
+            "\n"
+            "    def identify(self, environ):\n"
+            "        return None\n"
+            "\n"
+            "    def remember(self, environ, identity):\n"
+            "        return []\n"
+            "\n"
+            "    def forget(self, environ, identity):\n"
+            "        return []\n"
+            "\n"
+            "def make_plugin(marker=None):\n"
+            "    return SiblingIdentifier(marker)\n",
+            encoding="utf-8",
+        )
+        return workspace, sibling_root
+
+    def _assert_native_namespace(self, module):
+        self.assertIsNone(module.__spec__.origin)
+        self.assertIsNotNone(module.__spec__.submodule_search_locations)
+
+    def _clear_repoze_modules(self):
+        for module_name in list(sys.modules):
+            if module_name == "repoze" or module_name.startswith("repoze."):
+                del sys.modules[module_name]
+
+    def test_config_resolves_plugin_from_sibling_namespace_package(self):
+        workspace, sibling_root = self._create_sibling()
+
+        original_sys_path = list(sys.path)
+        original_modules = {
+            k: v
+            for k, v in sys.modules.items()
+            if k == "repoze" or k.startswith("repoze.")
+        }
+        self._clear_repoze_modules()
+
+        try:
+            sys.path.insert(0, str(sibling_root))
+            import repoze
+            import repoze.who
+            import repoze.who.plugins
+            from repoze.who.config import WhoConfig
+
+            self._assert_native_namespace(repoze)
+            self._assert_native_namespace(repoze.who)
+            self._assert_native_namespace(repoze.who.plugins)
+
+            config = WhoConfig("/")
+            config.parse(
+                "[plugin:sibling]\n"
+                "use = repoze.who.plugins.sibling_identifier:make_plugin\n"
+                "marker = loaded from sibling namespace\n"
+                "\n"
+                "[identifiers]\n"
+                "plugins = sibling\n"
+            )
+
+            plugin = config.plugins["sibling"]
+            self.assertEqual(plugin.marker, "loaded from sibling namespace")
+            self.assertEqual(
+                plugin.__class__.__module__,
+                "repoze.who.plugins.sibling_identifier",
+            )
+            self.assertEqual(config.identifiers, [("sibling", plugin)])
+        finally:
+            sys.path[:] = original_sys_path
+            self._clear_repoze_modules()
+            sys.modules.update(original_modules)
+            shutil.rmtree(str(workspace))
diff --git a/repoze/who/tests/test_utils.py b/repoze/who/tests/test_utils.py
new file mode 100644
index 0000000..9d222d8
--- /dev/null
+++ b/repoze/who/tests/test_utils.py
@@ -0,0 +1,38 @@
+import unittest
+
+
+class ResolveDottedTests(unittest.TestCase):
+
+    def _callFUT(self, dotted_or_ep):
+        from repoze.who.utils import resolveDotted
+        return resolveDotted(dotted_or_ep)
+
+    def test_resolve_module_colon_object(self):
+        resolved = self._callFUT("repoze.who.tests.test_utils:DummyCallable")
+        self.assertEqual(resolved.__name__, "DummyCallable")
+        self.assertIn("test_utils", resolved.__module__)
+
+    def test_resolve_missing_colon_raises_value_error(self):
+        self.assertRaises(
+            ValueError,
+            self._callFUT,
+            "repoze.who.tests.test_utils.DummyCallable",
+        )
+
+    def test_resolve_empty_object_raises_value_error(self):
+        self.assertRaises(
+            ValueError,
+            self._callFUT,
+            "repoze.who.tests.test_utils:",
+        )
+
+    def test_resolve_extras_suffix_raises_value_error(self):
+        self.assertRaises(
+            ValueError,
+            self._callFUT,
+            "repoze.who.tests.test_utils:DummyCallable [extra]",
+        )
+
+
+class DummyCallable:
+    pass
diff --git a/repoze/who/utils.py b/repoze/who/utils.py
index 857dd2d..0afe2e0 100644
--- a/repoze/who/utils.py
+++ b/repoze/who/utils.py
@@ -1,5 +1,13 @@
+from importlib.metadata import EntryPoint
+
+
 def resolveDotted(dotted_or_ep):
-    """ Resolve a dotted name or setuptools entry point to a callable.
+    """Resolve a standard ``module:object`` reference to a callable.
     """
-    from pkg_resources import EntryPoint
-    return EntryPoint.parse('x=%s' % dotted_or_ep).resolve()
+    name = dotted_or_ep.strip()
+    if ":" not in name or "[" in name or "]" in name:
+        raise ValueError(f"Invalid dotted name: {name}")
+    module_name, object_name = name.split(":", 1)
+    if not module_name.strip() or not object_name.strip():
+        raise ValueError(f"Invalid dotted name: {name}")
+    return EntryPoint(name="x", value=name, group="x").load()
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index c4bd7a6..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[easy_install]
-zip_ok = false
-
-[aliases]
-dev = develop
diff --git a/setup.py b/setup.py
index b2b3224..26e08e4 100644
--- a/setup.py
+++ b/setup.py
@@ -1,79 +1,5 @@
-##############################################################################
-#
-# Copyright (c) 2007-2009 Agendaless Consulting and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the BSD-like license at
-# http://www.repoze.org/LICENSE.txt.  A copy of the license should accompany
-# this distribution.  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL
-# EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO,
-# THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
-# FITNESS FOR A PARTICULAR PURPOSE
-#
-##############################################################################
+from setuptools import setup
 
-import os
 
-from setuptools import setup, find_packages
-
-here = os.path.abspath(os.path.dirname(__file__))
-def _read_file(filename):
-    try:
-        with open(os.path.join(here, filename)) as f:
-            return f.read()
-    except IOError:  # Travis???
-        return ''
-
-README = _read_file('README.rst')
-CHANGES = _read_file('CHANGES.rst')
-
-setup(name='repoze.who',
-      version='3.1.0',
-      description=('repoze.who is an identification and authentication '
-                   'framework for WSGI.'),
-      long_description='\n\n'.join([README, CHANGES]),
-      long_description_content_type="text/x-rst",
-      classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Programming Language :: Python :: 3.13",
-        "Programming Language :: Python :: Implementation :: CPython",
-        "Programming Language :: Python :: Implementation :: PyPy",
-        "Topic :: Internet :: WWW/HTTP",
-        "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
-        "Topic :: Internet :: WWW/HTTP :: WSGI",
-        "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
-        ],
-      python_requires=">=3.9",
-      keywords='web application server wsgi zope',
-      author="Agendaless Consulting",
-      author_email="[email protected]",
-      url="http://www.repoze.org";,
-      license="BSD-derived (http://www.repoze.org/LICENSE.txt)",
-      packages=find_packages(),
-      include_package_data=True,
-      namespace_packages=['repoze', 'repoze.who', 'repoze.who.plugins'],
-      zip_safe=False,
-      install_requires=[
-        'WebOb',
-        'zope.interface',
-        'setuptools',
-        'legacy-cgi; python_version > "3.12"',  # WebOb uses the cgi module
-      ],
-      test_suite="repoze.who",
-      entry_points = """\
-      [paste.filter_app_factory]
-      test = repoze.who.middleware:make_test_middleware
-      config = repoze.who.config:make_middleware_with_config
-      predicate = repoze.who.restrict:make_predicate_restriction
-      authenticated = repoze.who.restrict:make_authenticated_restriction
-      """,
-      extras_require = {
-        'docs': ['Sphinx', 'repoze.sphinx.autointerface'],
-      },
-)
+if __name__ == "__main__":
+    setup()
-- 
2.53.0

Reply via email to