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