Skip to content

Commit 4d99ecc

Browse files
Recursive up-to-date checks (ref #614). (#627)
undefined
1 parent fb3dfe2 commit 4d99ecc

File tree

4 files changed

+63
-2
lines changed

4 files changed

+63
-2
lines changed

cmdstanpy/model.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,14 @@ def compile(
474474
self._compiler_options.add(compiler_options)
475475
exe_target = os.path.splitext(self._stan_file)[0] + EXTENSION
476476
if os.path.exists(exe_target):
477-
src_time = os.path.getmtime(self._stan_file)
478477
exe_time = os.path.getmtime(exe_target)
479-
if exe_time > src_time and not force:
478+
included_files = [self._stan_file]
479+
included_files.extend(self.src_info().get('included_files', []))
480+
out_of_date = any(
481+
os.path.getmtime(included_file) > exe_time
482+
for included_file in included_files
483+
)
484+
if not out_of_date and not force:
480485
get_logger().debug('found newer exe file, not recompiling')
481486
if self._exe_file is None: # called from constructor
482487
self._exe_file = exe_target

test/data/add_one_model.stan

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
functions {
2+
#include add_one_function.stan
3+
}
4+
5+
generated quantities {
6+
real x = add_one(3);
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
real add_one(real x) {
2+
return x + 1;
3+
}

test/test_model.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,52 @@ def test_model_info(self):
207207
self.assertIn('theta', model_info_include['parameters'])
208208
self.assertIn('included_files', model_info_include)
209209

210+
def test_compile_with_includes(self):
211+
getmtime = os.path.getmtime
212+
configs = [
213+
('add_one_model.stan', ['include-path']),
214+
('bernoulli_include.stan', []),
215+
]
216+
for stan_file, include_paths in configs:
217+
stan_file = os.path.join(DATAFILES_PATH, stan_file)
218+
include_paths = [
219+
os.path.join(DATAFILES_PATH, path) for path in include_paths
220+
]
221+
222+
# Compile for the first time.
223+
model = CmdStanModel(
224+
stan_file=stan_file,
225+
compile=False,
226+
stanc_options={"include-paths": include_paths},
227+
)
228+
with LogCapture(level=logging.INFO) as log:
229+
model.compile()
230+
log.check_present(
231+
('cmdstanpy', 'INFO', StringComparison('compiling stan file'))
232+
)
233+
234+
# Compile for the second time, ensuring cache is used.
235+
with LogCapture(level=logging.DEBUG) as log:
236+
model.compile()
237+
log.check_present(
238+
('cmdstanpy', 'DEBUG', StringComparison('found newer exe file'))
239+
)
240+
241+
# Compile after modifying included file, ensuring cache is not used.
242+
def _patched_getmtime(filename: str) -> float:
243+
includes = ['divide_real_by_two.stan', 'add_one_function.stan']
244+
if any(filename.endswith(include) for include in includes):
245+
return float('inf')
246+
return getmtime(filename)
247+
248+
with LogCapture(level=logging.INFO) as log, patch(
249+
'os.path.getmtime', side_effect=_patched_getmtime
250+
):
251+
model.compile()
252+
log.check_present(
253+
('cmdstanpy', 'INFO', StringComparison('compiling stan file'))
254+
)
255+
210256
def test_compile_force(self):
211257
if os.path.exists(BERN_EXE):
212258
os.remove(BERN_EXE)

0 commit comments

Comments
 (0)