Skip to content

Conversation

@n-takumasa
Copy link

@n-takumasa n-takumasa commented Oct 24, 2025

Fixes # NA

Summary/Motivation:

To provide better autocompletion in vscode+pylance.
Complex dynamic patterns (e.g. Var() can return ScalarVar or IndexedVar) are out of scope as they are difficult to model accurately.

Changes proposed in this PR:

  • TYP: Use typing.overload during TYPE_CHECKING
  • TYP: Model.__new__ should return an instance of the subclass
  • TYP: better typing for SolverFactorys and pyomo.future.solver_factory
  • TYP: reorder @overloads, overlapped overloads never be used
image

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! I have several questions (see below). Some are less about this PR specifically, and more about generally how we want to better support typing in Pyomo. It would be useful to talk through that sometime in a Tuesday Dev Call.

Comment on lines 21 to 23
if typing.TYPE_CHECKING:
from typing import overload as overload
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to disable the overload?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A function calling typing.overload is not recognized as an overload by the type checker.
We need to replace it with typing.overload during type checking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I went digging and it turns out that logic is primarily needed by a dependent project -- but only for Python versions through 3.10. I think we should actually document that better in the code, with something like:

if sys.version_info[:2] <= (3, 10) and not TYPE_CHECKING:
    def overload(func: typing.Callable):
        """Wrap typing.overload that remembers the overloaded signatures

        This provides a custom implementation of typing.overload that
        remembers the overloaded signatures so that they are available for
        runtime inspection (backporting `get_overloads` from Python 3.11+).

        """
        _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), [])
else:
    from typing import overload, get_overloads as get_overloads_for

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing_extensions might be helpful.



# NOTE: Python 3.11+ use `typing.Self`
ModelT = TypeVar("ModelT", bound="Model")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't really settled on a typing convention for naming types, but appending T seems confusing.

  • would standardizing on appendint Type be more clear?
  • should local TypeVar objects be private by default (i.e., _modelType here)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ModelType is better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

pyomo/future.py Outdated
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need these annotations, should we?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we cannot add attributes to FunctionType.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't annotate the function, plus the attribute whereit is first instantiated at the bottom of the file?

def solver_factory(version: int | None = None) -> int:
# ...
solver_factory._active_version: int = solver_factory()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is allowed at runtime, but it's a type violation.
ref: microsoft/pyright#8838

@codecov
Copy link

codecov bot commented Nov 17, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.42%. Comparing base (a079b25) to head (b4fcea0).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3771   +/-   ##
=======================================
  Coverage   89.41%   89.42%           
=======================================
  Files         909      909           
  Lines      105579   105589   +10     
=======================================
+ Hits        94407    94421   +14     
+ Misses      11172    11168    -4     
Flag Coverage Δ
builders 29.11% <35.48%> (-0.01%) ⬇️
default 86.04% <51.61%> (?)
expensive 35.77% <48.38%> (?)
linux 86.73% <100.00%> (-2.46%) ⬇️
linux_other 86.73% <100.00%> (+<0.01%) ⬆️
osx 82.88% <100.00%> (+<0.01%) ⬆️
win 84.99% <100.00%> (+0.01%) ⬆️
win_other 84.99% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jsiirola
Copy link
Member

We were talking though this PR on the dev call this week. I think there are 3 changes that need to be made to get it merged:

  • rename modelT to ModelType
  • rework the TYPE_CHECKING test, possibly using the suggestion from the review
  • refactor the solver_factory function to promote the state to a module-level variable so that it can be statically-typed in a way that makes pyright happy.

@n-takumasa
Copy link
Author

Almost done.

  • rename modelT to ModelType

Is this intended to be public?
For now, I committed it as private with _ModelType.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments. The biggest is that I would prefer to see the changes to pyomo.future.solver_factory() be reverted with the exception of promoting the _active_version attribute on the function to a _active_solver_factory_version module attribute. The changes add an unnecessary (admittedly slight) overhead and break some of the documented functionality.

pyomo/future.py Outdated
Comment on lines 40 to 42
solver_factory_v1: _SolverFactoryClassV1 = _solvers.LegacySolverFactory
solver_factory_v2: _SolverFactoryClassV2 = _appsi.SolverFactory
solver_factory_v3: _SolverFactoryClassV3 = _contrib.SolverFactory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to declare these at the module scope: if we do, the module __getattr__ won't be called, which was how we were implementing the Python idiom:

from pyomo.__future__ import solver_factory_v3

as an alias for

from pyomo.__future__ import solver_factory
solver_factory(3)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, it's regression.
I didn't realize that importing changes the state.

pyomo/future.py Outdated
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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future is intended to manage all "future compatibility changes", so we should use a more specific name than versions here - there will eventually be several "versions" that this module will manage.

I would prefer not promoting this dict to the module scope: solver_factory will probably never be called more than once, so the performance gain of preserving the dict isn't worth the memory.

pyomo/future.py Outdated
Comment on lines 36 to 38
_SolverFactoryClassV1 = _solvers.SolverFactoryClass
_SolverFactoryClassV2 = _appsi.SolverFactoryClass
_SolverFactoryClassV3 = _contrib.SolverFactoryClass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not caching these at the module scope.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super close. If we can delete the 3 lines indicated, then I think this is good to go.

pyomo/future.py Outdated
_active_solver_factory_version = ver
break
return solver_factory._active_version
return _active_solver_factory_version
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[This is my fault] As I am thinking through this, I think this is actually a bug and this should be reworked. Right now, everything is "fine" because we are guaranteed that the first time this function is called, it will be called with version=None (that is hard-coded below and will happen on import). BUT that seems fragile. I think I would propose combining this logic with the if version is None below:

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

@n-takumasa: totally up to you if you want to make this change in this PR or leave it to me to get back to later

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree because if we delete _active_solver_factory_version = solver_factory() and call solver_factory(int), it will break.

@github-project-automation github-project-automation bot moved this from Todo to Reviewer Approved in Pyomo 6.10 Dec 12, 2025
Co-Authored-By: John Siirola <356359+jsiirola@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Reviewer Approved

Development

Successfully merging this pull request may close these issues.

4 participants