From e48da29ba04f38a1485d2ae6ef7a6ec47fd2bda1 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:04:13 -0400 Subject: [PATCH 01/11] ENH: PEP 562 lazy loading for top-level itk package Replace the LazyITKModule sys.modules-swap orchestration in itk/__init__.py with a native PEP 562 implementation: module-level __getattr__ and __dir__ resolve symbols on first access, gated by a single threading.RLock. itkConfig.LazyLoading is no longer consulted; lazy is the only mode. multiprocessing.RLock is deliberately avoided so the multiprocessing start method stays pickable after `import itk`. Per-submodule LazyITKModule instances and the __init___.py discovery hook are preserved unchanged so the build stays green; Phase 03 will migrate them. itk_base_global_lazy_attributes (consumed by support/template_class._LoadModules) is still populated with the full set of owners per attribute, alongside the new first-owner-only _lazy_attribute_to_module that drives the top-level __getattr__. Validated via py_compile and a mock-driver smoke harness covering PEP 366 __package__, dir-without-load, lazy-load-with-cache, first-owner precedence, AttributeError on miss, dunder short-circuit, and a 6-thread first-touch race. --- Wrapping/Generators/Python/itk/__init__.py | 175 ++++++++++++--------- 1 file changed, 101 insertions(+), 74 deletions(-) diff --git a/Wrapping/Generators/Python/itk/__init__.py b/Wrapping/Generators/Python/itk/__init__.py index 0476d3d2fee..5eb721d37b9 100644 --- a/Wrapping/Generators/Python/itk/__init__.py +++ b/Wrapping/Generators/Python/itk/__init__.py @@ -25,6 +25,9 @@ # LazyLoading and other values may be different in the two contexts. from itkConfig import ITK_GLOBAL_VERSION_STRING as __version__ +# Tests/lazy.py asserts itk.__package__ == "itk"; guard the invariant. +assert __package__ == "itk" + from itk.support.extras import * from itk.support.init_helpers import * from itk.support.types import ( @@ -47,96 +50,118 @@ OT, ) +import threading as _threading + +import itkConfig as _itkConfig +from itk.support import base as _base + +# Recursive lock so transitive dependency loads from the same thread +# don't self-deadlock. Deliberately threading.RLock — a +# multiprocessing.RLock would pin the multiprocessing start method +# and break spawn/forkserver after `import itk`. +_lazy_load_lock = _threading.RLock() +_lazy_attribute_to_module: dict[str, str] = {} +_MISSING = object() + + +def __getattr__(name: str): + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + try: + module_name = _lazy_attribute_to_module[name] + except KeyError: + raise AttributeError( + f"module 'itk' has no attribute {name!r}" + ) from None + g = globals() + with _lazy_load_lock: + cached = g.get(name, _MISSING) + if cached is not _MISSING: + return cached + namespace: dict = {} + _base.itk_load_swig_module(module_name, namespace) + g.update(namespace) + if _itkConfig.DefaultFactoryLoading: + _base.load_module_needed_factories(module_name) + try: + return g[name] + except KeyError: + raise AttributeError( + f"module 'itk' has no attribute {name!r}" + ) from None + + +def __dir__(): + return sorted(set(globals().keys()) | _lazy_attribute_to_module.keys()) + def _initialize_module(): """ A function to explicitly avoid polluting the global namespace """ - from .support.base import ITKModuleInfo, ITKTemplateFeatures + import importlib + import os + import sys # Needed to avoid problem with aliasing of itk.set (itkTemplate) # inside the itk namespace. We need to explicitly specify the # use of the builtin set from builtins import set as _builtin_set - def _get_lazy_attributes(local_lazy_attributes, l_module, l_data: ITKModuleInfo): - """ - Set up lazy attribute relationships - """ - for template_feature in l_data._template_feature_tuples: + from .support import lazy as _lazy + + # Build the flat first-owner-wins map driving module-level + # __getattr__, and populate base.itk_base_global_lazy_attributes + # with the full set of owners per attribute (consumed by + # support/template_class.py:_LoadModules). + # + # Precedence rules (mirroring the legacy _get_lazy_attributes + + # __belong_lazy_attributes pipeline): + # - template_feature with _class_in_module=True: that module + # wins, even over a previously recorded owner. + # - template_feature with _class_in_module=False: first walk + # wins; later writers do not override. + # - snake_case_functions: first walk wins; never override. + for module, data in _base.itk_base_global_module_data.items(): + for template_feature in data._template_feature_tuples: + attr = template_feature._py_class_name + _base.itk_base_global_lazy_attributes.setdefault( + attr, _builtin_set() + ).add(module) + if template_feature._class_in_module: + _lazy_attribute_to_module[attr] = module + else: + _lazy_attribute_to_module.setdefault(attr, module) + for function in data._snake_case_functions: + _base.itk_base_global_lazy_attributes.setdefault( + function, _builtin_set() + ).add(module) + _lazy_attribute_to_module.setdefault(function, module) + + # Per-submodule LazyITKModule instances (itk.ITKCommon, ...). + # Phase 03 will replace these with plain types.ModuleType + PEP 562 + # closures; for now they remain so the build stays green and the + # pickle / cloudpickle paths exercised by Tests/lazy.py keep working. + this_module = sys.modules[__name__] + for module, data in _base.itk_base_global_module_data.items(): + attributes: dict[str, list[str]] = {} + for template_feature in data._template_feature_tuples: if template_feature._class_in_module: - # insert in front front if in library - local_lazy_attributes.setdefault( + attributes.setdefault( template_feature._py_class_name, [] - ).insert(0, l_module) + ).insert(0, module) else: - # append to end - local_lazy_attributes.setdefault( + attributes.setdefault( template_feature._py_class_name, [] - ).append(l_module) - - for function in l_data._snake_case_functions: - # snake case always appended to end - local_lazy_attributes.setdefault(function, []).append(l_module) - - # Remove duplicates in attributes, preserving only the first - def _dedup(seq): + ).append(module) + for function in data._snake_case_functions: + attributes.setdefault(function, []).append(module) + for k, v in attributes.items(): seen = _builtin_set() - seen_add = seen.add - return [x for x in seq if not (x in seen or seen_add(x))] - - for k, v in local_lazy_attributes.items(): - local_lazy_attributes[k] = _dedup(v) - - from .support import base as _base - from .support import lazy as _lazy - from itkConfig import LazyLoading as _LazyLoading - import sys - import os - import importlib + attributes[k] = [m for m in v if not (m in seen or seen.add(m))] - if _LazyLoading: - # If we are loading lazily (on-demand), make a dict mapping the available - # classes/functions/etc. (read from the configuration modules) to the - # modules they are declared in. Then pass that dict to a LazyITKModule - # instance and (later) do some surgery on sys.modules so that the 'itk' - # module becomes that new instance instead of what is executed from this - # file. - lazy_attributes = {} - for module, data in _base.itk_base_global_module_data.items(): - _get_lazy_attributes(lazy_attributes, module, data) - - if isinstance(sys.modules[__name__], _lazy.LazyITKModule): - # Handle reload case where we've already done this once. - # If we made a new module every time, multiple reload()s would fail - # because the identity of sys.modules['itk'] would always be changing. - sys.modules[__name__].__init__(__name__, lazy_attributes) - del lazy_attributes - else: - # Create a new LazyITKModule - lzy_module = _lazy.LazyITKModule(__name__, lazy_attributes) - - # Pre-existing attributes need to be propagated too! - # except for the lazy overridden elements - exclusion_copy_list = ["__name__", "__loader__", "__builtins__"] - for k, v in sys.modules[__name__].__dict__.items(): - if k not in exclusion_copy_list: - setattr(lzy_module, k, v) - - # Now override the default sys.modules[__name__] (__name__ == 'itk' ) - sys.modules[__name__] = lzy_module - else: - # We're not lazy-loading. Just load the modules in the order specified in - # the known_modules list for consistency. - for module in _base.itk_base_global_module_data.keys(): - _base.itk_load_swig_module(module, sys.modules[__name__].__dict__) - - # Populate itk.ITKModuleName - for module, data in _base.itk_base_global_module_data.items(): - attributes = {} - _get_lazy_attributes(attributes, module, data) itk_module = _lazy.LazyITKModule(module, attributes) - setattr(sys.modules[__name__], module, itk_module) + setattr(this_module, module, itk_module) # Check if the module installed its own init file and load it. # ITK Modules __init__.py must be renamed to __init_{module_name}__.py before packaging @@ -157,6 +182,8 @@ def _dedup(seq): spec.loader.exec_module(loaded_module) -# After 'lifting' external symbols into this itk namespace, -# Now do the initialization, and conversion to LazyLoading if necessary +# Build the lazy-attribute map and materialise per-submodule +# LazyITKModule instances. This must run before any consumer attribute +# access; module-level __getattr__ depends on _lazy_attribute_to_module +# being populated. _initialize_module() From 730c94b7e75e09058b97310756e4cffec546fe66 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:14:48 -0400 Subject: [PATCH 02/11] BUG: Use set literal in itk.__dir__ to avoid itkTemplate shadow The new PEP 562 module-level __dir__ called the unqualified `set` name, which Python resolves through `itk.__dict__`. Once any SWIG submodule is loaded (e.g. ITKCommon on first `itk.Image` access), `itk.set` is populated as an `itkTemplate` binding `std::set` and shadows the builtin. Subsequent calls to `dir(itk)` then raised `TemplateTypeError: itk.set is not wrapped for input type None`. This is the same alias hazard that the existing _initialize_module already guards against by importing `_builtin_set` from `builtins`. Switch __dir__ to a set-literal form `{*globals().keys(), ...}` which constructs through the C-level set type with no name lookup, so it is unaffected by names introduced into module globals at lazy-load time. --- Wrapping/Generators/Python/itk/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Wrapping/Generators/Python/itk/__init__.py b/Wrapping/Generators/Python/itk/__init__.py index 5eb721d37b9..ba9efc981f9 100644 --- a/Wrapping/Generators/Python/itk/__init__.py +++ b/Wrapping/Generators/Python/itk/__init__.py @@ -92,7 +92,10 @@ def __getattr__(name: str): def __dir__(): - return sorted(set(globals().keys()) | _lazy_attribute_to_module.keys()) + # Set literal — avoids resolving the bare name `set`, which is + # shadowed in module globals by `itk.set` (itkTemplate std::set) + # once any SWIG module is loaded. + return sorted({*globals().keys(), *_lazy_attribute_to_module.keys()}) def _initialize_module(): From 99fdc3115dc6f439db28c08e2e947f777203ec32 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:30:54 -0400 Subject: [PATCH 03/11] ENH: Add _make_itk_lazy_submodule PEP 562 helper Introduce itk.support._lazy_submodule with a builder that returns a plain types.ModuleType for itk. wired with module-level __getattr__ / __dir__ closures, registered in sys.modules['itk.'], and carrying a one-line __reduce_ex__ shim for pickle / cloudpickle by-name round-trip. This is the replacement target for the legacy LazyITKModule subclass; the next commit swaps the construction site in itk/__init__.py. --- Wrapping/Generators/Python/CMakeLists.txt | 1 + .../Python/itk/support/_lazy_submodule.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 Wrapping/Generators/Python/itk/support/_lazy_submodule.py diff --git a/Wrapping/Generators/Python/CMakeLists.txt b/Wrapping/Generators/Python/CMakeLists.txt index 302e0f31d9b..8b76bb26a3c 100644 --- a/Wrapping/Generators/Python/CMakeLists.txt +++ b/Wrapping/Generators/Python/CMakeLists.txt @@ -186,6 +186,7 @@ if(NOT EXTERNAL_WRAP_ITK_PROJECT) support/extras support/xarray support/lazy + support/_lazy_submodule support/helpers support/init_helpers support/build_options diff --git a/Wrapping/Generators/Python/itk/support/_lazy_submodule.py b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py new file mode 100644 index 00000000000..bdd26bc99b1 --- /dev/null +++ b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py @@ -0,0 +1,120 @@ +# ========================================================================== +# +# Copyright NumFOCUS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0.txt +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================================================================== + +"""Builder for per-submodule lazy namespaces (``itk.ITKCommon``, ...). + +Replaces the legacy ``LazyITKModule(types.ModuleType)`` subclass with a +plain ``types.ModuleType`` instance wired with PEP 562 ``__getattr__`` / +``__dir__`` callables. The submodule is registered in +``sys.modules['itk.']`` so ``cloudpickle`` (used by the Dask +worker round-trip exercised in ``Tests/lazy.py``) can re-import it by +its dotted name. +""" + +import importlib +import sys +import types + +import itkConfig as _itkConfig +from itk.support import base as _base + + +_MISSING = object() + + +def _make_itk_lazy_submodule( + module_name: str, + lazy_attributes: dict[str, list[str]], + lazy_load_lock, +) -> types.ModuleType: + """Build the lazy ``itk.`` namespace. + + Parameters + ---------- + module_name + Bare submodule name (e.g. ``"ITKCommon"``); the resulting + module is registered as ``itk.``. + lazy_attributes + Mapping from attribute name to the list of owning submodule + names. The first element of each list wins (matching the + legacy ``__belong_lazy_attributes`` first-owner-wins rule). + lazy_load_lock + The shared recursive lock from ``itk/__init__.py`` used to + serialise SWIG module loads across both top-level and + per-submodule lazy paths. + """ + m = types.ModuleType(f"itk.{module_name}") + m.__package__ = "itk" # PEP 366 + m.__loader__ = None + + belong: dict[str, str] = {k: v[0] for k, v in lazy_attributes.items() if v} + loaded_modules: set[str] = set() + + def __getattr__(name: str): + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + try: + target = belong[name] + except KeyError: + raise AttributeError( + f"module {m.__name__!r} has no attribute {name!r}" + ) from None + d = m.__dict__ + with lazy_load_lock: + cached = d.get(name, _MISSING) + if cached is not _MISSING: + return cached + namespace: dict = {} + _base.itk_load_swig_module(target, namespace) + d.update(namespace) + loaded_modules.add(target) + if _itkConfig.DefaultFactoryLoading: + _base.load_module_needed_factories(target) + try: + return d[name] + except KeyError: + raise AttributeError( + f"module {m.__name__!r} has no attribute {name!r}" + ) from None + + def __dir__(): + # Set literal — avoids resolving the bare name `set`, which + # gets shadowed in this submodule's __dict__ by `itk.set` + # (itkTemplate std::set) once any SWIG module loads into it. + return sorted({*m.__dict__.keys(), *belong.keys()}) + + m.__getattr__ = __getattr__ + m.__dir__ = __dir__ + + # Pickle shim: a bare ``types.ModuleType`` has no reducer, so + # ``pickle.dumps`` raises ``TypeError: cannot pickle 'module' + # object`` on CPython 3.12 even when the instance is registered in + # ``sys.modules``. A per-instance ``__reduce_ex__`` makes pickle + # round-trip via ``importlib.import_module(name)``, which then + # resolves through ``sys.modules`` to the same instance. Set as + # an instance attribute (not on ``types.ModuleType``) so unrelated + # modules in the process are unaffected. + submodule_dotted_name = m.__name__ + + def __reduce_ex__(_protocol: int): + return (importlib.import_module, (submodule_dotted_name,)) + + m.__reduce_ex__ = __reduce_ex__ + + sys.modules[submodule_dotted_name] = m + return m From 0a20b3c34667236c0ca30f2ed237ca318dca355f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:34:31 -0400 Subject: [PATCH 04/11] ENH: Use _make_itk_lazy_submodule for per-submodule lazy namespaces Replace the LazyITKModule(types.ModuleType) construction in _initialize_module() with the PEP 562 helper added in the previous commit. Each itk. namespace is now a plain types.ModuleType with __getattr__/__dir__ closures and a sys.modules registration, so cloudpickle round-trips by dotted name (Tests/lazy.py). The shared _lazy_load_lock is passed into the helper so top-level and per-submodule lazy loads continue to serialise on a single RLock. After this change __init__.py no longer references LazyITKModule; the class itself remains in support/lazy.py for now and will be removed in a follow-up commit. --- Wrapping/Generators/Python/itk/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Wrapping/Generators/Python/itk/__init__.py b/Wrapping/Generators/Python/itk/__init__.py index ba9efc981f9..6131d744bdc 100644 --- a/Wrapping/Generators/Python/itk/__init__.py +++ b/Wrapping/Generators/Python/itk/__init__.py @@ -111,7 +111,7 @@ def _initialize_module(): # use of the builtin set from builtins import set as _builtin_set - from .support import lazy as _lazy + from .support._lazy_submodule import _make_itk_lazy_submodule # Build the flat first-owner-wins map driving module-level # __getattr__, and populate base.itk_base_global_lazy_attributes @@ -141,10 +141,11 @@ def _initialize_module(): ).add(module) _lazy_attribute_to_module.setdefault(function, module) - # Per-submodule LazyITKModule instances (itk.ITKCommon, ...). - # Phase 03 will replace these with plain types.ModuleType + PEP 562 - # closures; for now they remain so the build stays green and the - # pickle / cloudpickle paths exercised by Tests/lazy.py keep working. + # Per-submodule lazy namespaces (itk.ITKCommon, ...). Plain + # types.ModuleType instances wired with PEP 562 __getattr__ / + # __dir__ closures by _make_itk_lazy_submodule, registered in + # sys.modules['itk.'] so cloudpickle round-trips by + # dotted name (Tests/lazy.py). this_module = sys.modules[__name__] for module, data in _base.itk_base_global_module_data.items(): attributes: dict[str, list[str]] = {} @@ -163,7 +164,7 @@ def _initialize_module(): seen = _builtin_set() attributes[k] = [m for m in v if not (m in seen or seen.add(m))] - itk_module = _lazy.LazyITKModule(module, attributes) + itk_module = _make_itk_lazy_submodule(module, attributes, _lazy_load_lock) setattr(this_module, module, itk_module) # Check if the module installed its own init file and load it. @@ -185,8 +186,8 @@ def _initialize_module(): spec.loader.exec_module(loaded_module) -# Build the lazy-attribute map and materialise per-submodule -# LazyITKModule instances. This must run before any consumer attribute -# access; module-level __getattr__ depends on _lazy_attribute_to_module -# being populated. +# Build the lazy-attribute map and materialise per-submodule lazy +# namespaces. This must run before any consumer attribute access; +# module-level __getattr__ depends on _lazy_attribute_to_module being +# populated. _initialize_module() From e03b679aac79ad59dfff60962c2275013381eb76 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:51:43 -0400 Subject: [PATCH 05/11] ENH: Remove deprecated itkConfig.LazyLoading flag The PEP 562 mechanism is now the only lazy-loading path so the legacy itkConfig.LazyLoading flag and its ITK_PYTHON_LAZYLOADING environment variable are no-ops. Drop the flag definition, the docstring paragraph documenting it, and the three test-side overrides that toggled it. Specifically: * Wrapping/Generators/Python/itkConfig.template.in.py: drop the LazyLoading docstring paragraph and the LazyLoading bool assignment driven by ITK_PYTHON_LAZYLOADING. * Wrapping/Generators/Python/itk/__init__.py: refresh a leading comment that used LazyLoading as the canonical example of a mutable itkConfig flag; use DefaultFactoryLoading instead so the example still names a real attribute. * Wrapping/Generators/Python/Tests/lazy.py: remove the itkConfig.LazyLoading=True override at the top of the test and refresh the now-stale "PEP 366 compliance of LazyITKModule" comment to reference the per-submodule namespaces that replaced the class. * Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py: remove the itkConfig.LazyLoading=True override and the now-unused import itkConfig. The descriptive header comments referencing the *concept* of lazy loading remain intact. * Modules/Filtering/ImageIntensity/wrapping/test/itkImageFilterNumPyInputsTest.py: remove the itkConfig.LazyLoading=False override and the associated import itkConfig (otherwise the test would raise AttributeError once the flag is gone). --- .../wrapping/test/itkImageFilterNumPyInputsTest.py | 3 --- Wrapping/Generators/Python/Tests/lazy.py | 6 +----- .../Generators/Python/Tests/multiprocess_lazy_loading.py | 7 ------- Wrapping/Generators/Python/itk/__init__.py | 3 ++- Wrapping/Generators/Python/itkConfig.template.in.py | 5 +---- 5 files changed, 4 insertions(+), 20 deletions(-) diff --git a/Modules/Filtering/ImageIntensity/wrapping/test/itkImageFilterNumPyInputsTest.py b/Modules/Filtering/ImageIntensity/wrapping/test/itkImageFilterNumPyInputsTest.py index a1132a67a47..a8c6d9b020d 100644 --- a/Modules/Filtering/ImageIntensity/wrapping/test/itkImageFilterNumPyInputsTest.py +++ b/Modules/Filtering/ImageIntensity/wrapping/test/itkImageFilterNumPyInputsTest.py @@ -15,9 +15,6 @@ # limitations under the License. # # ========================================================================== -import itkConfig - -itkConfig.LazyLoading = False import itk import numpy as np diff --git a/Wrapping/Generators/Python/Tests/lazy.py b/Wrapping/Generators/Python/Tests/lazy.py index 0f0dd9c815b..1b5106d21f2 100644 --- a/Wrapping/Generators/Python/Tests/lazy.py +++ b/Wrapping/Generators/Python/Tests/lazy.py @@ -15,13 +15,9 @@ # limitations under the License. # # ========================================================================== -import itkConfig - -# Override environmental variable default to force LazyLoading -itkConfig.LazyLoading = True import itk -# Test PEP 366 compliance of LazyITKModule +# Test PEP 366 compliance of itk and per-submodule namespaces assert itk.__package__ == "itk" from itk import ITKCommon diff --git a/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py b/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py index 6246d9dc5e2..6e832456706 100755 --- a/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py +++ b/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py @@ -23,13 +23,6 @@ # to wait until this first thread releases the RLock. -# NOTE: This test requires itkConfig.LazyLoading=True -# Explicitly set to override potential environmental -# variable settings. -import itkConfig - -itkConfig.LazyLoading = True - from multiprocessing.pool import ThreadPool from multiprocessing import cpu_count diff --git a/Wrapping/Generators/Python/itk/__init__.py b/Wrapping/Generators/Python/itk/__init__.py index 6131d744bdc..2a821766458 100644 --- a/Wrapping/Generators/Python/itk/__init__.py +++ b/Wrapping/Generators/Python/itk/__init__.py @@ -22,7 +22,8 @@ # in order to maintain the singular value of the module values. # `import .conf.itkConfig` is a different context than # `import itkConfig`, even if they are the same file. The -# LazyLoading and other values may be different in the two contexts. +# DefaultFactoryLoading and other values may be different in the +# two contexts. from itkConfig import ITK_GLOBAL_VERSION_STRING as __version__ # Tests/lazy.py asserts itk.__package__ == "itk"; guard the invariant. diff --git a/Wrapping/Generators/Python/itkConfig.template.in.py b/Wrapping/Generators/Python/itkConfig.template.in.py index 1e68019fa54..8b235c8095e 100644 --- a/Wrapping/Generators/Python/itkConfig.template.in.py +++ b/Wrapping/Generators/Python/itkConfig.template.in.py @@ -30,9 +30,7 @@ be called when each new library is imported in the import process. ImportCallback must be a function that takes two parameters: the name of the library being imported, and a float (between 0 and 1) reflecting the - fraction of the import that is completed. - LazyLoading: Only load an itk library when needed. Before the library is - loaded, the namespace will be inhabited with dummy objects.""" + fraction of the import that is completed.""" # User options @@ -79,7 +77,6 @@ def _strtobool(val: str) -> bool: DefaultFactoryLoading: bool = _get_environment_boolean( "ITK_PYTHON_DEFAULTFACTORYLOADING", "True" ) -LazyLoading: bool = _get_environment_boolean("ITK_PYTHON_LAZYLOADING", "True") NotInPlace: bool = _get_environment_boolean("ITK_PYTHON_NOTINPLACE", "False") del _get_environment_boolean From 88139876f4011f76d4fb34e62667dad8e10194bb Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:53:38 -0400 Subject: [PATCH 06/11] ENH: Remove obsolete PythonNoLazyModule eager-mode test Tests/nolazy.py was the eager-mode counterpart to Tests/lazy.py and forced itkConfig.LazyLoading=False to exercise the non-lazy import path. With the LazyLoading flag removed in the previous commit the PEP 562 lazy mechanism is the only import path, so the eager test is no longer meaningful and would raise AttributeError on import. Delete the file and its CMake registration. * Wrapping/Generators/Python/Tests/nolazy.py: deleted. * Wrapping/Generators/Python/Tests/CMakeLists.txt: drop the itk_python_add_test(NAME PythonNoLazyModule ...) block. --- .../Generators/Python/Tests/CMakeLists.txt | 5 -- Wrapping/Generators/Python/Tests/nolazy.py | 46 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 Wrapping/Generators/Python/Tests/nolazy.py diff --git a/Wrapping/Generators/Python/Tests/CMakeLists.txt b/Wrapping/Generators/Python/Tests/CMakeLists.txt index 58057bb19ef..958f5b2e426 100644 --- a/Wrapping/Generators/Python/Tests/CMakeLists.txt +++ b/Wrapping/Generators/Python/Tests/CMakeLists.txt @@ -110,11 +110,6 @@ itk_python_add_test( COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/lazy.py ) -itk_python_add_test( - NAME PythonNoLazyModule - COMMAND - ${CMAKE_CURRENT_SOURCE_DIR}/nolazy.py -) itk_python_add_test( NAME PythonNoDefaultFactories COMMAND diff --git a/Wrapping/Generators/Python/Tests/nolazy.py b/Wrapping/Generators/Python/Tests/nolazy.py deleted file mode 100644 index b052333ff4a..00000000000 --- a/Wrapping/Generators/Python/Tests/nolazy.py +++ /dev/null @@ -1,46 +0,0 @@ -# ========================================================================== -# -# Copyright NumFOCUS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0.txt -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ========================================================================== -import itkConfig - -# Override environmental variable default to force non-LazyLoading -itkConfig.LazyLoading = False -import itk - -# Test PEP 366 compliance of LazyITKModule -assert itk.__package__ == "itk" -from itk import ITKCommon - -assert ITKCommon.__package__ == "itk" - -# Test pickling used bash Dask -_has_cloudpickle: bool = False -try: - import cloudpickle - - _has_cloudpickle = True -except ImportError: - _has_cloudpickle = False - pass - -if _has_cloudpickle: - print("Using cloudpickle to test dumping and loading itk.") - itkpickled = cloudpickle.dumps(itk) - cloudpickle.loads(itkpickled) -else: - print("cloudpickle module not available for testing.") - pass From 15c2c938af0d57c05a6d659d935604ed9629f5e8 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 17:56:43 -0400 Subject: [PATCH 07/11] ENH: Delete obsolete support/lazy.py LazyITKModule machinery Remove the legacy custom-subclass-of-types.ModuleType implementation now that the itk package and its per-submodule namespaces resolve attributes via module-level __getattr__ / __dir__. - Delete Wrapping/Generators/Python/itk/support/lazy.py (LazyITKModule, ITKLazyLoadLock, _lazy_itk_module_reconstructor, not_loaded sentinel). - Drop support/lazy from ITK_PYTHON_SUPPORT_MODULES in Wrapping/Generators/Python/CMakeLists.txt so the wheel no longer installs the deleted file. A repo-wide grep confirms no live import of from itk.support import lazy, from itk.support.lazy, support.lazy, _lazy., or LazyITKModule remains. --- Wrapping/Generators/Python/CMakeLists.txt | 1 - .../Generators/Python/itk/support/lazy.py | 181 ------------------ 2 files changed, 182 deletions(-) delete mode 100644 Wrapping/Generators/Python/itk/support/lazy.py diff --git a/Wrapping/Generators/Python/CMakeLists.txt b/Wrapping/Generators/Python/CMakeLists.txt index 8b76bb26a3c..1fc8bfc180f 100644 --- a/Wrapping/Generators/Python/CMakeLists.txt +++ b/Wrapping/Generators/Python/CMakeLists.txt @@ -185,7 +185,6 @@ if(NOT EXTERNAL_WRAP_ITK_PROJECT) support/types support/extras support/xarray - support/lazy support/_lazy_submodule support/helpers support/init_helpers diff --git a/Wrapping/Generators/Python/itk/support/lazy.py b/Wrapping/Generators/Python/itk/support/lazy.py deleted file mode 100644 index a8fbf21d94f..00000000000 --- a/Wrapping/Generators/Python/itk/support/lazy.py +++ /dev/null @@ -1,181 +0,0 @@ -# ========================================================================== -# -# Copyright NumFOCUS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0.txt -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ========================================================================== -import types -from itk.support import base -from itkConfig import DefaultFactoryLoading as _DefaultFactoryLoading - -# Needed to avoid problem with aliasing of itk.set (itkTemplate) -# inside the itk namespace. We need to explicitly specify the -# use of the builtin set -from builtins import set as _builtin_set - -not_loaded: str = "not loaded" - - -def _lazy_itk_module_reconstructor(module_name, state): - # Similar to copyreg._reconstructor - lazy_module = types.ModuleType.__new__(LazyITKModule, state) - types.ModuleType.__init__(lazy_module, module_name) - - return lazy_module - - -def _ThreadingRLock(*args, **kwargs): - """threading RLock""" - try: - from threading import RLock - - return RLock(*args, **kwargs) - except (ImportError, OSError): - pass - - -class ITKLazyLoadLock: - """Need to use a recursive lock for thread ownership - within the given thread you can acquire a RLock as often as you like. - Other threads need to wait until this thread releases the resource again. - - A single lock is needed for all lazy loading. This lock blocks - across all threads until this thread has completed all its imports - and dependencies. The complex inter-relationship, and the recursive - nature of imports, makes a more fine-grained locking very difficult - to implement robustly.""" - - # global thread lock so no setup required for multithreading. - # NB: Do not create multiprocessing lock as it sets the multiprocessing - # context, disallowing `spawn()`/`forkserver()` - th_lock = _ThreadingRLock() - - def __init__(self): - cls = type(self) - root_lock = cls.th_lock - if root_lock is not None: - root_lock.acquire() - cls.create_mp_lock() - self.locks = [lk for lk in [cls.mp_lock, cls.th_lock] if lk is not None] - if root_lock is not None: - root_lock.release() - - def acquire(self, *a, **k): - for lock in self.locks: - lock.acquire(*a, **k) - - def release(self): - for lock in self.locks[::-1]: # Release in inverse order of acquisition - lock.release() - - def __enter__(self): - self.acquire() - - def __exit__(self, *exc): - self.release() - - @classmethod - def create_mp_lock(cls): - if not hasattr(cls, "mp_lock"): - try: - from multiprocessing import RLock - - cls.mp_lock = RLock() - except (ImportError, OSError): - cls.mp_lock = None - - -class LazyITKModule(types.ModuleType): - """Subclass of ModuleType that implements a custom __getattribute__ method - to allow lazy-loading of attributes from ITK sub-modules.""" - - def __init__(self, name, lazy_attributes): - types.ModuleType.__init__(self, name) - for k, v in lazy_attributes.items(): - base.itk_base_global_lazy_attributes.setdefault(k, _builtin_set()).update(v) - self.__belong_lazy_attributes = { - k: v[0] for k, v in lazy_attributes.items() if len(v) > 0 - } - for k in lazy_attributes: - setattr(self, k, not_loaded) # use default known value - # For PEP 366 - setattr(self, "__package__", "itk") - setattr(self, "itk_base_global_lazy_attributes", lazy_attributes) - setattr(self, "loaded_lazy_modules", _builtin_set()) - - @classmethod - def set_lock(cls, lock): - """Set the global lock.""" - cls._lock = lock - - @classmethod - def get_lock(cls): - """Get the global lock. Construct it if it does not exist.""" - if not hasattr(cls, "_lock"): - cls._lock = ITKLazyLoadLock() - return cls._lock - - def __getattribute__(self, attr): - value = types.ModuleType.__getattribute__(self, attr) - if value is not_loaded: - with type(self).get_lock(): # All but one thread will block here. - if value is not_loaded: - # Only the first thread needs to run this code, all other blocked threads skip - module = self.__belong_lazy_attributes[attr] - namespace = {} - base.itk_load_swig_module(module, namespace) - self.loaded_lazy_modules.add(module) - for k, v in namespace.items(): - setattr(self, k, v) - value = namespace[attr] - if _DefaultFactoryLoading: - base.load_module_needed_factories(module) - else: # one of the other threads that had been blocking - # waiting for first thread to complete. Now the - # attribute is REQUIRED to be available - # can just fall through now. - value = types.ModuleType.__getattribute__(self, attr) - assert value is not not_loaded - return value - - # For pickle support - def __reduce_ex__(self, proto): - state = self.__getstate__() - return _lazy_itk_module_reconstructor, (self.__name__, state), state - - # For pickle support - def __getstate__(self): - state = self.__dict__.copy() - lazy_modules = list() - for key in self.itk_base_global_lazy_attributes: - if isinstance(state[key], LazyITKModule): - lazy_modules.append((key, state[key].itk_base_global_lazy_attributes)) - state[key] = not_loaded - state["lazy_modules"] = lazy_modules - - return state - - # For pickle support - def __setstate__(self, state): - self.__dict__.update(state) - for module_name, lazy_attributes in state["lazy_modules"]: - self.__dict__.update( - {module_name: LazyITKModule(module_name, lazy_attributes)} - ) - for module in state["loaded_lazy_modules"]: - namespace = {} - base.itk_load_swig_module(module, namespace) - for k, v in namespace.items(): - setattr(self, k, v) - base.load_module_needed_factories(module) From 7e86d80c837941cda4c0067d53c919ba1977d6df Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 30 Apr 2026 18:23:23 -0400 Subject: [PATCH 08/11] ENH: Cover PEP 562 __dir__, factory hook, stdlib pickle in lazy test Extend Tests/lazy.py beyond PEP 366 + cloudpickle to assert the three remaining contracts the PEP 562 lazy mechanism must preserve: - PEP 562 __dir__: "Image" appears in dir(itk) and dir(ITKCommon) but is absent from vars(itk) / vars(ITKCommon) until first access -- the lazy attribute map is enumerable without forcing a SWIG load. - Factory hook: under DefaultFactoryLoading, accessing a class whose module declares a needed factory (ITKIOImageBase -> ImageIO) grows ObjectFactoryBase.GetRegisteredFactories(); the disabled path is already covered by nodefaultfactories.py. - stdlib pickle: pickle.loads(pickle.dumps(itk.ITKCommon)) returns the same instance, exercising the per-submodule __reduce_ex__ shim wired by _make_itk_lazy_submodule (a bare types.ModuleType is unpicklable on CPython 3.12+ without it). Remove unused symbols: - _lazy_submodule.py: docstring named the deleted LazyITKModule subclass; rephrased to describe the module's behavior directly. - multiprocess_lazy_loading.py: header comments used CamelCase "LazyLoading" as a symbol; rephrased to "lazy module loading" / "PEP 562 __getattr__ hook" while keeping the threading contract the test still enforces. - Wrapping/Generators/Python/CMakeLists.txt: directory-layout comment listed itk(...|LazyLoading|...).py as a static support file; updated to the actual filenames currently shipped. Comment- and docstring-only; no functional change. PythonLazyModule, PythonMultiprocessLazyLoad, and PythonLazyLoadingImage all still pass. --- Wrapping/Generators/Python/CMakeLists.txt | 2 +- Wrapping/Generators/Python/Tests/lazy.py | 31 +++++++++++++++++++ .../Python/Tests/multiprocess_lazy_loading.py | 11 +++---- .../Python/itk/support/_lazy_submodule.py | 5 ++- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/Wrapping/Generators/Python/CMakeLists.txt b/Wrapping/Generators/Python/CMakeLists.txt index 1fc8bfc180f..e4fd7bad312 100644 --- a/Wrapping/Generators/Python/CMakeLists.txt +++ b/Wrapping/Generators/Python/CMakeLists.txt @@ -10,7 +10,7 @@ ## - itk/ ## - __init__.py # cmake copied file ## - support/ -## - itk(Extras|Template|Base|LazyLoading|...).py # static python files cmake copied +## - (extras|template_class|base|_lazy_submodule|...).py # static python files cmake copied ## - Configuration/ ## -- ITK${MODULE_ITEM}Config.py # igenerator.py output config database index files for .so ## -- ITK${MODULE_ITEM}_snake_case.py # igenerator.py output config database index files for .so diff --git a/Wrapping/Generators/Python/Tests/lazy.py b/Wrapping/Generators/Python/Tests/lazy.py index 1b5106d21f2..3bc75f9dd1c 100644 --- a/Wrapping/Generators/Python/Tests/lazy.py +++ b/Wrapping/Generators/Python/Tests/lazy.py @@ -23,6 +23,37 @@ assert ITKCommon.__package__ == "itk" +# PEP 562 __dir__: lazy attributes are visible to tab-completion and +# tooling without forcing a SWIG load. Only dunder attributes (which +# the lazy __getattr__ explicitly skips) have been touched up to this +# point, so the underlying SWIG modules have not been loaded yet -- +# "Image" must show up in dir() but stay absent from the module's +# __dict__ until first access. +assert "Image" in dir(itk) +assert "Image" not in vars(itk) +assert "Image" in dir(ITKCommon) +assert "Image" not in vars(ITKCommon) + +# Factory hook: under the default configuration, accessing a class +# whose module declares a needed factory (ITKIOImageBase -> ImageIO) +# must trigger the corresponding factory registration as a side effect +# of the lazy load. nodefaultfactories.py covers the disabled path. +import itkConfig + +if itkConfig.DefaultFactoryLoading: + factories_before = len(itk.ObjectFactoryBase.GetRegisteredFactories()) + _ = itk.ImageFileReader + factories_after = len(itk.ObjectFactoryBase.GetRegisteredFactories()) + assert factories_after > factories_before + +# stdlib pickle round-trip on a per-submodule namespace: the lazy +# submodule is registered in sys.modules and its instance __reduce_ex__ +# resolves through importlib.import_module, so plain pickle (not just +# cloudpickle) must round-trip to the same instance. +import pickle + +assert pickle.loads(pickle.dumps(itk.ITKCommon)) is itk.ITKCommon + # Test pickling used bash Dask _has_cloudpickle: bool = False try: diff --git a/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py b/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py index 6e832456706..753ae50867f 100755 --- a/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py +++ b/Wrapping/Generators/Python/Tests/multiprocess_lazy_loading.py @@ -1,25 +1,24 @@ #!/usr/bin/env python -# LazyLoading must be threadsafe +# Lazy module loading must be threadsafe # # The loading of modules in python *must* occur as a # single atomic transaction in multiprocessing environments (i.e. # the module should only be loaded by one thread). # -# The LazyLoading of ITK did not treat the loading of +# Historically ITK's lazy loader did not treat the loading of # modules as an atomic transaction, and multiple threads # would attempt to load the cascading dependencies out of # order. # -# The `getattr` override that allows LazyLoading to work -# in the case where the module is *not* loaded now blocks -# while the first thread completes the delayed module loading. +# The PEP 562 ``__getattr__`` hook that triggers a delayed +# load now blocks while the first thread completes the load. # After the first thread completes module load as an atomic # transaction, the other threads fall through (skip loading) # and return the value requested. # # Need to use a recursive lock for thread ownership so that the # first thread can can acquire a RLock as often as needed while -# recursively processing dependent modules lazy loads. Other threads need +# recursively processing dependent module loads. Other threads need # to wait until this first thread releases the RLock. diff --git a/Wrapping/Generators/Python/itk/support/_lazy_submodule.py b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py index bdd26bc99b1..4eb8e590a03 100644 --- a/Wrapping/Generators/Python/itk/support/_lazy_submodule.py +++ b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py @@ -18,9 +18,8 @@ """Builder for per-submodule lazy namespaces (``itk.ITKCommon``, ...). -Replaces the legacy ``LazyITKModule(types.ModuleType)`` subclass with a -plain ``types.ModuleType`` instance wired with PEP 562 ``__getattr__`` / -``__dir__`` callables. The submodule is registered in +Each submodule is a plain ``types.ModuleType`` instance wired with PEP 562 +``__getattr__`` / ``__dir__`` callables, registered in ``sys.modules['itk.']`` so ``cloudpickle`` (used by the Dask worker round-trip exercised in ``Tests/lazy.py``) can re-import it by its dotted name. From 99dd95882538e922420d40d96825a63079187193 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 1 May 2026 14:51:22 -0400 Subject: [PATCH 09/11] DOC: Note itkConfig.LazyLoading removal in ITK 6 guide Add a section to the ITK 6 migration guide describing the removal of itkConfig.LazyLoading and the ITK_PYTHON_LAZYLOADING environment variable, which were dropped when the Python lazy-loading mechanism was rewritten on top of PEP 562. The section distinguishes per-symbol behavior: assignment to itkConfig.LazyLoading is silently ignored, any subsequent read raises AttributeError, and the environment variable is no longer consulted. --- .../migration_guides/itk_6_migration_guide.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Documentation/docs/migration_guides/itk_6_migration_guide.md b/Documentation/docs/migration_guides/itk_6_migration_guide.md index 2ef423b5138..b592cc543d2 100644 --- a/Documentation/docs/migration_guides/itk_6_migration_guide.md +++ b/Documentation/docs/migration_guides/itk_6_migration_guide.md @@ -394,6 +394,68 @@ for thread in threads: issues with callbacks, you can disable GIL release by setting `-DITK_PYTHON_RELEASE_GIL=OFF` when building ITK. +Python lazy loading is always on +-------------------------------- + +The Python wrapping layer's lazy-loading mechanism has been rewritten on top +of the standard-library [PEP 562](https://peps.python.org/pep-0562/) +module-level `__getattr__` / `__dir__` protocol. Lazy first-touch resolution +is now the only mode of operation: there is no eager-import alternative. + +### Removed + +- `itkConfig.LazyLoading` — previously a boolean attribute on `itkConfig` + that, when set to `False`, forced every wrapped submodule to be imported + at `import itk` time. +- `ITK_PYTHON_LAZYLOADING` — the environment variable that seeded the value + of `itkConfig.LazyLoading` at startup. +- The custom `itk.support.lazy.LazyITKModule` `types.ModuleType` subclass + (and its `ITKLazyLoadLock`, `_lazy_itk_module_reconstructor`, and + `not_loaded` sentinel) that the previous implementation relied on. + +### What you need to do + +If your code or test suite contains either of the following, remove +them: + +- `itkConfig.LazyLoading = False` — assignment alone is silently ignored + (module attributes are mutable, but no ITK code reads the value + anymore). Any subsequent *read* of `itkConfig.LazyLoading`, however, + will raise `AttributeError`. Delete both the write and any reads: + + ```python + import itkConfig + itkConfig.LazyLoading = False # remove this line + if itkConfig.LazyLoading: # this read now raises AttributeError + ... + ``` + +- `ITK_PYTHON_LAZYLOADING` — the environment variable is no longer + consulted. Any export is silently ignored; remove it from launch + scripts and CI configurations: + + ```bash + ITK_PYTHON_LAZYLOADING=0 python my_script.py # drop the env var + ``` + +The observable contract from a caller's point of view is otherwise +unchanged: `import itk` returns the package, attribute access on `itk` +or any `itk.` resolves on first touch, `dir(itk)` lists the +full set of lazily-discoverable attributes, and pickle / cloudpickle +round-trips through `sys.modules` continue to work. + +### Why this changed + +The previous `LazyITKModule.__getattribute__` intercepted *every* attribute +read on the `itk` package just to check a sentinel; the PEP 562 hook only +fires on a miss, so steady-state attribute access now hits the standard +module fast path. The legacy `ITKLazyLoadLock` also combined +`threading.RLock` and `multiprocessing.RLock`, and constructing the +multiprocessing half pinned the multiprocessing start method at +`import itk` time — breaking downstream callers that wanted to call +`multiprocessing.set_start_method(...)` after import. The replacement uses +only `threading.RLock`. + Modern CMake Interface Libraries --------------------------------- From 82697ed5fff5af75232611ec659b8c3f5a2fa9b2 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 1 May 2026 15:51:58 -0400 Subject: [PATCH 10/11] STYLE: Run black on Wrapping/Generators/Python/itk/__init__.py Via pixi run pre-commit-run --- Wrapping/Generators/Python/itk/__init__.py | 26 +++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Wrapping/Generators/Python/itk/__init__.py b/Wrapping/Generators/Python/itk/__init__.py index 2a821766458..865c06d8af5 100644 --- a/Wrapping/Generators/Python/itk/__init__.py +++ b/Wrapping/Generators/Python/itk/__init__.py @@ -71,9 +71,7 @@ def __getattr__(name: str): try: module_name = _lazy_attribute_to_module[name] except KeyError: - raise AttributeError( - f"module 'itk' has no attribute {name!r}" - ) from None + raise AttributeError(f"module 'itk' has no attribute {name!r}") from None g = globals() with _lazy_load_lock: cached = g.get(name, _MISSING) @@ -87,9 +85,7 @@ def __getattr__(name: str): try: return g[name] except KeyError: - raise AttributeError( - f"module 'itk' has no attribute {name!r}" - ) from None + raise AttributeError(f"module 'itk' has no attribute {name!r}") from None def __dir__(): @@ -129,9 +125,9 @@ def _initialize_module(): for module, data in _base.itk_base_global_module_data.items(): for template_feature in data._template_feature_tuples: attr = template_feature._py_class_name - _base.itk_base_global_lazy_attributes.setdefault( - attr, _builtin_set() - ).add(module) + _base.itk_base_global_lazy_attributes.setdefault(attr, _builtin_set()).add( + module + ) if template_feature._class_in_module: _lazy_attribute_to_module[attr] = module else: @@ -152,13 +148,13 @@ def _initialize_module(): attributes: dict[str, list[str]] = {} for template_feature in data._template_feature_tuples: if template_feature._class_in_module: - attributes.setdefault( - template_feature._py_class_name, [] - ).insert(0, module) + attributes.setdefault(template_feature._py_class_name, []).insert( + 0, module + ) else: - attributes.setdefault( - template_feature._py_class_name, [] - ).append(module) + attributes.setdefault(template_feature._py_class_name, []).append( + module + ) for function in data._snake_case_functions: attributes.setdefault(function, []).append(module) for k, v in attributes.items(): From 9e33a8188328136b42a822799806c22ae20daedb Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Fri, 1 May 2026 15:53:10 -0400 Subject: [PATCH 11/11] ENH: Address PR 6183 Greptile P2 findings Two follow-ups from the Greptile review of PR 6183: * Drop the dead `loaded_modules: set[str]` declaration and its `loaded_modules.add(target)` write in `_make_itk_lazy_submodule`. The set was a vestige of `LazyITKModule.__getstate__`/`__setstate__`; the new `__reduce_ex__` shim delegates to `importlib.import_module` and never consults it. * Reword the migration-guide example for `itkConfig.LazyLoading`. The previous inline comment claimed the second line raised `AttributeError`, which was wrong: the preceding assignment had already set the attribute, so the read returned `False`. Split the example into separately commented write and read patterns and clarify that the `AttributeError` only occurs on a fresh `import itkConfig` with no prior assignment. --- .../docs/migration_guides/itk_6_migration_guide.md | 14 ++++++++++---- .../Python/itk/support/_lazy_submodule.py | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Documentation/docs/migration_guides/itk_6_migration_guide.md b/Documentation/docs/migration_guides/itk_6_migration_guide.md index b592cc543d2..2f479ba9bf5 100644 --- a/Documentation/docs/migration_guides/itk_6_migration_guide.md +++ b/Documentation/docs/migration_guides/itk_6_migration_guide.md @@ -420,13 +420,19 @@ them: - `itkConfig.LazyLoading = False` — assignment alone is silently ignored (module attributes are mutable, but no ITK code reads the value - anymore). Any subsequent *read* of `itkConfig.LazyLoading`, however, - will raise `AttributeError`. Delete both the write and any reads: + anymore). On a fresh `import itkConfig` with no prior assignment, a + *read* of `itkConfig.LazyLoading` raises `AttributeError` because the + attribute is no longer defined. Delete both the writes and any reads: ```python import itkConfig - itkConfig.LazyLoading = False # remove this line - if itkConfig.LazyLoading: # this read now raises AttributeError + + # Remove any assignments — they are silently no-ops: + itkConfig.LazyLoading = False + + # Remove any reads — without a prior assignment they raise + # AttributeError, since itkConfig no longer defines this attribute: + if itkConfig.LazyLoading: ... ``` diff --git a/Wrapping/Generators/Python/itk/support/_lazy_submodule.py b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py index 4eb8e590a03..7a41143faee 100644 --- a/Wrapping/Generators/Python/itk/support/_lazy_submodule.py +++ b/Wrapping/Generators/Python/itk/support/_lazy_submodule.py @@ -62,7 +62,6 @@ def _make_itk_lazy_submodule( m.__loader__ = None belong: dict[str, str] = {k: v[0] for k, v in lazy_attributes.items() if v} - loaded_modules: set[str] = set() def __getattr__(name: str): if name.startswith("__") and name.endswith("__"): @@ -81,7 +80,6 @@ def __getattr__(name: str): namespace: dict = {} _base.itk_load_swig_module(target, namespace) d.update(namespace) - loaded_modules.add(target) if _itkConfig.DefaultFactoryLoading: _base.load_module_needed_factories(target) try: