From 4ae3c859525a9030ddb2feab2ebffe4a7c5b5c69 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 12 Aug 2024 16:28:21 -0400 Subject: [PATCH 1/2] fix: fix recursion limit --- ilpy/expressions.py | 40 +++++++++++++++++++++++++++++++-------- tests/test_expressions.py | 14 ++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/ilpy/expressions.py b/ilpy/expressions.py index 9b44da6..c1e3b69 100644 --- a/ilpy/expressions.py +++ b/ilpy/expressions.py @@ -1,13 +1,26 @@ from __future__ import annotations import ast -from typing import Any, ClassVar, Sequence, Union +import sys +from contextlib import contextmanager +from typing import Any, ClassVar, Iterator, Sequence, Union from ilpy.wrapper import Constraint, Objective, Relation, Sense Number = Union[float, int] +@contextmanager +def recursion_limit_raised_by(N: int = 5000) -> Iterator[None]: + """Temporarily increase the recursion limit by N.""" + old_limit = sys.getrecursionlimit() + sys.setrecursionlimit(old_limit + N) + try: + yield + finally: + sys.setrecursionlimit(old_limit) + + class Expression(ast.AST): """Base class for all expression nodes. @@ -265,13 +278,24 @@ def _get_coeff_indices( l_coeffs: dict[int, float] = {} q_coeffs: dict[tuple[int, int], float] = {} constant = 0.0 - for var, coefficient in _get_coefficients(expr).items(): - if var is None: - constant = coefficient - elif isinstance(var, tuple): - q_coeffs[(_ensure_index(var[0]), _ensure_index(var[1]))] = coefficient - elif coefficient != 0: - l_coeffs[_ensure_index(var)] = coefficient + try: + with recursion_limit_raised_by(5000): + for var, coefficient in _get_coefficients(expr).items(): + if var is None: + constant = coefficient + elif isinstance(var, tuple): + q_coeffs[(_ensure_index(var[0]), _ensure_index(var[1]))] = ( + coefficient + ) + elif coefficient != 0: + l_coeffs[_ensure_index(var)] = coefficient + except RecursionError as e: + raise RecursionError( + "RecursionError when casting an ilpy.Expression to a Constraint or " + "Objective. If you really want an expression this large, you may raise the " + "limit temporarily with `ilpy.expressions.recursion_limit_raised_by` (or " + "manually with `sys.setrecursionlimit`)" + ) from e return l_coeffs, q_coeffs, constant diff --git a/tests/test_expressions.py b/tests/test_expressions.py index b0c582a..bcb9959 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -1,4 +1,5 @@ import operator +import sys import pytest from ilpy.expressions import Expression, Variable, _get_coefficients @@ -161,3 +162,16 @@ def test_adding() -> None: solver.set_objective(u) solver.add_constraint(u >= 0) solver.set_constraints(constraints) + + +def test_recursion() -> None: + from ilpy.expressions import recursion_limit_raised_by + + reclimit = sys.getrecursionlimit() + s = sum(Variable(str(x), index=x) for x in range(reclimit + 5001)) + SOME_MAX = 1000 + expr = s <= SOME_MAX + with pytest.raises(RecursionError): + expr.as_constraint() + with recursion_limit_raised_by(): + assert isinstance(expr.as_constraint(), Constraint) From 4a4a1e89c288ec356fc739af434ba539f5f48386 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 25 Oct 2024 11:55:28 +0200 Subject: [PATCH 2/2] skip on win 3.10 --- tests/test_expressions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index bcb9959..7aed0b4 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -1,4 +1,5 @@ import operator +import os import sys import pytest @@ -164,6 +165,10 @@ def test_adding() -> None: solver.set_constraints(constraints) +@pytest.mark.skipif( + sys.version_info < (3, 11) and os.name == "nt", + reason="fails too often on windows 3.10", +) def test_recursion() -> None: from ilpy.expressions import recursion_limit_raised_by