From 4d99ecc3ad65b440aa77a0def54514515c13ce8b Mon Sep 17 00:00:00 2001 From: Till Hoffmann Date: Thu, 20 Oct 2022 10:14:31 -0400 Subject: [PATCH] Recursive up-to-date checks (ref #614). (#627) undefined --- cmdstanpy/model.py | 9 +++- test/data/add_one_model.stan | 7 +++ test/data/include-path/add_one_function.stan | 3 ++ test/test_model.py | 46 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/data/add_one_model.stan create mode 100644 test/data/include-path/add_one_function.stan diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 7d4ab8d6..bffa5d5c 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -474,9 +474,14 @@ def compile( self._compiler_options.add(compiler_options) exe_target = os.path.splitext(self._stan_file)[0] + EXTENSION if os.path.exists(exe_target): - src_time = os.path.getmtime(self._stan_file) exe_time = os.path.getmtime(exe_target) - if exe_time > src_time and not force: + included_files = [self._stan_file] + included_files.extend(self.src_info().get('included_files', [])) + out_of_date = any( + os.path.getmtime(included_file) > exe_time + for included_file in included_files + ) + if not out_of_date and not force: get_logger().debug('found newer exe file, not recompiling') if self._exe_file is None: # called from constructor self._exe_file = exe_target diff --git a/test/data/add_one_model.stan b/test/data/add_one_model.stan new file mode 100644 index 00000000..46645c1d --- /dev/null +++ b/test/data/add_one_model.stan @@ -0,0 +1,7 @@ +functions { + #include add_one_function.stan +} + +generated quantities { + real x = add_one(3); +} diff --git a/test/data/include-path/add_one_function.stan b/test/data/include-path/add_one_function.stan new file mode 100644 index 00000000..f29a613a --- /dev/null +++ b/test/data/include-path/add_one_function.stan @@ -0,0 +1,3 @@ +real add_one(real x) { + return x + 1; +} diff --git a/test/test_model.py b/test/test_model.py index 4c3e2cc5..1aa44aa9 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -207,6 +207,52 @@ def test_model_info(self): self.assertIn('theta', model_info_include['parameters']) self.assertIn('included_files', model_info_include) + def test_compile_with_includes(self): + getmtime = os.path.getmtime + configs = [ + ('add_one_model.stan', ['include-path']), + ('bernoulli_include.stan', []), + ] + for stan_file, include_paths in configs: + stan_file = os.path.join(DATAFILES_PATH, stan_file) + include_paths = [ + os.path.join(DATAFILES_PATH, path) for path in include_paths + ] + + # Compile for the first time. + model = CmdStanModel( + stan_file=stan_file, + compile=False, + stanc_options={"include-paths": include_paths}, + ) + with LogCapture(level=logging.INFO) as log: + model.compile() + log.check_present( + ('cmdstanpy', 'INFO', StringComparison('compiling stan file')) + ) + + # Compile for the second time, ensuring cache is used. + with LogCapture(level=logging.DEBUG) as log: + model.compile() + log.check_present( + ('cmdstanpy', 'DEBUG', StringComparison('found newer exe file')) + ) + + # Compile after modifying included file, ensuring cache is not used. + def _patched_getmtime(filename: str) -> float: + includes = ['divide_real_by_two.stan', 'add_one_function.stan'] + if any(filename.endswith(include) for include in includes): + return float('inf') + return getmtime(filename) + + with LogCapture(level=logging.INFO) as log, patch( + 'os.path.getmtime', side_effect=_patched_getmtime + ): + model.compile() + log.check_present( + ('cmdstanpy', 'INFO', StringComparison('compiling stan file')) + ) + def test_compile_force(self): if os.path.exists(BERN_EXE): os.remove(BERN_EXE)