Skip to content

Commit c35d42d

Browse files
authored
Merge pull request #472 from stan-dev/issue/401-cmdstan-version-at
logic fix for cmdstan_version_at, get_latest_cmdstan
2 parents acab458 + 2b8bd60 commit c35d42d

6 files changed

Lines changed: 143 additions & 72 deletions

File tree

cmdstanpy/cmdstan_args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from cmdstanpy import _TMPDIR
1414
from cmdstanpy.utils import (
1515
cmdstan_path,
16-
cmdstan_version_at,
16+
cmdstan_version_before,
1717
create_named_text_file,
1818
get_logger,
1919
read_metric,
@@ -818,7 +818,7 @@ def validate(self) -> None:
818818
'Argument "sig_figs" must be an integer between 1 and 18,'
819819
' found {}'.format(self.sig_figs)
820820
)
821-
if not cmdstan_version_at(2, 25):
821+
if cmdstan_version_before(2, 25):
822822
self.sig_figs = None
823823
get_logger().warning(
824824
'Argument "sig_figs" invalid for CmdStan versions < 2.25, '

cmdstanpy/stanfit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
EXTENSION,
4545
check_sampler_csv,
4646
cmdstan_path,
47-
cmdstan_version_at,
47+
cmdstan_version_before,
4848
create_named_text_file,
4949
do_command,
5050
flatten_chains,
@@ -879,7 +879,7 @@ def summary(
879879
dir=_TMPDIR, prefix=tmp_csv_file, suffix='.csv', name_only=True
880880
)
881881
csv_str = '--csv_filename={}'.format(tmp_csv_path)
882-
if not cmdstan_version_at(2, 24):
882+
if cmdstan_version_before(2, 24):
883883
csv_str = '--csv_file={}'.format(tmp_csv_path)
884884
cmd = [
885885
cmd_path,

cmdstanpy/utils.py

Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -84,38 +84,43 @@ def get_latest_cmdstan(cmdstan_dir: str) -> Optional[str]:
8484
"""
8585
Given a valid directory path, find all installed CmdStan versions
8686
and return highest (i.e., latest) version number.
87-
Assumes directory populated via `install_cmdstan`.
87+
88+
Assumes directory consists of CmdStan releases, created by
89+
function `install_cmdstan`, and therefore dirnames have format
90+
"cmdstan-<maj>.<min>.<patch>" or "cmdstan-<maj>.<min>.<patch>-rc<num>",
91+
which is CmdStan release practice as of v 2.24.
8892
"""
8993
versions = [
90-
''.join(name.split('-')[1:]) # name may contain '-rc'
94+
name[8:]
9195
for name in os.listdir(cmdstan_dir)
9296
if os.path.isdir(os.path.join(cmdstan_dir, name))
9397
and name.startswith('cmdstan-')
9498
and name[8].isdigit()
99+
and len(name[8:].split('.')) == 3
95100
]
96-
# munge rc for sort, e.g. 2.25.0-rc1 -> 2.25.0.-99
101+
if len(versions) == 0:
102+
return None
103+
# munge rc for sort, e.g. 2.25.0-rc1 -> 2.25.-99
97104
for i in range(len(versions)): # # pylint: disable=C0200
98-
tmp = versions[i].split('rc')
99-
if len(tmp) == 1:
100-
versions[i] = '.'.join([tmp[0], '0'])
101-
else:
102-
rc_sortable = str(int(tmp[1]) - 100)
103-
versions[i] = '.'.join([tmp[0], rc_sortable])
105+
if '-rc' in versions[i]:
106+
comps = versions[i].split('-rc')
107+
mmp = comps[0].split('.')
108+
rc_num = comps[1]
109+
patch = str(int(rc_num) - 100)
110+
versions[i] = '.'.join([mmp[0], mmp[1], patch])
104111

105112
versions.sort(key=lambda s: list(map(int, s.split('.'))))
106-
if len(versions) == 0:
107-
return None
108-
latest = 'cmdstan-{}'.format(versions[len(versions) - 1])
113+
latest = versions[len(versions) - 1]
109114

110-
# unmunge
111-
tmp = latest.split('.')
112-
prefix = '.'.join(tmp[0:3])
113-
if int(tmp[3]) == 0:
114-
latest = prefix
115-
else:
116-
tmp[3] = 'rc' + str(int(tmp[3]) + 100)
117-
latest = '-'.join([prefix, tmp[3]])
118-
return latest
115+
# unmunge as needed
116+
mmp = latest.split('.')
117+
if int(mmp[2]) < 0:
118+
print("here")
119+
rc_num = str(int(mmp[2]) + 100)
120+
mmp[2] = "0-rc" + rc_num
121+
latest = '.'.join(mmp)
122+
123+
return 'cmdstan-' + latest
119124

120125

121126
def validate_cmdstan_path(path: str) -> None:
@@ -177,48 +182,75 @@ def cmdstan_path() -> str:
177182
return cmdstan
178183

179184

180-
def cmdstan_version_at(maj: int, min: int) -> bool:
185+
def cmdstan_version() -> Optional[Tuple[int, ...]]:
181186
"""
182-
Check that CmdStan version is at or above Maj.min version.
183-
Parses version string out of CmdStan makefile in CmdStan path dir.
187+
Parses version string out of CmdStan makefile variable CMDSTAN_VERSION,
188+
returns Tuple(Major, minor).
184189
185-
:param maj: Major version number
186-
:param min: Minor version number
187-
188-
:return: True if version at or above, else False
190+
If CmdStan installation is not found or cannot parse version from makefile
191+
logs warning and returns None. Lenient behavoir required for CI tests,
192+
per comment:
193+
https://github.com/stan-dev/cmdstanpy/pull/321#issuecomment-733817554
189194
"""
190-
# pylint:disable=bare-except
191195
try:
192-
path = cmdstan_path()
193-
makefile = os.path.join(path, 'makefile')
194-
if not os.path.exists(makefile):
195-
raise ValueError(
196-
'CmdStan installation {}: missing makefile'.format(path)
197-
)
198-
version = None
199-
with open(makefile, 'r') as fd:
200-
contents = fd.read()
201-
start_idx = contents.find('CMDSTAN_VERSION := ') + len(
202-
'CMDSTAN_VERSION := '
203-
)
204-
end_idx = contents.find('\n', start_idx)
205-
version = contents[start_idx:end_idx]
206-
if version is None:
207-
raise ValueError(
208-
'Cannot parse version from makefile: {}'.format(makefile)
209-
)
210-
splits = version.split('.')
211-
if len(splits) < 2:
212-
raise ValueError(
213-
'Cannot parse version from makefile: {}'.format(makefile)
214-
)
215-
cur_maj = int(splits[0])
216-
cur_min = int(splits[1])
196+
makefile = os.path.join(cmdstan_path(), 'makefile')
197+
except ValueError:
198+
get_logger().info('No CmdStan installation found.')
199+
return None
217200

218-
if cur_maj > maj or (cur_maj == maj and cur_min >= min):
219-
return True
220-
except: # noqa
221-
pass
201+
if not os.path.exists(makefile):
202+
get_logger().info(
203+
'CmdStan installation %s missing makefile, cannot get version.',
204+
cmdstan_path(),
205+
)
206+
return None
207+
208+
with open(makefile, 'r') as fd:
209+
contents = fd.read()
210+
211+
start_idx = contents.find('CMDSTAN_VERSION := ')
212+
if start_idx < 0:
213+
get_logger().info(
214+
'Cannot parse version from makefile: %s.',
215+
makefile,
216+
)
217+
return None
218+
219+
start_idx += len('CMDSTAN_VERSION := ')
220+
end_idx = contents.find('\n', start_idx)
221+
222+
version = contents[start_idx:end_idx]
223+
splits = version.split('.')
224+
if len(splits) != 3:
225+
get_logger().info(
226+
'Cannot parse version, expected "<major>.<minor>.<patch>", '
227+
'found: "%s".',
228+
version,
229+
)
230+
return None
231+
return tuple(int(x) for x in splits[0:2])
232+
233+
234+
def cmdstan_version_before(major: int, minor: int) -> bool:
235+
"""
236+
Check that CmdStan version is less than Major.minor version.
237+
238+
:param major: Major version number
239+
:param minor: Minor version number
240+
241+
242+
:return: True if version at or above major.minor, else False.
243+
"""
244+
cur_version = cmdstan_version()
245+
if cur_version is None:
246+
get_logger().info(
247+
'Cannot determine whether version is before %d.%d.', major, minor
248+
)
249+
return False
250+
if cur_version[0] < major or (
251+
cur_version[0] == major and cur_version[1] < minor
252+
):
253+
return True
222254
return False
223255

224256

test/test_cmdstan_args.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
SamplerArgs,
1818
VariationalArgs,
1919
)
20-
from cmdstanpy.utils import cmdstan_version_at
20+
from cmdstanpy.utils import cmdstan_version_before
2121

2222
HERE = os.path.dirname(os.path.abspath(__file__))
2323
DATAFILES_PATH = os.path.join(HERE, 'data')
@@ -576,7 +576,7 @@ def test_args_bad(self):
576576
def test_args_sig_figs(self):
577577
sampler_args = SamplerArgs()
578578
cmdstan_path() # sets os.environ['CMDSTAN']
579-
if not cmdstan_version_at(2, 25):
579+
if cmdstan_version_before(2, 25):
580580
with LogCapture() as log:
581581
logging.getLogger()
582582
CmdStanArgs(
@@ -588,7 +588,7 @@ def test_args_sig_figs(self):
588588
)
589589
expect = (
590590
'Argument "sig_figs" invalid for CmdStan versions < 2.25, '
591-
'using verson {} in directory {}'
591+
'using version {} in directory {}'
592592
).format(
593593
os.path.basename(cmdstan_path()),
594594
os.path.dirname(cmdstan_path()),

test/test_sample.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from cmdstanpy.cmdstan_args import CmdStanArgs, Method, SamplerArgs
2828
from cmdstanpy.model import CmdStanModel
2929
from cmdstanpy.stanfit import CmdStanMCMC, RunSet, from_csv
30-
from cmdstanpy.utils import EXTENSION, cmdstan_version_at
30+
from cmdstanpy.utils import EXTENSION, cmdstan_version_before
3131

3232
HERE = os.path.dirname(os.path.abspath(__file__))
3333
DATAFILES_PATH = os.path.join(HERE, 'data')
@@ -557,7 +557,7 @@ def test_bernoulli_path_with_space(self):
557557
)
558558

559559
def test_index_bounds_error(self):
560-
if cmdstan_version_at(2, 25) or cmdstan_version_at(2, 26):
560+
if not cmdstan_version_before(2, 27):
561561
oob_stan = os.path.join(DATAFILES_PATH, 'out_of_bounds.stan')
562562
oob_model = CmdStanModel(stan_file=oob_stan)
563563
with self.assertRaises(RuntimeError):
@@ -1496,7 +1496,7 @@ def test_validate(self):
14961496
self.assertEqual(bern_fit.metric_type, 'diag_e')
14971497

14981498
def test_validate_sample_sig_figs(self, stanfile='bernoulli.stan'):
1499-
if cmdstan_version_at(2, 25):
1499+
if not cmdstan_version_before(2, 25):
15001500
stan = os.path.join(DATAFILES_PATH, stanfile)
15011501
bern_model = CmdStanModel(stan_file=stan)
15021502

@@ -1568,7 +1568,7 @@ def test_validate_summary_sig_figs(self):
15681568
beta1_default = format(sum_default.iloc[1, 0], '.18g')
15691569
self.assertTrue(beta1_default.startswith('1.3'))
15701570

1571-
if cmdstan_version_at(2, 25):
1571+
if not cmdstan_version_before(2, 25):
15721572
sum_17 = fit.summary(sig_figs=17)
15731573
beta1_17 = format(sum_17.iloc[1, 0], '.18g')
15741574
self.assertTrue(beta1_17.startswith('1.345767078273'))

test/test_utils.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import string
1414
import tempfile
1515
import unittest
16+
from pathlib import Path
1617

1718
import numpy as np
1819
import pandas as pd
@@ -28,7 +29,8 @@
2829
TemporaryCopiedFile,
2930
check_sampler_csv,
3031
cmdstan_path,
31-
cmdstan_version_at,
32+
cmdstan_version,
33+
cmdstan_version_before,
3234
do_command,
3335
flatten_chains,
3436
get_latest_cmdstan,
@@ -202,9 +204,46 @@ def test_munge_cmdstan_versions(self):
202204
os.makedirs(os.path.join(tdir, 'cmdstan-2.22.0'))
203205
self.assertEqual(get_latest_cmdstan(tdir), 'cmdstan-2.22.0')
204206

205-
def test_cmdstan_version_at(self):
207+
def test_cmdstan_version_before(self):
206208
cmdstan_path() # sets os.environ['CMDSTAN']
207-
self.assertFalse(cmdstan_version_at(99, 99))
209+
self.assertTrue(cmdstan_version_before(99, 99))
210+
self.assertFalse(cmdstan_version_before(1, 1))
211+
212+
def test_cmdstan_version(self):
213+
with tempfile.TemporaryDirectory(
214+
prefix="cmdstan_tests", dir=_TMPDIR
215+
) as tmpdir:
216+
tdir = os.path.join(tmpdir, 'tmpdir_xxx')
217+
os.makedirs(tdir)
218+
fake_path = os.path.join(tdir, 'cmdstan-2.22.0')
219+
os.makedirs(os.path.join(fake_path))
220+
fake_bin = os.path.join(fake_path, 'bin')
221+
os.makedirs(fake_bin)
222+
Path(os.path.join(fake_bin, 'stanc' + EXTENSION)).touch()
223+
os.environ['CMDSTAN'] = fake_path
224+
self.assertTrue(fake_path == cmdstan_path())
225+
expect = (
226+
'CmdStan installation {} missing makefile, '
227+
'cannot get version.'.format(fake_path)
228+
)
229+
with LogCapture() as log:
230+
logging.getLogger()
231+
cmdstan_version()
232+
log.check_present(('cmdstanpy', 'INFO', expect))
233+
fake_makefile = os.path.join(fake_path, 'makefile')
234+
with open(fake_makefile, 'w') as fd:
235+
fd.write('... CMDSTAN_VERSION := dont_need_no_mmp\n\n')
236+
expect = (
237+
'Cannot parse version, expected "<major>.<minor>.<patch>", '
238+
'found: "dont_need_no_mmp".'
239+
)
240+
with LogCapture() as log:
241+
logging.getLogger()
242+
cmdstan_version()
243+
log.check_present(('cmdstanpy', 'INFO', expect))
244+
# cleanup
245+
del os.environ['CMDSTAN']
246+
cmdstan_path()
208247

209248

210249
class DataFilesTest(unittest.TestCase):

0 commit comments

Comments
 (0)