From 13b0cd556cd6667b7235e81112e2bcf04f2c9015 Mon Sep 17 00:00:00 2001 From: Anton <100830759+antonwolfy@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:47:23 +0200 Subject: [PATCH] Resolve an issue with cyclic import in `linalg` submodule (#2608) The PR reworks the import of `linalg` submodule to avoid a wildcard import. Also it resolves the issue reported by pylint: > Cyclic import (dpnp.linalg -> dpnp.linalg.dpnp_iface_linalg -> dpnp.linalg.dpnp_utils_linalg) (cyclic-import) and moves `LinAlgError` exception to be exposed by LAPACK pybind11 extension, because it is created there. While later the exception patched at python level to be set to "dpnp.linalg" submodule explicitly. Otherwise we have the import cycle like: > linalg/__init__.py -> dpnp_iface_linalg.py -> dpnp_utils_linalg.py -> linalg/__init__.py which might cause the import failure. --- CHANGELOG.md | 1 + dpnp/__init__.py | 4 ++ dpnp/backend/extensions/lapack/lapack_py.cpp | 7 ++-- dpnp/dpnp_iface.py | 1 - dpnp/linalg/__init__.py | 41 +++++++++++++++++++- dpnp/linalg/dpnp_iface_linalg.py | 8 +++- dpnp/linalg/dpnp_utils_linalg.py | 21 +++++----- 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa60d94bdfe9..4fe8515d1346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ This release is compatible with NumPy 2.3.3. * Fixed tests for the rounding functions to depend on minimum required numpy version [#2589](https://github.com/IntelPython/dpnp/pull/2589) * Fixed tests for the ufuncs to depend on minimum required numpy version [#2590](https://github.com/IntelPython/dpnp/pull/2590) * Added missing permission definition in `Autoupdate pre-commit` GitHub workflow [#2591](https://github.com/IntelPython/dpnp/pull/2591) +* Resolved issue with the cyclic import in `linalg` submodule [#2608](https://github.com/IntelPython/dpnp/pull/2608) ### Security diff --git a/dpnp/__init__.py b/dpnp/__init__.py index 744200ca13f6..4fbac978ec99 100644 --- a/dpnp/__init__.py +++ b/dpnp/__init__.py @@ -70,10 +70,14 @@ from .dpnp_iface_utils import * from .dpnp_iface_utils import __all__ as _ifaceutils__all__ from ._version import get_versions +from . import linalg as linalg __all__ = _iface__all__ __all__ += _ifaceutils__all__ +# add submodules +__all__ += ["linalg"] + __version__ = get_versions()["version"] del get_versions diff --git a/dpnp/backend/extensions/lapack/lapack_py.cpp b/dpnp/backend/extensions/lapack/lapack_py.cpp index 46471cc2f366..5e072bfeac84 100644 --- a/dpnp/backend/extensions/lapack/lapack_py.cpp +++ b/dpnp/backend/extensions/lapack/lapack_py.cpp @@ -83,10 +83,9 @@ PYBIND11_MODULE(_lapack_impl, m) .value("C", oneapi::mkl::transpose::C) .export_values(); // Optional, allows access like `Transpose.N` - // Register a custom LinAlgError exception in the dpnp.linalg submodule - py::module_ linalg_module = py::module_::import("dpnp.linalg"); - py::register_exception( - linalg_module, "LinAlgError", PyExc_ValueError); + // Register a LinAlgError exception in the current submodule + py::register_exception(m, "LinAlgError", + PyExc_ValueError); init_dispatch_vectors(); init_dispatch_tables(); diff --git a/dpnp/dpnp_iface.py b/dpnp/dpnp_iface.py index 4b354d547025..f7f4f470be48 100644 --- a/dpnp/dpnp_iface.py +++ b/dpnp/dpnp_iface.py @@ -52,7 +52,6 @@ from dpnp.dpnp_algo import * from dpnp.dpnp_array import dpnp_array from dpnp.fft import * -from dpnp.linalg import * from dpnp.memory import * from dpnp.random import * from dpnp.special import * diff --git a/dpnp/linalg/__init__.py b/dpnp/linalg/__init__.py index fd315e5ed434..3139f5dfb84e 100644 --- a/dpnp/linalg/__init__.py +++ b/dpnp/linalg/__init__.py @@ -34,7 +34,44 @@ """ -from dpnp.linalg.dpnp_iface_linalg import * -from dpnp.linalg.dpnp_iface_linalg import __all__ as __all__linalg +from .dpnp_iface_linalg import ( + LinAlgError, +) +from .dpnp_iface_linalg import __all__ as __all__linalg +from .dpnp_iface_linalg import ( + cholesky, + cond, + cross, + det, + diagonal, + eig, + eigh, + eigvals, + eigvalsh, + inv, + lstsq, + lu_factor, + lu_solve, + matmul, + matrix_norm, + matrix_power, + matrix_rank, + matrix_transpose, + multi_dot, + norm, + outer, + pinv, + qr, + slogdet, + solve, + svd, + svdvals, + tensordot, + tensorinv, + tensorsolve, + trace, + vecdot, + vector_norm, +) __all__ = __all__linalg diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index f73443229c49..c31a56c24957 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -38,6 +38,7 @@ # pylint: disable=invalid-name # pylint: disable=no-member +# pylint: disable=no-name-in-module from typing import NamedTuple @@ -45,6 +46,7 @@ from dpctl.tensor._numpy_helper import normalize_axis_tuple import dpnp +from dpnp.backend.extensions.lapack._lapack_impl import LinAlgError from .dpnp_utils_linalg import ( assert_2d, @@ -70,6 +72,7 @@ ) __all__ = [ + "LinAlgError", "cholesky", "cond", "cross", @@ -105,6 +108,9 @@ "vector_norm", ] +# Need to set the module explicitly, since exposed by LAPACK pybind11 extension +LinAlgError.__module__ = "dpnp.linalg" + # pylint:disable=missing-class-docstring class EigResult(NamedTuple): @@ -2330,7 +2336,7 @@ def tensorsolve(a, b, axes=None): prod = numpy.prod(old_shape) if a.size != prod**2: - raise dpnp.linalg.LinAlgError( + raise LinAlgError( "Input arrays must satisfy the requirement " "prod(a.shape[b.ndim:]) == prod(a.shape[:b.ndim])" ) diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index 44a3816cc165..ce36b39f86a7 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -50,7 +50,6 @@ import dpnp import dpnp.backend.extensions.lapack._lapack_impl as li from dpnp.dpnp_utils import get_usm_allocations -from dpnp.linalg import LinAlgError as LinAlgError __all__ = [ "assert_2d", @@ -943,7 +942,7 @@ def _check_lapack_dev_info(dev_info, error_msg=None): if any(dev_info): error_msg = error_msg or "Singular matrix" - raise LinAlgError(error_msg) + raise li.LinAlgError(error_msg) def _common_type(*arrays): @@ -1879,7 +1878,7 @@ def assert_2d(*arrays): for a in arrays: if a.ndim != 2: - raise LinAlgError( + raise li.LinAlgError( f"{a.ndim}-dimensional array given. The input " "array must be exactly two-dimensional" ) @@ -1906,7 +1905,7 @@ def assert_stacked_2d(*arrays): for a in arrays: if a.ndim < 2: - raise LinAlgError( + raise li.LinAlgError( f"{a.ndim}-dimensional array given. The input " "array must be at least two-dimensional" ) @@ -1942,7 +1941,7 @@ def assert_stacked_square(*arrays): for a in arrays: m, n = a.shape[-2:] if m != n: - raise LinAlgError( + raise li.LinAlgError( "Last 2 dimensions of the input array must be square" ) @@ -2086,7 +2085,7 @@ def dpnp_cond(x, p=None): """Compute the condition number of a matrix.""" if _is_empty_2d(x): - raise LinAlgError("cond is not defined on empty arrays") + raise li.LinAlgError("cond is not defined on empty arrays") if p is None or p == 2 or p == -2: s = dpnp.linalg.svd(x, compute_uv=False) if p == -2: @@ -2340,7 +2339,7 @@ def dpnp_lstsq(a, b, rcond=None): """ if b.ndim > 2: - raise LinAlgError( + raise li.LinAlgError( f"{b.ndim}-dimensional array given. The input " "array must be exactly two-dimensional" ) @@ -2348,7 +2347,7 @@ def dpnp_lstsq(a, b, rcond=None): m, n = a.shape[-2:] m2 = b.shape[0] if m != m2: - raise LinAlgError("Incompatible dimensions") + raise li.LinAlgError("Incompatible dimensions") u, s, vh = dpnp_svd(a, full_matrices=False, related_arrays=[b]) @@ -2669,20 +2668,20 @@ def dpnp_multi_dot(n, arrays, out=None): """Compute dot product of two or more arrays in a single function call.""" if not arrays[0].ndim in [1, 2]: - raise LinAlgError( + raise li.LinAlgError( f"{arrays[0].ndim}-dimensional array given. " "First array must be 1-D or 2-D." ) if not arrays[-1].ndim in [1, 2]: - raise LinAlgError( + raise li.LinAlgError( f"{arrays[-1].ndim}-dimensional array given. " "Last array must be 1-D or 2-D." ) for arr in arrays[1:-1]: if arr.ndim != 2: - raise LinAlgError( + raise li.LinAlgError( f"{arr.ndim}-dimensional array given. Inner arrays must be 2-D." )