From 4389d6d3bf542cb3d248e2c3d64aa57f2a05ad89 Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Wed, 22 Oct 2025 14:22:52 +0900 Subject: [PATCH 01/11] TYP: Use `typing.overload` during `TYPE_CHECKING` --- pyomo/common/pyomo_typing.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/common/pyomo_typing.py b/pyomo/common/pyomo_typing.py index 35f3567432a..be4631e97cd 100644 --- a/pyomo/common/pyomo_typing.py +++ b/pyomo/common/pyomo_typing.py @@ -18,16 +18,20 @@ def _get_fullqual_name(func: typing.Callable) -> str: return f"{func.__module__}.{func.__qualname__}" -def overload(func: typing.Callable): - """Wrap typing.overload that remembers the overloaded signatures +if typing.TYPE_CHECKING: + from typing import overload as overload +else: - This provides a custom implementation of typing.overload that - remembers the overloaded signatures so that they are available for - runtime inspection. + def overload(func: typing.Callable): + """Wrap typing.overload that remembers the overloaded signatures - """ - _overloads.setdefault(_get_fullqual_name(func), []).append(func) - return typing.overload(func) + This provides a custom implementation of typing.overload that + remembers the overloaded signatures so that they are available for + runtime inspection. + + """ + _overloads.setdefault(_get_fullqual_name(func), []).append(func) + return typing.overload(func) def get_overloads_for(func: typing.Callable): From 4765c4a0f4124e99fc38b8d202b74a7ace4dfc53 Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Wed, 22 Oct 2025 15:14:59 +0900 Subject: [PATCH 02/11] TYP: `Model.__new__` should return an instance of the subclass --- pyomo/core/base/PyomoModel.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index c2da52eed5f..accdba29148 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -14,6 +14,7 @@ from weakref import ref as weakref_ref import gc import math +from typing import TypeVar from pyomo.common import timing from pyomo.common.collections import Bunch @@ -572,6 +573,10 @@ def select( StaleFlagManager.mark_all_as_stale(delayed=True) +# NOTE: Python 3.11+ use `typing.Self` +ModelT = TypeVar("ModelT", bound="Model") + + @ModelComponentFactory.register( 'Model objects can be used as a component of other models.' ) @@ -583,9 +588,9 @@ class Model(ScalarBlock): _Block_reserved_words = set() - def __new__(cls, *args, **kwds): + def __new__(cls: type[ModelT], *args, **kwds) -> ModelT: if cls != Model: - return super(Model, cls).__new__(cls) + return super(Model, cls).__new__(cls) # type: ignore raise TypeError( "Directly creating the 'Model' class is not allowed. Please use the " From 15528c38a08a0f241551cd069148d57120347f5e Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Wed, 22 Oct 2025 15:35:07 +0900 Subject: [PATCH 03/11] TYP: better typing for `SolverFactory`s and `pyomo.future.solver_factory` --- pyomo/contrib/appsi/base.py | 5 ++++ pyomo/contrib/solver/common/factory.py | 10 +++++++- pyomo/future.py | 35 +++++++++++++++++++++----- pyomo/opt/base/solvers.py | 6 +++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 181cb5e28ab..44536c57c03 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -16,6 +16,7 @@ import weakref from typing import ( + TYPE_CHECKING, Sequence, Dict, Optional, @@ -1714,5 +1715,9 @@ class LegacySolver(LegacySolverInterface, cls): return decorator + if TYPE_CHECKING: + # NOTE: `Factory.__call__` can return None, but for the common case + def __call__(self, name, **kwds) -> Solver: ... + SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/solver/common/factory.py b/pyomo/contrib/solver/common/factory.py index 8af5de6ab9c..a982b05477a 100644 --- a/pyomo/contrib/solver/common/factory.py +++ b/pyomo/contrib/solver/common/factory.py @@ -10,9 +10,11 @@ # ___________________________________________________________________________ -from pyomo.opt.base.solvers import LegacySolverFactory +from typing import TYPE_CHECKING + from pyomo.common.factory import Factory from pyomo.contrib.solver.common.base import LegacySolverWrapper +from pyomo.opt.base.solvers import LegacySolverFactory class SolverFactoryClass(Factory): @@ -107,6 +109,12 @@ class LegacySolver(LegacySolverWrapper, cls): return decorator + if TYPE_CHECKING: + from pyomo.contrib.solver.common.base import SolverBase + + # NOTE: `Factory.__call__` can return None, but for the common case + def __call__(self, name, **kwds) -> SolverBase: ... + #: Global registry/factory for "v2" solver interfaces. SolverFactory: SolverFactoryClass = SolverFactoryClass() diff --git a/pyomo/future.py b/pyomo/future.py index 2d8718f5f47..94168324d3c 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -9,8 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from typing import TYPE_CHECKING, Any, Literal, overload + import pyomo.environ as _environ +if TYPE_CHECKING: + import pyomo.contrib.appsi.base as _appsi + import pyomo.contrib.solver.common.factory as _contrib + import pyomo.opt.base.solvers as _solvers + __doc__ = """ Preview capabilities through ``pyomo.__future__`` ================================================= @@ -28,13 +35,29 @@ """ +solver_factory_v1: "_solvers.SolverFactoryClass" +solver_factory_v2: "_appsi.SolverFactoryClass" +solver_factory_v3: "_contrib.SolverFactoryClass" + def __getattr__(name): - if name in ('solver_factory_v1', 'solver_factory_v2', 'solver_factory_v3'): + if name in ("solver_factory_v1", "solver_factory_v2", "solver_factory_v3"): return solver_factory(int(name[-1])) raise AttributeError(f"module '{__name__}' has no attribute '{name}'") +@overload +def solver_factory(version: None = None) -> int: ... +@overload +def solver_factory(version: Literal[1]) -> "_solvers.SolverFactoryClass": ... +@overload +def solver_factory(version: Literal[2]) -> "_appsi.SolverFactoryClass": ... +@overload +def solver_factory(version: Literal[3]) -> "_contrib.SolverFactoryClass": ... +@overload +def solver_factory(version: int) -> Any: ... + + def solver_factory(version=None): """Get (or set) the active implementation of the SolverFactory @@ -90,19 +113,19 @@ def solver_factory(version=None): if current is None: for ver, cls in versions.items(): if cls._cls is _environ.SolverFactory._cls: - solver_factory._active_version = ver + solver_factory._active_version = ver # type: ignore break - return solver_factory._active_version + return solver_factory._active_version # type: ignore # # The user is just asking what the current SolverFactory is; tell them. if version is None: - return solver_factory._active_version + return solver_factory._active_version # type: ignore # # Update the current SolverFactory to be a shim around (shallow copy # of) the new active factory src = versions.get(version, None) if version is not None: - solver_factory._active_version = version + solver_factory._active_version = version # type: ignore for attr in ('_description', '_cls', '_doc'): setattr(_environ.SolverFactory, attr, getattr(src, attr)) else: @@ -113,4 +136,4 @@ def solver_factory(version=None): return src -solver_factory._active_version = solver_factory() +solver_factory._active_version = solver_factory() # type: ignore diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index 158d6888f14..563376704e2 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -14,6 +14,7 @@ import time import logging import shlex +from typing import overload from pyomo.common import Factory from pyomo.common.enums import SolverAPIVersion @@ -144,6 +145,11 @@ def _solver_error(self, method_name): class SolverFactoryClass(Factory): + @overload + def __call__(self, _name: None = None, **kwds) -> "SolverFactoryClass": ... + @overload + def __call__(self, _name, **kwds) -> "OptSolver": ... + def __call__(self, _name=None, **kwds): if _name is None: return self From 58ec35d3e547d5c7c9f17c439857749b4c4ce156 Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Tue, 28 Oct 2025 09:43:47 +0900 Subject: [PATCH 04/11] TYP: reorder `@overload`s, overlapped overloads never be used --- pyomo/core/base/block.py | 10 +++++----- pyomo/core/base/constraint.py | 10 +++++----- pyomo/core/base/param.py | 10 +++++----- pyomo/core/base/set.py | 4 ++-- pyomo/core/base/var.py | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 97b2dee721b..851eda9d74c 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2085,17 +2085,17 @@ class Block(ActiveIndexedComponent): _ComponentDataClass = BlockData _private_data_initializers = defaultdict(lambda: dict) - @overload - def __new__( - cls: Type[Block], *args, **kwds - ) -> Union[ScalarBlock, IndexedBlock]: ... - @overload def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: ... @overload def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: ... + @overload + def __new__( + cls: Type[Block], *args, **kwds + ) -> Union[ScalarBlock, IndexedBlock]: ... + def __new__(cls, *args, **kwds): if cls != Block: return super(Block, cls).__new__(cls) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 970c393425b..8f15060c93e 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -638,17 +638,17 @@ class Constraint(ActiveIndexedComponent): Violated = Infeasible Satisfied = Feasible - @overload - def __new__( - cls: Type[Constraint], *args, **kwds - ) -> Union[ScalarConstraint, IndexedConstraint]: ... - @overload def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: ... @overload def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: ... + @overload + def __new__( + cls: Type[Constraint], *args, **kwds + ) -> Union[ScalarConstraint, IndexedConstraint]: ... + def __new__(cls, *args, **kwds): if cls != Constraint: return super().__new__(cls) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 02ba103cae3..d6406f2ba98 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -309,17 +309,17 @@ class NoValue: pass - @overload - def __new__( - cls: Type[Param], *args, **kwds - ) -> Union[ScalarParam, IndexedParam]: ... - @overload def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: ... @overload def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: ... + @overload + def __new__( + cls: Type[Param], *args, **kwds + ) -> Union[ScalarParam, IndexedParam]: ... + def __new__(cls, *args, **kwds): if cls != Param: return super(Param, cls).__new__(cls) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8c858f96ba8..4f62c822310 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2128,10 +2128,10 @@ class SortedOrder: _UnorderedInitializers = {set} @overload - def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... @overload - def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... + def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... def __new__(cls, *args, **kwds): if cls is not Set: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 6b9b5fb4151..9bb9555ae10 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -575,15 +575,15 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): _ComponentDataClass = VarData - @overload - def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... - @overload def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: ... @overload def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: ... + @overload + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... + def __new__(cls, *args, **kwargs): if cls is not Var: return super(Var, cls).__new__(cls) From 4863938fd14188b41b798fe94f6df6c14efe8bbf Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Fri, 12 Dec 2025 08:33:03 +0900 Subject: [PATCH 05/11] TYP: Rename `ModelT` to `ModelType` --- pyomo/core/base/PyomoModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index accdba29148..e8fec936536 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -574,7 +574,7 @@ def select( # NOTE: Python 3.11+ use `typing.Self` -ModelT = TypeVar("ModelT", bound="Model") +ModelType = TypeVar("ModelType", bound="Model") @ModelComponentFactory.register( @@ -588,7 +588,7 @@ class Model(ScalarBlock): _Block_reserved_words = set() - def __new__(cls: type[ModelT], *args, **kwds) -> ModelT: + def __new__(cls: type[ModelType], *args, **kwds) -> ModelType: if cls != Model: return super(Model, cls).__new__(cls) # type: ignore From f9863169c0da3a457dfc851d33900207dfb3eecc Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Fri, 12 Dec 2025 08:48:14 +0900 Subject: [PATCH 06/11] TYP: Use `typing.overload` and `typing.get_overloads` in py3.11+ Co-Authored-By: John Siirola <356359+jsiirola@users.noreply.github.com> --- pyomo/common/pyomo_typing.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/common/pyomo_typing.py b/pyomo/common/pyomo_typing.py index be4631e97cd..6caca9ce256 100644 --- a/pyomo/common/pyomo_typing.py +++ b/pyomo/common/pyomo_typing.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import sys import typing _overloads = {} @@ -18,9 +19,7 @@ def _get_fullqual_name(func: typing.Callable) -> str: return f"{func.__module__}.{func.__qualname__}" -if typing.TYPE_CHECKING: - from typing import overload as overload -else: +if sys.version_info[:2] <= (3, 10) and not typing.TYPE_CHECKING: def overload(func: typing.Callable): """Wrap typing.overload that remembers the overloaded signatures @@ -33,6 +32,8 @@ def overload(func: typing.Callable): _overloads.setdefault(_get_fullqual_name(func), []).append(func) return typing.overload(func) - -def get_overloads_for(func: typing.Callable): - return _overloads.get(_get_fullqual_name(func), []) + def get_overloads_for(func: typing.Callable): + return _overloads.get(_get_fullqual_name(func), []) +else: + from typing import get_overloads as get_overloads_for + from typing import overload as overload From b5a45cba79920186902a96cb2f9380db62e48273 Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Fri, 12 Dec 2025 09:40:09 +0900 Subject: [PATCH 07/11] refactor: `solver_factory`: Promote the state to module-level variable --- pyomo/future.py | 96 +++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/pyomo/future.py b/pyomo/future.py index 94168324d3c..e8f2da3a568 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -9,15 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from typing import TYPE_CHECKING, Any, Literal, overload - -import pyomo.environ as _environ - -if TYPE_CHECKING: - import pyomo.contrib.appsi.base as _appsi - import pyomo.contrib.solver.common.factory as _contrib - import pyomo.opt.base.solvers as _solvers - __doc__ = """ Preview capabilities through ``pyomo.__future__`` ================================================= @@ -35,30 +26,53 @@ """ -solver_factory_v1: "_solvers.SolverFactoryClass" -solver_factory_v2: "_appsi.SolverFactoryClass" -solver_factory_v3: "_contrib.SolverFactoryClass" +from typing import Any, Literal, overload + +import pyomo.contrib.appsi.base as _appsi +import pyomo.contrib.solver.common.factory as _contrib +import pyomo.environ as _environ +import pyomo.opt.base.solvers as _solvers + +_SolverFactoryClassV1 = _solvers.SolverFactoryClass +_SolverFactoryClassV2 = _appsi.SolverFactoryClass +_SolverFactoryClassV3 = _contrib.SolverFactoryClass + +solver_factory_v1: _SolverFactoryClassV1 = _solvers.LegacySolverFactory +solver_factory_v2: _SolverFactoryClassV2 = _appsi.SolverFactory +solver_factory_v3: _SolverFactoryClassV3 = _contrib.SolverFactory +_versions = { + 1: solver_factory_v1, + 2: solver_factory_v2, + 3: solver_factory_v3, +} -def __getattr__(name): - if name in ("solver_factory_v1", "solver_factory_v2", "solver_factory_v3"): - return solver_factory(int(name[-1])) - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + +def _get_environ_version() -> int: + # Go look and see what it was initialized to in pyomo.environ + for ver, cls in _versions.items(): + if cls._cls is _environ.SolverFactory._cls: + return ver + # If initialized correctly, never reached + raise NotImplementedError + + +_active_version = _get_environ_version() @overload def solver_factory(version: None = None) -> int: ... @overload -def solver_factory(version: Literal[1]) -> "_solvers.SolverFactoryClass": ... +def solver_factory(version: Literal[1]) -> _SolverFactoryClassV1: ... @overload -def solver_factory(version: Literal[2]) -> "_appsi.SolverFactoryClass": ... +def solver_factory(version: Literal[2]) -> _SolverFactoryClassV2: ... @overload -def solver_factory(version: Literal[3]) -> "_contrib.SolverFactoryClass": ... +def solver_factory(version: Literal[3]) -> _SolverFactoryClassV3: ... @overload def solver_factory(version: int) -> Any: ... -def solver_factory(version=None): +def solver_factory(version: int | None = None): """Get (or set) the active implementation of the SolverFactory This allows users to query / set the current implementation of the @@ -97,43 +111,21 @@ def solver_factory(version=None): >>> from pyomo.__future__ import solver_factory_v1 """ - import pyomo.opt.base.solvers as _solvers - import pyomo.contrib.solver.common.factory as _contrib - import pyomo.contrib.appsi.base as _appsi - - versions = { - 1: _solvers.LegacySolverFactory, - 2: _appsi.SolverFactory, - 3: _contrib.SolverFactory, - } - - current = getattr(solver_factory, '_active_version', None) - # First time through, _active_version is not defined. Go look and - # see what it was initialized to in pyomo.environ - if current is None: - for ver, cls in versions.items(): - if cls._cls is _environ.SolverFactory._cls: - solver_factory._active_version = ver # type: ignore - break - return solver_factory._active_version # type: ignore - # - # The user is just asking what the current SolverFactory is; tell them. + global _active_version + if version is None: - return solver_factory._active_version # type: ignore - # + return _active_version + # Update the current SolverFactory to be a shim around (shallow copy # of) the new active factory - src = versions.get(version, None) - if version is not None: - solver_factory._active_version = version # type: ignore - for attr in ('_description', '_cls', '_doc'): - setattr(_environ.SolverFactory, attr, getattr(src, attr)) + selected_factory = _versions.get(version, None) + if selected_factory is not None: + _active_version = version + for attr in ("_description", "_cls", "_doc"): + setattr(_environ.SolverFactory, attr, getattr(selected_factory, attr)) else: raise ValueError( "Invalid value for target solver factory version; expected {1, 2, 3}, " f"received {version}" ) - return src - - -solver_factory._active_version = solver_factory() # type: ignore + return selected_factory From f2db1c789d387bb252429e344b640c51b63d7c5e Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Fri, 12 Dec 2025 09:44:28 +0900 Subject: [PATCH 08/11] style: reformat with black --- pyomo/common/pyomo_typing.py | 1 + pyomo/future.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/common/pyomo_typing.py b/pyomo/common/pyomo_typing.py index 6caca9ce256..98c54349645 100644 --- a/pyomo/common/pyomo_typing.py +++ b/pyomo/common/pyomo_typing.py @@ -34,6 +34,7 @@ def overload(func: typing.Callable): def get_overloads_for(func: typing.Callable): return _overloads.get(_get_fullqual_name(func), []) + else: from typing import get_overloads as get_overloads_for from typing import overload as overload diff --git a/pyomo/future.py b/pyomo/future.py index e8f2da3a568..afdb8d39528 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -41,11 +41,7 @@ solver_factory_v2: _SolverFactoryClassV2 = _appsi.SolverFactory solver_factory_v3: _SolverFactoryClassV3 = _contrib.SolverFactory -_versions = { - 1: solver_factory_v1, - 2: solver_factory_v2, - 3: solver_factory_v3, -} +_versions = {1: solver_factory_v1, 2: solver_factory_v2, 3: solver_factory_v3} def _get_environ_version() -> int: From fa1ade78725ad988b909e163a6040f9594886bb2 Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Sat, 13 Dec 2025 05:12:24 +0900 Subject: [PATCH 09/11] Revert "refactor: `solver_factory`: Promote the state to module-level variable" This reverts commit b5a45cba79920186902a96cb2f9380db62e48273. --- pyomo/future.py | 92 ++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/pyomo/future.py b/pyomo/future.py index afdb8d39528..94168324d3c 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -9,6 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from typing import TYPE_CHECKING, Any, Literal, overload + +import pyomo.environ as _environ + +if TYPE_CHECKING: + import pyomo.contrib.appsi.base as _appsi + import pyomo.contrib.solver.common.factory as _contrib + import pyomo.opt.base.solvers as _solvers + __doc__ = """ Preview capabilities through ``pyomo.__future__`` ================================================= @@ -26,49 +35,30 @@ """ -from typing import Any, Literal, overload - -import pyomo.contrib.appsi.base as _appsi -import pyomo.contrib.solver.common.factory as _contrib -import pyomo.environ as _environ -import pyomo.opt.base.solvers as _solvers - -_SolverFactoryClassV1 = _solvers.SolverFactoryClass -_SolverFactoryClassV2 = _appsi.SolverFactoryClass -_SolverFactoryClassV3 = _contrib.SolverFactoryClass - -solver_factory_v1: _SolverFactoryClassV1 = _solvers.LegacySolverFactory -solver_factory_v2: _SolverFactoryClassV2 = _appsi.SolverFactory -solver_factory_v3: _SolverFactoryClassV3 = _contrib.SolverFactory +solver_factory_v1: "_solvers.SolverFactoryClass" +solver_factory_v2: "_appsi.SolverFactoryClass" +solver_factory_v3: "_contrib.SolverFactoryClass" -_versions = {1: solver_factory_v1, 2: solver_factory_v2, 3: solver_factory_v3} - -def _get_environ_version() -> int: - # Go look and see what it was initialized to in pyomo.environ - for ver, cls in _versions.items(): - if cls._cls is _environ.SolverFactory._cls: - return ver - # If initialized correctly, never reached - raise NotImplementedError - - -_active_version = _get_environ_version() +def __getattr__(name): + if name in ("solver_factory_v1", "solver_factory_v2", "solver_factory_v3"): + return solver_factory(int(name[-1])) + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @overload def solver_factory(version: None = None) -> int: ... @overload -def solver_factory(version: Literal[1]) -> _SolverFactoryClassV1: ... +def solver_factory(version: Literal[1]) -> "_solvers.SolverFactoryClass": ... @overload -def solver_factory(version: Literal[2]) -> _SolverFactoryClassV2: ... +def solver_factory(version: Literal[2]) -> "_appsi.SolverFactoryClass": ... @overload -def solver_factory(version: Literal[3]) -> _SolverFactoryClassV3: ... +def solver_factory(version: Literal[3]) -> "_contrib.SolverFactoryClass": ... @overload def solver_factory(version: int) -> Any: ... -def solver_factory(version: int | None = None): +def solver_factory(version=None): """Get (or set) the active implementation of the SolverFactory This allows users to query / set the current implementation of the @@ -107,21 +97,43 @@ def solver_factory(version: int | None = None): >>> from pyomo.__future__ import solver_factory_v1 """ - global _active_version - + import pyomo.opt.base.solvers as _solvers + import pyomo.contrib.solver.common.factory as _contrib + import pyomo.contrib.appsi.base as _appsi + + versions = { + 1: _solvers.LegacySolverFactory, + 2: _appsi.SolverFactory, + 3: _contrib.SolverFactory, + } + + current = getattr(solver_factory, '_active_version', None) + # First time through, _active_version is not defined. Go look and + # see what it was initialized to in pyomo.environ + if current is None: + for ver, cls in versions.items(): + if cls._cls is _environ.SolverFactory._cls: + solver_factory._active_version = ver # type: ignore + break + return solver_factory._active_version # type: ignore + # + # The user is just asking what the current SolverFactory is; tell them. if version is None: - return _active_version - + return solver_factory._active_version # type: ignore + # # Update the current SolverFactory to be a shim around (shallow copy # of) the new active factory - selected_factory = _versions.get(version, None) - if selected_factory is not None: - _active_version = version - for attr in ("_description", "_cls", "_doc"): - setattr(_environ.SolverFactory, attr, getattr(selected_factory, attr)) + src = versions.get(version, None) + if version is not None: + solver_factory._active_version = version # type: ignore + for attr in ('_description', '_cls', '_doc'): + setattr(_environ.SolverFactory, attr, getattr(src, attr)) else: raise ValueError( "Invalid value for target solver factory version; expected {1, 2, 3}, " f"received {version}" ) - return selected_factory + return src + + +solver_factory._active_version = solver_factory() # type: ignore From b42557aba11cd016624438bc8490d92999f7dd9c Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Sat, 13 Dec 2025 05:35:45 +0900 Subject: [PATCH 10/11] refactor: `solver_factory`: Promote the state to module-level variable --- pyomo/future.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyomo/future.py b/pyomo/future.py index 94168324d3c..664bf6ed41d 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -58,7 +58,7 @@ def solver_factory(version: Literal[3]) -> "_contrib.SolverFactoryClass": ... def solver_factory(version: int) -> Any: ... -def solver_factory(version=None): +def solver_factory(version: int | None = None) -> int | Any: """Get (or set) the active implementation of the SolverFactory This allows users to query / set the current implementation of the @@ -97,6 +97,7 @@ def solver_factory(version=None): >>> from pyomo.__future__ import solver_factory_v1 """ + global _active_solver_factory_version import pyomo.opt.base.solvers as _solvers import pyomo.contrib.solver.common.factory as _contrib import pyomo.contrib.appsi.base as _appsi @@ -107,25 +108,26 @@ def solver_factory(version=None): 3: _contrib.SolverFactory, } - current = getattr(solver_factory, '_active_version', None) - # First time through, _active_version is not defined. Go look and - # see what it was initialized to in pyomo.environ - if current is None: + # First time through, _active_solver_factory_version is not defined. + # Go look and see what it was initialized to in pyomo.environ + try: + _ = _active_solver_factory_version + except NameError: for ver, cls in versions.items(): if cls._cls is _environ.SolverFactory._cls: - solver_factory._active_version = ver # type: ignore + _active_solver_factory_version = ver break - return solver_factory._active_version # type: ignore + return _active_solver_factory_version # # The user is just asking what the current SolverFactory is; tell them. if version is None: - return solver_factory._active_version # type: ignore + return _active_solver_factory_version # # Update the current SolverFactory to be a shim around (shallow copy # of) the new active factory src = versions.get(version, None) - if version is not None: - solver_factory._active_version = version # type: ignore + if src is not None: + _active_solver_factory_version = version for attr in ('_description', '_cls', '_doc'): setattr(_environ.SolverFactory, attr, getattr(src, attr)) else: @@ -136,4 +138,4 @@ def solver_factory(version=None): return src -solver_factory._active_version = solver_factory() # type: ignore +_active_solver_factory_version = solver_factory() From b4fcea0db18b8d5f5c683ba35c34633a54a3177a Mon Sep 17 00:00:00 2001 From: Takumasa Nakamura Date: Sat, 13 Dec 2025 07:35:30 +0900 Subject: [PATCH 11/11] fix: potentially bug if deleting first call Co-Authored-By: John Siirola <356359+jsiirola@users.noreply.github.com> --- pyomo/future.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyomo/future.py b/pyomo/future.py index 664bf6ed41d..97c2d7279af 100644 --- a/pyomo/future.py +++ b/pyomo/future.py @@ -107,20 +107,13 @@ def solver_factory(version: int | None = None) -> int | Any: 2: _appsi.SolverFactory, 3: _contrib.SolverFactory, } - - # First time through, _active_solver_factory_version is not defined. - # Go look and see what it was initialized to in pyomo.environ - try: - _ = _active_solver_factory_version - except NameError: - for ver, cls in versions.items(): - if cls._cls is _environ.SolverFactory._cls: - _active_solver_factory_version = ver - break - return _active_solver_factory_version - # # The user is just asking what the current SolverFactory is; tell them. if version is None: + if "_active_solver_factory_version" not in globals(): + for ver, cls in versions.items(): + if cls._cls is _environ.SolverFactory._cls: + _active_solver_factory_version = ver + break return _active_solver_factory_version # # Update the current SolverFactory to be a shim around (shallow copy