Skip to content

Conversation

@frrad
Copy link
Contributor

@frrad frrad commented Mar 28, 2025

Adds typing to files in examples/.

Also removed several more pulp/ files from the mypy blacklist, adding type: ignores where necessary.

Copy link
Collaborator

@pchtsp pchtsp left a comment

Choose a reason for hiding this comment

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

Thanks! I added minor questions about the renaming of some objects. I'm curious what tool renames objects based on their implied type. It sounds a bit weird.

lenOpts = [5, 7, 9]

def __init__(self, name, lengths=None):
def __init__(self, name: str, lengths: List[int]) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't we keep the =None here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed it because the zip below on L87 fails to typecheck if lengths is None. Since this code is presumably working I took that to mean that this is never used that way in practice.

lenOpts = ["5", "7", "9"]
numPatterns = 0

def __init__(self, name, lengths=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

also here, should we keep the =None?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as above

# The problem data is written to an .lp file
prob.writeLP("BeerDistributionProblem.lp")
for demand in range(500, 601, 10):
for demand_value in range(500, 601, 10):
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as with _dict, is this some kind of naming convention based on the type of the object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar to previously there is already a demand in scope and this was me fixing the collision.

@pchtsp pchtsp mentioned this pull request Mar 28, 2025
@christiansegercrantz
Copy link
Contributor

Thanks! I added minor questions about the renaming of some objects. I'm curious what tool renames objects based on their implied type. It sounds a bit weird.

This naming convention is in my opinion a bit outdated with modern typing. Instead of changing names to indicate type it would be better, as is being done in the linked PR, to indicate the type properly with typing.

@MBradbury
Copy link
Contributor

Few comments based on #807:

  1. Why use List and not list now that 3.8 has been dropped?
  2. I tried to check the type hints from the previous PR and different tools work differently. For example, I use pyright which is typically significant stricter than mypy and pyright still identifies some issues.
  3. In Type hints #807, I also did a lot of typing work in examples/, its not likely that I will get round to fixing that PR, so might be worth taking a look and extracting what is needed.

try: # allow Python 2/3 compatibility
maketrans = str.maketrans
except AttributeError:
from string import maketrans
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just change to be: from string import maketrans, Python 2 is not supported


_DICT_TYPE = dict

if sys.platform not in ["cli"]:
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no sys.platform which is cli any more. I presume this was IronPython. Why not simplify and just remove _DICT_TYPE as per https://github.com/coin-or/pulp/pull/807/files#diff-b2a258edfe0fa294f032a978656a4082842558faa98b0950d9f68c50f546bbe2L151

"""
self.expr = e if isinstance(e, LpAffineExpression) else LpAffineExpression(e)
self.name = name
self.constant: float = self.expr.constant
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

In my PR, I added some type aliases, which I then used where appropriate.

LptNumber: TypeAlias = int | float
LptItem: TypeAlias = Union[LptNumber, "LpVariable"]
LptExpr: TypeAlias = Union[LptItem, "LpAffineExpression"]
LptConstExpr: TypeAlias = Union[LptNumber, "LpAffineExpression"]
LptExprConstraint: TypeAlias = Union[LptExpr, "LpConstraint"]
LptVars: TypeAlias = "LpVariable"

def valid(self, eps: float = 0) -> bool:
val = self.value()
if self.sense == const.LpConstraintEQ:
return abs(val) <= eps
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

My links don't seem to have worked, there needs to be a check like:

val = self.value()
if val is None:
    return False

if name is not None:
self.__name = name.translate(LpAffineExpression.trans)
else:
self.__name = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why this is needed?

@MBradbury
Copy link
Contributor

There are some easy wins in the type hinting space, but as I found in #807, there are some architectural issues with LpElement, LpVariable and LpConstraintVar that need to be fixed. Also FractionElasticSubProblem just seems fundamentally broken.

Then there is also the question of what type the coefficient should be? int, float, decimal, numpy int/float types, or what combination of these?

Patterns += [Pattern("P" + str(len(Patterns)), i)]

# This loop will be repeated until morePatterns is set to False
while morePatterns == True:
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the == True

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am trying to keep the scope of this PR as limited as possible. There's a ton of less-than-ideal code here. If I started fixing things I don't know where I would be able to stop.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same problem I had in #807 :)

while morePatterns == True:
# Solve the problem as a Relaxed LP
duals = masterSolve(Patterns, rollData)
assert isinstance(duals, dict)
Copy link
Contributor

Choose a reason for hiding this comment

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

Surely, the return type is a tuple as per masterSolve?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that function returns a union type. this assert is necessary to tell mypy which part of the union this is.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should the return should be duals, _ = masterSolve(Patterns, rollData) then?

@frrad
Copy link
Contributor Author

frrad commented Mar 28, 2025

👋 thanks for all the reviews.

To clear up some possible confusion - this PR is intended to be the minimum possible change to add types to the examples/ directory. It is explicitly not trying to do things like fix types in pulp/ or improve the non-type code in examples. I think structuring things this way will lead to a change which is easy to review and a pareto improvement over the current situation. There will still definitely remain a lot of room to improve things afterwards, but by trying to be focused here I think we can hopefully get this one in without too much back and forth and make the next PR easier for whoever ends up making it.

Additional responses inline:

Thanks! I added minor questions about the renaming of some objects. I'm curious what tool renames objects based on their implied type. It sounds a bit weird.

This naming convention is in my opinion a bit outdated with modern typing. Instead of changing names to indicate type it would be better, as is being done in the linked PR, to indicate the type properly with typing.

Sorry for the confusion caused by my clunky variable names. They are not meant to convey the type, just fix the variable type change issues. These variables all actually typecheck fine without adding explicit hints.

re: changes to pulp

These are all great questions. The intention of this PR is not to fix any type problems in pulp/. I just added whatever type: ignore were necessary to allow me to remove these files from the mypy blacklist. I definitely agree that a lot more work is needed to actually fix the issues there.

@MBradbury
Copy link
Contributor

re: changes to pulp

These are all great questions. The intention of this PR is not to fix any type problems in pulp/. I just added whatever type: ignore were necessary to allow me to remove these files from the mypy blacklist. I definitely agree that a lot more work is needed to actually fix the issues there.

Would it be better to not add the # type: ignores then? By adding them potential bugs are covered up. IMO, better to let the tests fail and then fix the bug than to hide the issue.

@frrad
Copy link
Contributor Author

frrad commented Mar 28, 2025

In general unit tests / CI work best if you keep them in a state where you always expect them to pass. You want their binary pass / fail state to be a useful signal of whether or not a new change introduces new bugs. If you add tests which you expect to fail it can confuse things quite a bit.

The type ignore statements are a useful crutch to be able to make incremental improvements without having to fix everything at once. With their help we can do

  1. The current state mypy doesn't even consider these files and CI gives us no information about what changes do to typing in them
  2. After we add ignores readers of the code will at least be aware of existing type issues.
  3. Any future PRs which touch these files will fail CI if they introduce new type errors.
  4. New PRs can be made which fix the existing type issues and remove the ignore comments. Reviewers will be able to be confident in the correctness of these PRs because CI status will tell them whether or not mypy passes

@christiansegercrantz
Copy link
Contributor

Thanks! I added minor questions about the renaming of some objects. I'm curious what tool renames objects based on their implied type. It sounds a bit weird.

This naming convention is in my opinion a bit outdated with modern typing. Instead of changing names to indicate type it would be better, as is being done in the linked PR, to indicate the type properly with typing.

Sorry for the confusion caused by my clunky variable names. They are not meant to convey the type, just fix the variable type change issues. These variables all actually typecheck fine without adding explicit hints.

Makes sense, didn't read the code in larger context. Good change then, at some point those should probably have completely different names to convey their meaning better. But a good first step.

@frrad
Copy link
Contributor Author

frrad commented Apr 2, 2025

merged master to pick up #820 - looks like I have some work to do

@frrad
Copy link
Contributor Author

frrad commented Apr 2, 2025

That wasn't too bad at least 😅

@pchtsp I think I have addressed all your comments. Please let me know if there is anything more I can answer or change.

There are some references to these files in the documentation by line number like this:

.. literalinclude:: ../../../examples/WhiskasModel1.py
:lines: 7-8

which may need to be updated if things have shifted around. I'm happy to have a look at this as well as part of this PR. I want to wait until the actual code changes look good first though in case the lines end up moving around more.

@pchtsp
Copy link
Collaborator

pchtsp commented Apr 4, 2025

thanks @frrad . I don't mind if we do not have a perfect state now. Anything that gets as closer is good.
@christiansegercrantz , @MBradbury thanks for your help, if you can make any improvement on this on a separate PR (e.g., through #807) I can review it and merge it. Again, if we do it in pieces, we will probably finish sooner.

Thanks again

@pchtsp pchtsp merged commit ba0efaa into coin-or:master Apr 4, 2025
14 checks passed
frrad added a commit to frrad/pulp that referenced this pull request Apr 5, 2025
* initial pass

* black + isort

* add ignores in pulp.py and pulp/__init__.py

* start fixing mypy errors

* fix some mypy

* mypy passes

* move Pattern class definition to be before use
pchtsp pushed a commit that referenced this pull request Apr 9, 2025
* remove build from gitignore

* commit doc build output

* fix references

* remove compile output

* Type examples/ (#819)

* initial pass

* black + isort

* add ignores in pulp.py and pulp/__init__.py

* start fixing mypy errors

* fix some mypy

* mypy passes

* move Pattern class definition to be before use

* restore gitignore

* whiskas 1

* whiskas 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants