Skip to content

Commit ef6cf7b

Browse files
authored
Merge branch 'main' into main
2 parents 622715c + 08b74aa commit ef6cf7b

File tree

20 files changed

+612
-138
lines changed

20 files changed

+612
-138
lines changed

.github/workflows/test_branches.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ env:
2323
PYTHON_CORE_PKGS: wheel
2424
PYPI_ONLY: z3-solver linear-tree
2525
PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree
26-
CACHE_VER: v221013.1
26+
CACHE_VER: v250820.0
2727
NEOS_EMAIL: tests@pyomo.org
2828
SRC_REF: ${{ github.head_ref || github.ref }}
2929
PYOMO_WORKFLOW: branch
@@ -147,7 +147,6 @@ jobs:
147147
else
148148
echo "GHA_JOBGROUP=other" >> $GITHUB_ENV
149149
fi
150-
# Note: pandas 1.0.3 causes gams 29.1.0 import to fail in python 3.8
151150
EXTRAS=tests
152151
if test -z "${{matrix.slim}}"; then
153152
EXTRAS="$EXTRAS,docs,optional"
@@ -179,7 +178,7 @@ jobs:
179178
id: download-cache
180179
with:
181180
path: cache/download
182-
key: download-${{env.CACHE_VER}}.0-${{runner.os}}
181+
key: download-${{env.CACHE_VER}}-${{runner.os}}
183182

184183
- name: Configure curl
185184
run: |
@@ -540,7 +539,7 @@ jobs:
540539
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
541540
$INSTALLER = "${env:DOWNLOAD_DIR}/gams_install.exe"
542541
# Demo licenses are included for 5mo from the newest release
543-
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/50.1.0"
542+
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/latest"
544543
if ( "${{matrix.TARGET}}" -eq "win" ) {
545544
$URL = "$URL/windows/windows_x64_64.exe"
546545
} elseif ( "${{matrix.TARGET}}" -eq "osx" ) {

.github/workflows/test_pr_and_main.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ env:
3131
PYTHON_CORE_PKGS: wheel
3232
PYPI_ONLY: z3-solver linear-tree
3333
PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree
34-
CACHE_VER: v221013.1
34+
CACHE_VER: v250820.0
3535
NEOS_EMAIL: tests@pyomo.org
3636
SRC_REF: ${{ github.head_ref || github.ref }}
3737
PYOMO_WORKFLOW: |
@@ -199,7 +199,6 @@ jobs:
199199
else
200200
echo "GHA_JOBGROUP=other" >> $GITHUB_ENV
201201
fi
202-
# Note: pandas 1.0.3 causes gams 29.1.0 import to fail in python 3.8
203202
EXTRAS=tests
204203
if test -z "${{matrix.slim}}"; then
205204
EXTRAS="$EXTRAS,docs,optional"
@@ -231,7 +230,7 @@ jobs:
231230
id: download-cache
232231
with:
233232
path: cache/download
234-
key: download-${{env.CACHE_VER}}.0-${{runner.os}}
233+
key: download-${{env.CACHE_VER}}-${{runner.os}}
235234

236235
- name: Configure curl
237236
run: |
@@ -592,7 +591,7 @@ jobs:
592591
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
593592
$INSTALLER = "${env:DOWNLOAD_DIR}/gams_install.exe"
594593
# Demo licenses are included for 5mo from the newest release
595-
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/50.1.0"
594+
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/latest"
596595
if ( "${{matrix.TARGET}}" -eq "win" ) {
597596
$URL = "$URL/windows/windows_x64_64.exe"
598597
} elseif ( "${{matrix.TARGET}}" -eq "osx" ) {

pyomo/common/enums.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
.. autosummary::
2525
2626
ObjectiveSense
27+
SolverAPIVersion
2728
2829
"""
2930

@@ -213,5 +214,22 @@ def __str__(self):
213214
return self.name
214215

215216

217+
class SolverAPIVersion(NamedIntEnum):
218+
"""
219+
Enum identifying Pyomo solver API version
220+
221+
The numeric values are intentionally a bit odd because APPSI came
222+
between the official V1 and V2. We still want it to be chronologically
223+
in order without sacrificing the human-logic of v1 vs. v2.
224+
"""
225+
226+
#: Original Coopr/Pyomo solver interface
227+
V1 = 10
228+
#: Automatic Persistent Pyomo Solver Interface (experimental)
229+
APPSI = 15
230+
#: Redesigned solver interface (circa 2024)
231+
V2 = 20
232+
233+
216234
minimize = ObjectiveSense.minimize
217235
maximize = ObjectiveSense.maximize

pyomo/common/envvar.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,39 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12+
"""Attributes describing the current platform and user configuration.
13+
14+
This module provides standardized attributes that other parts of Pyomo
15+
can use to interrogate aspects of the current platform, and to find
16+
information about the current user configuration (including where to
17+
locate the main Pyomo configuration directory).
18+
19+
"""
20+
1221
import os
1322
import platform
1423

24+
_platform = platform.system().lower()
25+
#: bool : True if running in a "native" Windows environment
26+
is_native_windows = _platform.startswith('windows')
27+
#: bool : True if running on Windows (native or cygwin)
28+
is_windows = is_native_windows or _platform.startswith('cygwin')
29+
#: bool : True if running on Mac/OSX
30+
is_osx = _platform.startswith('darwin')
31+
#: bool: True if running under the PyPy interpreter
32+
is_pypy = platform.python_implementation().lower().startswith('pypy')
33+
34+
#: str : Absolute path to the user's Pyomo Configuration Directory.
35+
#:
36+
#: By default, this is ``~/.pyomo`` on Linux and OSX and
37+
#: ``%LOCALAPPDATA%/Pyomo`` on Windows. It can be overridden by
38+
#: setting the ``PYOMO_CONFIG_DIR`` environment variable before
39+
#: importing Pyomo.
40+
PYOMO_CONFIG_DIR = None
41+
1542
if 'PYOMO_CONFIG_DIR' in os.environ:
1643
PYOMO_CONFIG_DIR = os.path.abspath(os.environ['PYOMO_CONFIG_DIR'])
17-
elif platform.system().lower().startswith(('windows', 'cygwin')):
44+
elif is_windows:
1845
PYOMO_CONFIG_DIR = os.path.abspath(
1946
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Pyomo')
2047
)

pyomo/common/fileutils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,50 @@ def import_file(path, clear_cache=False, infer_package=True, module_name=None):
505505
return module
506506

507507

508+
def to_legal_filename(name, universal=False) -> str:
509+
"""Convert a string to a legal filename on the current platform.
510+
511+
This converts a candidate file name (not a path) and converts it to
512+
a legal file name on the current platform. This includes replacing
513+
any unallowable characters (including the path separator) with
514+
underscores (``_``), and on some platforms, enforcing restrictions
515+
on the allowable final character.
516+
517+
Parameters
518+
----------
519+
name : str
520+
521+
The original (desired) file name
522+
523+
universal : bool
524+
525+
If True, this will attempt a form of "universal" standardization
526+
that uses the most restrictive set of character translations and
527+
rules. Currently, ``universal=True`` is equivalent to running
528+
the Windows translations.
529+
530+
"""
531+
if envvar.is_windows or universal:
532+
tr = getattr(to_legal_filename, 'tr', None)
533+
if tr is None:
534+
# Windows illegal characters: 0-31, plus < > : " / \ | ? *
535+
_illegal = r'<>:"/\|?*' + ''.join(map(chr, range(32)))
536+
tr = to_legal_filename.tr = str.maketrans(_illegal, '_' * len(_illegal))
537+
# Remove illegal characters
538+
name = name.translate(tr)
539+
if name:
540+
# Windows allows filenames to end with space or dot, but the
541+
# file explorer can't interact with them
542+
if name[-1] in ' .':
543+
name = name[:-1] + '_'
544+
# Similarly, starting with a space is generally a bad idea
545+
if name[0] == ' ':
546+
name = '_' + name[1:]
547+
else:
548+
name = name.replace('/', '_').replace(chr(0), '_')
549+
return name
550+
551+
508552
class PathData(object):
509553
"""An object for storing and managing a :py:class:`PathManager` path"""
510554

pyomo/common/tee.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,8 @@ def open(self, mode="w", buffering=-1, encoding=None, newline=None):
765765
# Note that is it VERY important to close file handles in the
766766
# same thread that opens it. If you don't you can see deadlocks
767767
# and a peculiar error ("libgcc_s.so.1 must be installed for
768-
# pthread_cancel to work"; see https://bugs.python.org/issue18748)
768+
# pthread_cancel to work"; see
769+
# https://github.com/python/cpython/issues/62948)
769770
#
770771
# To accomplish this, we will keep two handle lists: one is the
771772
# set of "active" handles that the (merged reader) thread is

pyomo/common/tests/test_enums.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import pyomo.common.unittest as unittest
1515

16-
from pyomo.common.enums import ExtendedEnumType, ObjectiveSense
16+
from pyomo.common.enums import ExtendedEnumType, ObjectiveSense, SolverAPIVersion
1717

1818

1919
class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType):
@@ -95,3 +95,25 @@ def test_call(self):
9595
def test_str(self):
9696
self.assertEqual(str(ObjectiveSense.minimize), 'minimize')
9797
self.assertEqual(str(ObjectiveSense.maximize), 'maximize')
98+
99+
100+
class TestSolverAPIVersion(unittest.TestCase):
101+
def test_members(self):
102+
self.assertEqual(
103+
list(SolverAPIVersion),
104+
[SolverAPIVersion.V1, SolverAPIVersion.APPSI, SolverAPIVersion.V2],
105+
)
106+
107+
def test_call(self):
108+
self.assertIs(SolverAPIVersion(10), SolverAPIVersion.V1)
109+
self.assertIs(SolverAPIVersion(15), SolverAPIVersion.APPSI)
110+
self.assertIs(SolverAPIVersion(20), SolverAPIVersion.V2)
111+
112+
self.assertIs(SolverAPIVersion('V1'), SolverAPIVersion.V1)
113+
self.assertIs(SolverAPIVersion('APPSI'), SolverAPIVersion.APPSI)
114+
self.assertIs(SolverAPIVersion('V2'), SolverAPIVersion.V2)
115+
116+
with self.assertRaisesRegex(
117+
ValueError, "'foo' is not a valid SolverAPIVersion"
118+
):
119+
SolverAPIVersion('foo')

pyomo/common/tests/test_fileutils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
_libExt,
3939
ExecutableData,
4040
import_file,
41+
to_legal_filename,
4142
)
4243
from pyomo.common.download import FileDownloader
4344

@@ -497,3 +498,35 @@ def test_PathManager(self):
497498
Executable(f_in_path2).rehash()
498499
self.assertTrue(Executable(f_in_path2).available())
499500
self.assertEqual(Executable(f_in_path2).path(), f_loc)
501+
502+
def test_to_legal_filename(self):
503+
self.assertEqual('abc', to_legal_filename('abc'))
504+
self.assertEqual('', to_legal_filename(''))
505+
if envvar.is_windows:
506+
self.assertEqual('_abc', to_legal_filename(' abc'))
507+
self.assertEqual('abc_', to_legal_filename('abc.'))
508+
self.assertEqual('abc_', to_legal_filename('abc '))
509+
self.assertEqual('abc_def', to_legal_filename('abc/def'))
510+
self.assertEqual('abc_def', to_legal_filename('abc\\def'))
511+
self.assertEqual(
512+
'a_b_c', to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c']))
513+
)
514+
else:
515+
self.assertEqual(' abc', to_legal_filename(' abc'))
516+
self.assertEqual('abc.', to_legal_filename('abc.'))
517+
self.assertEqual('abc ', to_legal_filename('abc '))
518+
self.assertEqual('abc_def', to_legal_filename('abc/def'))
519+
self.assertEqual('abc\\def', to_legal_filename('abc\\def'))
520+
self.assertEqual(
521+
'a_b' + chr(7) + 'c',
522+
to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c'])),
523+
)
524+
525+
self.assertEqual('_abc', to_legal_filename(' abc', True))
526+
self.assertEqual('abc_', to_legal_filename('abc.', True))
527+
self.assertEqual('abc_', to_legal_filename('abc ', True))
528+
self.assertEqual('abc_def', to_legal_filename('abc/def', True))
529+
self.assertEqual('abc_def', to_legal_filename('abc\\def', True))
530+
self.assertEqual(
531+
'a_b_c', to_legal_filename(''.join(['a', chr(0), 'b', chr(7), 'c']), True)
532+
)

pyomo/contrib/appsi/base.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat
3030
from pyomo.common.errors import ApplicationError
31-
from pyomo.common.enums import IntEnum
31+
from pyomo.common.enums import IntEnum, SolverAPIVersion
3232
from pyomo.common.factory import Factory
3333
from pyomo.common.timing import HierarchicalTimer
3434
from pyomo.core.base.constraint import ConstraintData, Constraint
@@ -635,6 +635,18 @@ def __str__(self):
635635
# preserve the previous behavior
636636
return self.name
637637

638+
@classmethod
639+
def api_version(self):
640+
"""
641+
Return the public API supported by this interface.
642+
643+
Returns
644+
-------
645+
~pyomo.common.enums.SolverAPIVersion
646+
A solver API enum object
647+
"""
648+
return SolverAPIVersion.APPSI
649+
638650
@abc.abstractmethod
639651
def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results:
640652
"""

pyomo/contrib/solver/common/base.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pyomo.core.base.block import BlockData
1919
from pyomo.core.base.objective import Objective, ObjectiveData
2020
from pyomo.common.config import ConfigValue
21-
from pyomo.common.enums import IntEnum
21+
from pyomo.common.enums import IntEnum, SolverAPIVersion
2222
from pyomo.common.errors import ApplicationError
2323
from pyomo.common.deprecation import deprecation_warning
2424
from pyomo.common.modeling import NOTSET
@@ -105,6 +105,18 @@ def __enter__(self):
105105
def __exit__(self, exc_type, exc_value, exc_traceback):
106106
"""Exit statement - enables `with` statements."""
107107

108+
@classmethod
109+
def api_version(self):
110+
"""
111+
Return the public API supported by this interface.
112+
113+
Returns
114+
-------
115+
~pyomo.common.enums.SolverAPIVersion
116+
A solver API enum object
117+
"""
118+
return SolverAPIVersion.V2
119+
108120
def solve(self, model: BlockData, **kwargs) -> Results:
109121
"""Solve a Pyomo model.
110122

0 commit comments

Comments
 (0)