Skip to content

Commit c0fd062

Browse files
authored
Merge pull request #3088 from michaelbynum/ginac
Interface to GiNaC for Simplification
2 parents 12bcecf + 35b71f8 commit c0fd062

25 files changed

+1242
-45
lines changed

.github/workflows/test_branches.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ jobs:
181181
# Notes:
182182
# - install glpk
183183
# - pyodbc needs: gcc pkg-config unixodbc freetds
184-
for pkg in bash pkg-config unixodbc freetds glpk; do
184+
for pkg in bash pkg-config unixodbc freetds glpk ginac; do
185185
brew list $pkg || brew install $pkg
186186
done
187187
@@ -193,7 +193,8 @@ jobs:
193193
# - install glpk
194194
# - ipopt needs: libopenblas-dev gfortran liblapack-dev
195195
sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \
196-
install libopenblas-dev gfortran liblapack-dev glpk-utils
196+
install libopenblas-dev gfortran liblapack-dev glpk-utils \
197+
libginac-dev
197198
sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os
198199
199200
- name: Update Windows
@@ -264,7 +265,7 @@ jobs:
264265
if test -z "${{matrix.slim}}"; then
265266
python -m pip install --cache-dir cache/pip cplex docplex \
266267
|| echo "WARNING: CPLEX Community Edition is not available"
267-
python -m pip install --cache-dir cache/pip gurobipy==10.0.3\
268+
python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \
268269
|| echo "WARNING: Gurobi is not available"
269270
python -m pip install --cache-dir cache/pip xpress \
270271
|| echo "WARNING: Xpress Community Edition is not available"
@@ -339,7 +340,7 @@ jobs:
339340
echo "*** Install Pyomo dependencies ***"
340341
# Note: this will fail the build if any installation fails (or
341342
# possibly if it outputs messages to stderr)
342-
conda install --update-deps -y $CONDA_DEPENDENCIES
343+
conda install --update-deps -q -y $CONDA_DEPENDENCIES
343344
if test -z "${{matrix.slim}}"; then
344345
PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g')
345346
echo "Installing for $PYVER"

.github/workflows/test_pr_and_main.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ jobs:
218218
# Notes:
219219
# - install glpk
220220
# - pyodbc needs: gcc pkg-config unixodbc freetds
221-
for pkg in bash pkg-config unixodbc freetds glpk; do
221+
for pkg in bash pkg-config unixodbc freetds glpk ginac; do
222222
brew list $pkg || brew install $pkg
223223
done
224224
@@ -230,7 +230,8 @@ jobs:
230230
# - install glpk
231231
# - ipopt needs: libopenblas-dev gfortran liblapack-dev
232232
sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \
233-
install libopenblas-dev gfortran liblapack-dev glpk-utils
233+
install libopenblas-dev gfortran liblapack-dev glpk-utils \
234+
libginac-dev
234235
sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os
235236
236237
- name: Update Windows
@@ -372,6 +373,7 @@ jobs:
372373
CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG"
373374
fi
374375
done
376+
echo ""
375377
echo "*** Install Pyomo dependencies ***"
376378
# Note: this will fail the build if any installation fails (or
377379
# possibly if it outputs messages to stderr)

.jenkins.sh

+14-4
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ fi
4343
if test -z "$SLIM"; then
4444
export VENV_SYSTEM_PACKAGES='--system-site-packages'
4545
fi
46-
if test ! -z "$CATEGORY"; then
47-
export PY_CAT="-m $CATEGORY"
48-
fi
4946

5047
if test "$WORKSPACE" != "`pwd`"; then
5148
echo "ERROR: pwd is not WORKSPACE"
@@ -122,10 +119,23 @@ if test -z "$MODE" -o "$MODE" == setup; then
122119
echo "PYOMO_CONFIG_DIR=$PYOMO_CONFIG_DIR"
123120
echo ""
124121

122+
# Call Pyomo build scripts to build TPLs that would normally be
123+
# skipped by the pyomo download-extensions / build-extensions
124+
# actions below
125+
if [[ " $CATEGORY " == *" builders "* ]]; then
126+
echo ""
127+
echo "Running local build scripts..."
128+
echo ""
129+
set -x
130+
python pyomo/contrib/simplification/build.py --build-deps || exit 1
131+
set +x
132+
fi
133+
125134
# Use Pyomo to download & compile binary extensions
126135
i=0
127136
while /bin/true; do
128137
i=$[$i+1]
138+
echo ""
129139
echo "Downloading pyomo extensions (attempt $i)"
130140
pyomo download-extensions $PYOMO_DOWNLOAD_ARGS
131141
if test $? == 0; then
@@ -178,7 +188,7 @@ if test -z "$MODE" -o "$MODE" == test; then
178188
python -m pytest -v \
179189
-W ignore::Warning \
180190
--junitxml="TEST-pyomo.xml" \
181-
$PY_CAT $TEST_SUITES $PYTEST_EXTRA_ARGS
191+
-m "$CATEGORY" $TEST_SUITES $PYTEST_EXTRA_ARGS
182192

183193
# Combine the coverage results and upload
184194
if test -z "$DISABLE_COVERAGE"; then

conftest.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@
1111

1212
import pytest
1313

14+
_implicit_markers = {'default'}
15+
_extended_implicit_markers = _implicit_markers.union({'solver'})
16+
17+
18+
def pytest_collection_modifyitems(items):
19+
"""
20+
This method will mark any unmarked tests with the implicit marker ('default')
21+
22+
"""
23+
for item in items:
24+
try:
25+
next(item.iter_markers())
26+
except StopIteration:
27+
for marker in _implicit_markers:
28+
item.add_marker(getattr(pytest.mark, marker))
29+
1430

1531
def pytest_runtest_setup(item):
1632
"""
@@ -32,23 +48,20 @@ def pytest_runtest_setup(item):
3248
the default mode; but if solver tests are also marked with an explicit
3349
category (e.g., "expensive"), we will skip them.
3450
"""
35-
marker = item.iter_markers()
3651
solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")]
3752
solveroption = item.config.getoption("--solver")
3853
markeroption = item.config.getoption("-m")
39-
implicit_markers = ['default']
40-
extended_implicit_markers = implicit_markers + ['solver']
41-
item_markers = set(mark.name for mark in marker)
54+
item_markers = set(mark.name for mark in item.iter_markers())
4255
if solveroption:
4356
if solveroption not in solvernames:
4457
pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption))
4558
return
4659
elif markeroption:
4760
return
4861
elif item_markers:
49-
if not set(implicit_markers).issubset(
50-
item_markers
51-
) and not item_markers.issubset(set(extended_implicit_markers)):
62+
if not _implicit_markers.issubset(item_markers) and not item_markers.issubset(
63+
_extended_implicit_markers
64+
):
5265
pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.')
5366

5467

pyomo/common/download.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
urllib_error = attempt_import('urllib.error')[0]
3030
ssl = attempt_import('ssl')[0]
3131
zipfile = attempt_import('zipfile')[0]
32+
tarfile = attempt_import('tarfile')[0]
3233
gzip = attempt_import('gzip')[0]
3334
distro, distro_available = attempt_import('distro')
3435

@@ -371,7 +372,7 @@ def get_zip_archive(self, url, dirOffset=0):
371372
# Simple sanity checks
372373
for info in zip_file.infolist():
373374
f = info.filename
374-
if f[0] in '\\/' or '..' in f:
375+
if f[0] in '\\/' or '..' in f or os.path.isabs(f):
375376
logger.error(
376377
"malformed (potentially insecure) filename (%s) "
377378
"found in zip archive. Skipping file." % (f,)
@@ -387,6 +388,61 @@ def get_zip_archive(self, url, dirOffset=0):
387388
info.filename = target[-1] + '/' if f[-1] == '/' else target[-1]
388389
zip_file.extract(f, os.path.join(self._fname, *tuple(target[dirOffset:-1])))
389390

391+
def get_tar_archive(self, url, dirOffset=0):
392+
if self._fname is None:
393+
raise DeveloperError(
394+
"target file name has not been initialized "
395+
"with set_destination_filename"
396+
)
397+
if os.path.exists(self._fname) and not os.path.isdir(self._fname):
398+
raise RuntimeError(
399+
"Target directory (%s) exists, but is not a directory" % (self._fname,)
400+
)
401+
402+
def filter_fcn(info):
403+
# this mocks up the `tarfile` filter introduced in Python
404+
# 3.12 and backported to later releases of Python (e.g.,
405+
# 3.8.17, 3.9.17, 3.10.12, and 3.11.4)
406+
f = info.name
407+
if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)):
408+
logger.error(
409+
"malformed or potentially insecure filename (%s). "
410+
"Skipping file." % (f,)
411+
)
412+
return False
413+
target = self._splitpath(f)
414+
if len(target) <= dirOffset:
415+
if not info.isdir():
416+
logger.warning(
417+
"Skipping file (%s) in tar archive due to dirOffset." % (f,)
418+
)
419+
return False
420+
info.name = f = '/'.join(target[dirOffset:])
421+
target = os.path.realpath(os.path.join(dest, f))
422+
try:
423+
if os.path.commonpath([target, dest]) != dest:
424+
logger.error(
425+
"potentially insecure filename (%s) resolves outside target "
426+
"directory. Skipping file." % (f,)
427+
)
428+
return False
429+
except ValueError:
430+
# commonpath() will raise ValueError for paths that
431+
# don't have anything in common (notably, when files are
432+
# on different drives on Windows)
433+
logger.error(
434+
"potentially insecure filename (%s) resolves outside target "
435+
"directory. Skipping file." % (f,)
436+
)
437+
return False
438+
# Strip high bits & group/other write bits
439+
info.mode &= 0o755
440+
return True
441+
442+
with tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) as TAR:
443+
dest = os.path.realpath(self._fname)
444+
TAR.extractall(dest, filter(filter_fcn, TAR.getmembers()))
445+
390446
def get_gzipped_binary_file(self, url):
391447
if self._fname is None:
392448
raise DeveloperError(

pyomo/common/enums.py

+16-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
.. autosummary::
1818
1919
ExtendedEnumType
20+
NamedIntEnum
2021
2122
Standard Enums:
2223
@@ -130,7 +131,21 @@ def __new__(metacls, cls, bases, classdict, **kwds):
130131
return super().__new__(metacls, cls, bases, classdict, **kwds)
131132

132133

133-
class ObjectiveSense(enum.IntEnum):
134+
class NamedIntEnum(enum.IntEnum):
135+
"""An extended version of :py:class:`enum.IntEnum` that supports
136+
creating members by name as well as value.
137+
138+
"""
139+
140+
@classmethod
141+
def _missing_(cls, value):
142+
for member in cls:
143+
if member.name == value:
144+
return member
145+
return None
146+
147+
148+
class ObjectiveSense(NamedIntEnum):
134149
"""Flag indicating if an objective is minimizing (1) or maximizing (-1).
135150
136151
While the numeric values are arbitrary, there are parts of Pyomo
@@ -150,13 +165,6 @@ class ObjectiveSense(enum.IntEnum):
150165
def __str__(self):
151166
return self.name
152167

153-
@classmethod
154-
def _missing_(cls, value):
155-
for member in cls:
156-
if member.name == value:
157-
return member
158-
return None
159-
160168

161169
minimize = ObjectiveSense.minimize
162170
maximize = ObjectiveSense.maximize

pyomo/common/fileutils.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import os
3939
import platform
4040
import importlib.util
41+
import subprocess
4142
import sys
4243

4344
from . import envvar
@@ -375,9 +376,27 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None):
375376
if libname_base.startswith('lib') and _system() != 'windows':
376377
libname_base = libname_base[3:]
377378
if ext.lower().startswith(('.so', '.dll', '.dylib')):
378-
return ctypes.util.find_library(libname_base)
379+
lib = ctypes.util.find_library(libname_base)
379380
else:
380-
return ctypes.util.find_library(libname)
381+
lib = ctypes.util.find_library(libname)
382+
if lib and os.path.sep not in lib:
383+
# work around https://github.com/python/cpython/issues/65241,
384+
# where python does not return the absolute path on *nix
385+
try:
386+
libname = lib + ' '
387+
with subprocess.Popen(
388+
['/sbin/ldconfig', '-p'],
389+
stdin=subprocess.DEVNULL,
390+
stderr=subprocess.DEVNULL,
391+
stdout=subprocess.PIPE,
392+
env={'LC_ALL': 'C', 'LANG': 'C'},
393+
) as p:
394+
for line in os.fsdecode(p.stdout.read()).splitlines():
395+
if line.lstrip().startswith(libname):
396+
return os.path.realpath(line.split()[-1])
397+
except:
398+
pass
399+
return lib
381400

382401

383402
def find_executable(exename, cwd=True, include_PATH=True, pathlist=None):

0 commit comments

Comments
 (0)