Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-lazy-loader for 
openSUSE:Factory checked in at 2026-10-20 12:40:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-lazy-loader (Old)
 and      /work/SRC/openSUSE:Factory/.python-lazy-loader.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-lazy-loader"

Tue Oct 20 12:40:17 2026 rev:5 rq:1386241 version:0.6

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-lazy-loader/python-lazy-loader.changes    
2026-03-16 14:20:01.235989240 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-lazy-loader.new.3698736/python-lazy-loader.changes
       2026-10-20 12:41:44.890092194 +0200
@@ -1,0 +2,9 @@
+Sat Oct 17 15:14:56 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.6:
+  * Improve performance.
+  * Guard against import machinery masking out attributes.
+  * Support Python 3.15.
+  * Add runtime dependency on python-packaging.
+
+-------------------------------------------------------------------

Old:
----
  lazy_loader-0.5.tar.gz

New:
----
  lazy_loader-0.6.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-lazy-loader.spec ++++++
--- /var/tmp/diff_new_pack.sTuAGk/_old  2026-10-20 12:41:45.653124129 +0200
+++ /var/tmp/diff_new_pack.sTuAGk/_new  2026-10-20 12:41:45.655124212 +0200
@@ -17,18 +17,20 @@
 
 
 Name:           python-lazy-loader
-Version:        0.5
+Version:        0.6
 Release:        0
 Summary:        Populate library namespace without incurring immediate import 
costs
 License:        BSD-3-Clause
-URL:            https://github.com/scientific-python/lazy_loader
+URL:            https://github.com/scientific-python/lazy-loader
 Source:         
https://files.pythonhosted.org/packages/source/l/lazy_loader/lazy_loader-%{version}.tar.gz
 BuildRequires:  %{python_module flit-core}
+BuildRequires:  %{python_module packaging}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
+Requires:       python-packaging
 Provides:       python-lazy_loader = %{version}-%{release}
 BuildArch:      noarch
 %python_subpackages

++++++ lazy_loader-0.5.tar.gz -> lazy_loader-0.6.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/CHANGELOG.md 
new/lazy_loader-0.6/CHANGELOG.md
--- old/lazy_loader-0.5/CHANGELOG.md    2026-03-06 16:44:33.000000000 +0100
+++ new/lazy_loader-0.6/CHANGELOG.md    2026-09-21 23:31:11.115617800 +0200
@@ -1,3 +1,70 @@
+# lazy-loader 0.6
+
+We're happy to announce the release of lazy-loader 0.6!
+
+## Enhancements
+
+- improve perf, dogfood 
([#173](https://github.com/scientific-python/lazy-loader/pull/173)).
+
+## Bug Fixes
+
+- Guard against import machinery masking out attributes 
([#171](https://github.com/scientific-python/lazy-loader/pull/171)).
+
+## Maintenance
+
+- Support for Python 3.15 
([#176](https://github.com/scientific-python/lazy-loader/pull/176)).
+- Update release process 
([#177](https://github.com/scientific-python/lazy-loader/pull/177)).
+
+## Contributors
+
+3 authors added to this release (alphabetically):
+
+- [@benedikt-bartscher](https://github.com/benedikt-bartscher)
+- Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman))
+- Stefan van der Walt ([@stefanv](https://github.com/stefanv))
+
+4 reviewers added to this release (alphabetically):
+
+- Brigitta Sipőcz ([@bsipocz](https://github.com/bsipocz))
+- Chris Markiewicz ([@effigies](https://github.com/effigies))
+- Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman))
+- Stefan van der Walt ([@stefanv](https://github.com/stefanv))
+
+_These lists are automatically generated, and may not be complete or may 
contain
+duplicates._
+
+# lazy-loader 0.6rc0
+
+We're happy to announce the release of lazy-loader 0.6rc0!
+
+## Enhancements
+
+- improve perf, dogfood 
([#173](https://github.com/scientific-python/lazy-loader/pull/173)).
+
+## Bug Fixes
+
+- Guard against import machinery masking out attributes 
([#171](https://github.com/scientific-python/lazy-loader/pull/171)).
+
+## Maintenance
+
+- Support for Python 3.15 
([#176](https://github.com/scientific-python/lazy-loader/pull/176)).
+
+## Contributors
+
+3 authors added to this release (alphabetically):
+
+- [@benedikt-bartscher](https://github.com/benedikt-bartscher)
+- Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman))
+- Stefan van der Walt ([@stefanv](https://github.com/stefanv))
+
+3 reviewers added to this release (alphabetically):
+
+- Brigitta Sipőcz ([@bsipocz](https://github.com/bsipocz))
+- Chris Markiewicz ([@effigies](https://github.com/effigies))
+- Stefan van der Walt ([@stefanv](https://github.com/stefanv))
+
+_These lists are automatically generated, and may not be complete or may 
contain duplicates._
+
 # lazy-loader 0.5
 
 ## Enhancements
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/PKG-INFO new/lazy_loader-0.6/PKG-INFO
--- old/lazy_loader-0.5/PKG-INFO        2026-03-06 16:44:41.855128300 +0100
+++ new/lazy_loader-0.6/PKG-INFO        2026-09-21 23:31:17.495432900 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: lazy-loader
-Version: 0.5
+Version: 0.6
 Summary: Makes it easy to load subpackages and functions on demand.
 Author: Scientific Python Developers
 License-Expression: BSD-3-Clause
@@ -14,6 +14,7 @@
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
 Requires-Python: >=3.9
 Description-Content-Type: text/markdown
 License-File: LICENSE.md
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/pyproject.toml 
new/lazy_loader-0.6/pyproject.toml
--- old/lazy_loader-0.5/pyproject.toml  2026-03-06 16:44:33.000000000 +0100
+++ new/lazy_loader-0.6/pyproject.toml  2026-09-21 23:31:11.115617800 +0200
@@ -18,6 +18,7 @@
   "Programming Language :: Python :: 3.12",
   "Programming Language :: Python :: 3.13",
   "Programming Language :: Python :: 3.14",
+  "Programming Language :: Python :: 3.15",
 ]
 description = "Makes it easy to load subpackages and functions on demand."
 dependencies = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/src/lazy_loader/__init__.py 
new/lazy_loader-0.6/src/lazy_loader/__init__.py
--- old/lazy_loader-0.5/src/lazy_loader/__init__.py     2026-03-06 
16:44:33.000000000 +0100
+++ new/lazy_loader-0.6/src/lazy_loader/__init__.py     2026-09-21 
23:31:11.116297500 +0200
@@ -5,20 +5,49 @@
 Makes it easy to load subpackages and functions on demand.
 """
 
-import ast
+import _thread
 import importlib
-import importlib.util
 import os
 import sys
-import threading
 import types
-import warnings
 
-__version__ = "0.5"
+__version__ = "0.6"
 __all__ = ["attach", "attach_stub", "load"]
 
 
-threadlock = threading.Lock()
+# Same lock type as threading.Lock(), without the threading import cost
+threadlock = _thread.allocate_lock()
+
+
+class _ShadowGuardModule(types.ModuleType):
+    """Module type to protect function attributes from being overwritten.
+
+    When a function has the same name as the submodule it resides in
+    (e.g. a ``max_tree`` function defined in ``max_tree.py``),
+    importing that submodule causes the import machinery to call
+    ``setattr(pkg, "max_tree", <submodule>)``.  That updates the
+    package ``__dict__``, preventing ``__getattr__`` from ever
+    resolving the name to the function again. The same problem occurs
+    when ``x`` is defined in ``x/sub.py``.
+
+    This subclass suppresses those dictionary updates (only in the
+    shadowing case).
+
+    We track the set of protected names in the ``__lazy_shadowed__``
+    attr.
+
+    """
+
+    def __setattr__(self, name, value):
+        shadowed = self.__dict__.get("__lazy_shadowed__")
+        if (
+            shadowed is not None
+            and name in shadowed
+            # Is it trying to set this attribute to the system module?
+            and value is sys.modules.get(f"{self.__name__}.{name}")
+        ):
+            return
+        super().__setattr__(name, value)
 
 
 def attach(package_name, submodules=None, submod_attrs=None):
@@ -72,26 +101,49 @@
 
     def __getattr__(name):
         if name in submodules:
-            return importlib.import_module(f"{package_name}.{name}")
+            attr = importlib.import_module(f"{package_name}.{name}")
         elif name in attr_to_modules:
             submod_path = f"{package_name}.{attr_to_modules[name]}"
             submod = importlib.import_module(submod_path)
             attr = getattr(submod, name)
-
-            # If the attribute lives in a file (module) with the same
-            # name as the attribute, ensure that the attribute and *not*
-            # the module is accessible on the package.
-            if name == attr_to_modules[name]:
-                pkg = sys.modules[package_name]
-                pkg.__dict__[name] = attr
-
-            return attr
         else:
             raise AttributeError(f"No {package_name} attribute {name}")
 
+        # Cache the resolved value on the package so that subsequent
+        # accesses bypass __getattr__; this also ensures an attribute
+        # shadows a same-named submodule.
+        pkg = sys.modules.get(package_name)
+        if pkg is not None:
+            pkg.__dict__[name] = attr
+
+        return attr
+
     def __dir__():
         return __all__.copy()
 
+    # When a function has the same name as a module the import
+    # machinery needs to load along the way to accessing it
+    # (e.g. `max_tree` from `max_tree.py`, or `x` from `x/sub.py`), a
+    # side-effect of it loading that module is overwriting the package
+    # attribute (so it points to the module, i.e. to `max_tree` or `x`
+    # the module), shadowing the function (see _ShadowGuardModule).
+    #
+    # Record affected cases and, only in those cases, swap in the
+    # guarding module type.
+    shadowed = {
+        attr for attr, mod in attr_to_modules.items() if attr == 
mod.split(".")[0]
+    }
+    if shadowed:
+        pkg = sys.modules.get(package_name)
+        # Only touch plain package modules (or our own wrapper) --- we
+        # don't want to mess with custom module classes.
+        if type(pkg) in (types.ModuleType, _ShadowGuardModule):
+            pkg.__dict__["__lazy_shadowed__"] = (
+                pkg.__dict__.get("__lazy_shadowed__", set()) | shadowed
+            )
+            if type(pkg) is types.ModuleType:
+                pkg.__class__ = _ShadowGuardModule
+
     eager_import = os.environ.get("EAGER_IMPORT", "") not in ("0", "")
     if eager_import:
         for attr in set(attr_to_modules.keys()) | submodules:
@@ -191,7 +243,11 @@
         if have_module and require is None:
             return module
 
+        import importlib.util
+
         if not suppress_warning and "." in fullname:
+            import warnings
+
             msg = (
                 "subpackages can technically be lazily loaded, but it causes 
the "
                 "package to be eagerly loaded even if it is already lazily 
loaded. "
@@ -276,31 +332,6 @@
     )
 
 
-class _StubVisitor(ast.NodeVisitor):
-    """AST visitor to parse a stub file for submodules and submod_attrs."""
-
-    def __init__(self):
-        self._submodules = set()
-        self._submod_attrs = {}
-
-    def visit_ImportFrom(self, node: ast.ImportFrom):
-        if node.level != 1:
-            raise ValueError(
-                "Only within-module imports are supported (`from .* import`)"
-            )
-        if node.module:
-            attrs: list = self._submod_attrs.setdefault(node.module, [])
-            aliases = [alias.name for alias in node.names]
-            if "*" in aliases:
-                raise ValueError(
-                    "lazy stub loader does not support star import "
-                    f"`from {node.module} import *`"
-                )
-            attrs.extend(aliases)
-        else:
-            self._submodules.update(alias.name for alias in node.names)
-
-
 def attach_stub(package_name: str, filename: str):
     """Attach lazily loaded submodules, functions from a type stub.
 
@@ -327,6 +358,32 @@
         If a stub file is not found for `filename`, or if the stubfile is 
formmated
         incorrectly (e.g. if it contains an relative import from outside of 
the module)
     """
+    import ast
+
+    class _StubVisitor(ast.NodeVisitor):
+        """AST visitor to parse a stub file for submodules and submod_attrs."""
+
+        def __init__(self):
+            self._submodules = set()
+            self._submod_attrs = {}
+
+        def visit_ImportFrom(self, node: ast.ImportFrom):
+            if node.level != 1:
+                raise ValueError(
+                    "Only within-module imports are supported (`from .* 
import`)"
+                )
+            if node.module:
+                attrs: list = self._submod_attrs.setdefault(node.module, [])
+                aliases = [alias.name for alias in node.names]
+                if "*" in aliases:
+                    raise ValueError(
+                        "lazy stub loader does not support star import "
+                        f"`from {node.module} import *`"
+                    )
+                attrs.extend(aliases)
+            else:
+                self._submodules.update(alias.name for alias in node.names)
+
     stubfile = (
         filename if filename.endswith("i") else 
f"{os.path.splitext(filename)[0]}.pyi"
     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/src/lazy_loader.egg-info/PKG-INFO 
new/lazy_loader-0.6/src/lazy_loader.egg-info/PKG-INFO
--- old/lazy_loader-0.5/src/lazy_loader.egg-info/PKG-INFO       2026-03-06 
16:44:41.000000000 +0100
+++ new/lazy_loader-0.6/src/lazy_loader.egg-info/PKG-INFO       2026-09-21 
23:31:17.479494600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: lazy-loader
-Version: 0.5
+Version: 0.6
 Summary: Makes it easy to load subpackages and functions on demand.
 Author: Scientific Python Developers
 License-Expression: BSD-3-Clause
@@ -14,6 +14,7 @@
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
 Requires-Python: >=3.9
 Description-Content-Type: text/markdown
 License-File: LICENSE.md
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/src/lazy_loader.egg-info/SOURCES.txt 
new/lazy_loader-0.6/src/lazy_loader.egg-info/SOURCES.txt
--- old/lazy_loader-0.5/src/lazy_loader.egg-info/SOURCES.txt    2026-03-06 
16:44:41.000000000 +0100
+++ new/lazy_loader-0.6/src/lazy_loader.egg-info/SOURCES.txt    2026-09-21 
23:31:17.487139700 +0200
@@ -15,4 +15,7 @@
 tests/test_lazy_loader.py
 tests/fake_pkg/__init__.py
 tests/fake_pkg/__init__.pyi
-tests/fake_pkg/some_func.py
\ No newline at end of file
+tests/fake_pkg/some_func.py
+tests/fake_pkg_submodule/__init__.py
+tests/fake_pkg_submodule/x/__init__.py
+tests/fake_pkg_submodule/x/sub.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/tests/fake_pkg_submodule/__init__.py 
new/lazy_loader-0.6/tests/fake_pkg_submodule/__init__.py
--- old/lazy_loader-0.5/tests/fake_pkg_submodule/__init__.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/lazy_loader-0.6/tests/fake_pkg_submodule/__init__.py    2026-09-21 
23:31:11.116575000 +0200
@@ -0,0 +1,3 @@
+import lazy_loader as lazy
+
+__getattr__, __dir__, __all__ = lazy.attach(__name__, submod_attrs={"x.sub": 
["x"]})
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/tests/fake_pkg_submodule/x/sub.py 
new/lazy_loader-0.6/tests/fake_pkg_submodule/x/sub.py
--- old/lazy_loader-0.5/tests/fake_pkg_submodule/x/sub.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/lazy_loader-0.6/tests/fake_pkg_submodule/x/sub.py       2026-09-21 
23:31:11.116744800 +0200
@@ -0,0 +1,2 @@
+def x():
+    """Function with the same name as the package it lives under."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lazy_loader-0.5/tests/test_lazy_loader.py 
new/lazy_loader-0.6/tests/test_lazy_loader.py
--- old/lazy_loader-0.5/tests/test_lazy_loader.py       2026-03-06 
16:44:33.000000000 +0100
+++ new/lazy_loader-0.6/tests/test_lazy_loader.py       2026-09-21 
23:31:11.116744800 +0200
@@ -13,9 +13,9 @@
 @pytest.fixture
 def clean_fake_pkg():
     yield
-    sys.modules.pop("tests.fake_pkg.some_func", None)
-    sys.modules.pop("tests.fake_pkg", None)
-    sys.modules.pop("tests", None)
+    for name in [*sys.modules]:
+        if name == "tests" or name.startswith("tests.fake_pkg"):
+            del sys.modules[name]
 
 
 @pytest.mark.parametrize("attempt", [1, 2])
@@ -178,6 +178,28 @@
         assert isinstance(some_func, types.FunctionType)
 
 
+def test_attach_caches_resolved_attrs(clean_fake_pkg):
+    from tests import fake_pkg
+
+    assert "aux_func" not in vars(fake_pkg)
+    aux_func = fake_pkg.aux_func
+    # The resolved attribute is cached on the package, so later accesses
+    # do not go through __getattr__ again
+    assert vars(fake_pkg)["aux_func"] is aux_func
+    assert fake_pkg.aux_func is aux_func
+
+
+def test_attach_subpackage_does_not_shadow_function(clean_fake_pkg):
+    # Where function `x` is defined in `x/sub.py`, i.e. a module
+    # nested inside a subpackage that shares its name with the
+    # function: importing `x.sub` causes the import machinery to set
+    # the package attribute `x` to the `x` subpackage, which would
+    # otherwise shadow the `x` function on subsequent access.
+    from tests import fake_pkg_submodule
+
+    assert isinstance(fake_pkg_submodule.x, types.FunctionType)
+
+
 FAKE_STUB = """
 from . import rank
 from ._gaussian import gaussian

Reply via email to