Skip to content

Commit a40b6a1

Browse files
authored
Merge pull request #518 from stan-dev/src-info-includes
Add include paths to src_info, update stanc3 argument handling
2 parents 683a135 + 457a397 commit a40b6a1

5 files changed

Lines changed: 106 additions & 43 deletions

File tree

cmdstanpy/compiler_opts.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
"""
44

55
import os
6+
from copy import copy
67
from pathlib import Path
78
from typing import Any, Dict, List, Optional, Union
89

910
from cmdstanpy.utils import get_logger
1011

1112
STANC_OPTS = [
1213
'O',
13-
'allow_undefined',
14+
'allow-undefined',
1415
'use-opencl',
1516
'warn-uninitialized',
16-
'include_paths',
17+
'include-paths',
1718
'name',
1819
'warn-pedantic',
1920
]
2021

22+
STANC_DEPRECATED_OPTS = {
23+
'allow_undefined': 'allow-undefined',
24+
'include_paths': 'include-paths',
25+
}
26+
2127
STANC_IGNORE_OPTS = [
2228
'debug-lex',
2329
'debug-parse',
@@ -121,19 +127,37 @@ def validate_stanc_opts(self) -> None:
121127
return
122128
ignore = []
123129
paths = None
130+
for deprecated, replacement in STANC_DEPRECATED_OPTS.items():
131+
if deprecated in self._stanc_options:
132+
if replacement:
133+
get_logger().warning(
134+
'compiler option "%s" is deprecated, use "%s" instead',
135+
deprecated,
136+
replacement,
137+
)
138+
self._stanc_options[replacement] = copy(
139+
self._stanc_options[deprecated]
140+
)
141+
del self._stanc_options[deprecated]
142+
else:
143+
get_logger().warning(
144+
'compiler option "%s" is deprecated and '
145+
'should not be used',
146+
deprecated,
147+
)
124148
for key, val in self._stanc_options.items():
125149
if key in STANC_IGNORE_OPTS:
126150
get_logger().info('ignoring compiler option: %s', key)
127151
ignore.append(key)
128152
elif key not in STANC_OPTS:
129153
raise ValueError(f'unknown stanc compiler option: {key}')
130-
elif key == 'include_paths':
154+
elif key == 'include-paths':
131155
paths = val
132156
if isinstance(val, str):
133157
paths = val.split(',')
134158
elif not isinstance(val, list):
135159
raise ValueError(
136-
'Invalid include_paths, expecting list or '
160+
'Invalid include-paths, expecting list or '
137161
f'string, found type: {type(val)}.'
138162
)
139163
elif key == 'use-opencl':
@@ -145,10 +169,10 @@ def validate_stanc_opts(self) -> None:
145169
for opt in ignore:
146170
del self._stanc_options[opt]
147171
if paths is not None:
148-
self._stanc_options['include_paths'] = paths
172+
self._stanc_options['include-paths'] = paths
149173
bad_paths = [
150174
dir
151-
for dir in self._stanc_options['include_paths']
175+
for dir in self._stanc_options['include-paths']
152176
if not os.path.exists(dir)
153177
]
154178
if any(bad_paths):
@@ -190,8 +214,8 @@ def validate_user_header(self) -> None:
190214
raise ValueError(
191215
f"Header file must end in .hpp, got {self._user_header}"
192216
)
193-
if "allow_undefined" not in self._stanc_options:
194-
self._stanc_options["allow_undefined"] = True
217+
if "allow-undefined" not in self._stanc_options:
218+
self._stanc_options["allow-undefined"] = True
195219
# set full path
196220
self._user_header = os.path.abspath(self._user_header)
197221

@@ -218,7 +242,7 @@ def add(self, new_opts: "CompilerOptions") -> None: # noqa: disable=Q000
218242
self._stanc_options = new_opts.stanc_options
219243
else:
220244
for key, val in new_opts.stanc_options.items():
221-
if key == 'include_paths':
245+
if key == 'include-paths':
222246
self.add_include_path(str(val))
223247
else:
224248
self._stanc_options[key] = val
@@ -230,23 +254,23 @@ def add(self, new_opts: "CompilerOptions") -> None: # noqa: disable=Q000
230254

231255
def add_include_path(self, path: str) -> None:
232256
"""Adds include path to existing set of compiler options."""
233-
if 'include_paths' not in self._stanc_options:
234-
self._stanc_options['include_paths'] = [path]
235-
elif path not in self._stanc_options['include_paths']:
236-
self._stanc_options['include_paths'].append(path)
257+
if 'include-paths' not in self._stanc_options:
258+
self._stanc_options['include-paths'] = [path]
259+
elif path not in self._stanc_options['include-paths']:
260+
self._stanc_options['include-paths'].append(path)
237261

238262
def compose(self) -> List[str]:
239263
"""Format makefile options as list of strings."""
240264
opts = []
241265
if self._stanc_options is not None and len(self._stanc_options) > 0:
242266
for key, val in self._stanc_options.items():
243-
if key == 'include_paths':
267+
if key == 'include-paths':
244268
opts.append(
245-
'STANCFLAGS+=--include_paths='
269+
'STANCFLAGS+=--include-paths='
246270
+ ','.join(
247271
(
248272
Path(p).as_posix()
249-
for p in self._stanc_options['include_paths']
273+
for p in self._stanc_options['include-paths']
250274
)
251275
)
252276
)

cmdstanpy/model.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ def __init__(
128128
)
129129
self._name = model_name.strip()
130130

131+
self._compiler_options.validate()
132+
131133
if stan_file is None:
132134
if exe_file is None:
133135
raise ValueError(
@@ -152,13 +154,9 @@ def __init__(
152154
program = fd.read()
153155
if '#include' in program:
154156
path, _ = os.path.split(self._stan_file)
155-
if self._compiler_options is None:
156-
self._compiler_options = CompilerOptions(
157-
stanc_options={'include_paths': [path]}
158-
)
159-
elif self._compiler_options._stanc_options is None:
157+
if self._compiler_options._stanc_options is None:
160158
self._compiler_options._stanc_options = {
161-
'include_paths': [path]
159+
'include-paths': [path]
162160
}
163161
else:
164162
self._compiler_options.add_include_path(path)
@@ -186,8 +184,6 @@ def __init__(
186184
' found: {}.'.format(self._name, exename)
187185
)
188186

189-
self._compiler_options.validate()
190-
191187
if platform.system() == 'Windows':
192188
try:
193189
do_command(['where.exe', 'tbb.dll'], fd_out=None)
@@ -279,9 +275,19 @@ def src_info(self) -> Dict[str, Any]:
279275
if self.stan_file is None:
280276
return result
281277
try:
282-
278+
includes = ''
279+
if (
280+
self.stanc_options is not None
281+
and 'include-paths' in self.stanc_options
282+
):
283+
print(self.stanc_options)
284+
includes = '--include-paths=' + ','.join(
285+
Path(p).as_posix()
286+
for p in self.stanc_options['include-paths'] # type: ignore
287+
)
283288
cmd = [
284289
os.path.join('.', 'bin', 'stanc' + EXTENSION),
290+
includes,
285291
'--info',
286292
self.stan_file,
287293
]

docsrc/examples/Using External C++.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"cell_type": "markdown",
6161
"metadata": {},
6262
"source": [
63-
"Even enabling the `--allow_undefined` flag to stanc3 will not allow this model to be compiled quite yet."
63+
"Even enabling the `--allow-undefined` flag to stanc3 will not allow this model to be compiled quite yet."
6464
]
6565
},
6666
{
@@ -69,7 +69,7 @@
6969
"metadata": {},
7070
"outputs": [],
7171
"source": [
72-
"model_external.compile(stanc_options={'allow_undefined':True})"
72+
"model_external.compile(stanc_options={'allow-undefined':True})"
7373
]
7474
},
7575
{
@@ -80,7 +80,7 @@
8080
"\n",
8181
"We can provide a definition in a C++ header file by using the `user_header` argument to either the CmdStanModel constructor or the `compile` method. \n",
8282
"\n",
83-
"This will enables the `allow_undefined` flag automatically."
83+
"This will enables the `allow-undefined` flag automatically."
8484
]
8585
},
8686
{

test/test_compiler_opts.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import unittest
55

6+
from testfixtures import LogCapture
7+
68
from cmdstanpy.compiler_opts import CompilerOptions
79

810
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -70,6 +72,28 @@ def test_opts_stanc(self):
7072
['STANCFLAGS+=--warn-uninitialized', 'STANCFLAGS+=--name=foo'],
7173
)
7274

75+
def test_opts_stanc_deprecated(self):
76+
stanc_opts = {}
77+
stanc_opts['allow_undefined'] = True
78+
opts = CompilerOptions(stanc_options=stanc_opts)
79+
with LogCapture() as log:
80+
opts.validate()
81+
log.check_present(
82+
(
83+
'cmdstanpy',
84+
'WARNING',
85+
'compiler option "allow_undefined" is deprecated,'
86+
' use "allow-undefined" instead',
87+
)
88+
)
89+
self.assertEqual(opts.compose(), ['STANCFLAGS+=--allow-undefined'])
90+
91+
stanc_opts['include_paths'] = DATAFILES_PATH
92+
opts = CompilerOptions(stanc_options=stanc_opts)
93+
opts.validate()
94+
self.assertIn('include-paths', opts.stanc_options)
95+
self.assertNotIn('include_paths', opts.stanc_options)
96+
7397
def test_opts_stanc_opencl(self):
7498
stanc_opts = {}
7599
stanc_opts['use-opencl'] = 'foo'
@@ -89,22 +113,22 @@ def test_opts_stanc_ignore(self):
89113
def test_opts_stanc_includes(self):
90114
path2 = os.path.join(HERE, 'data', 'optimize')
91115
paths_str = ','.join([DATAFILES_PATH, path2]).replace('\\', '/')
92-
expect = 'STANCFLAGS+=--include_paths=' + paths_str
116+
expect = 'STANCFLAGS+=--include-paths=' + paths_str
93117

94-
stanc_opts = {'include_paths': paths_str}
118+
stanc_opts = {'include-paths': paths_str}
95119
opts = CompilerOptions(stanc_options=stanc_opts)
96120
opts.validate()
97121
opts_list = opts.compose()
98122
self.assertTrue(expect in opts_list)
99123

100-
stanc_opts = {'include_paths': [DATAFILES_PATH, path2]}
124+
stanc_opts = {'include-paths': [DATAFILES_PATH, path2]}
101125
opts = CompilerOptions(stanc_options=stanc_opts)
102126
opts.validate()
103127
opts_list = opts.compose()
104128
self.assertTrue(expect in opts_list)
105129

106130
def test_opts_add_include_paths(self):
107-
expect = 'STANCFLAGS+=--include_paths=' + DATAFILES_PATH.replace(
131+
expect = 'STANCFLAGS+=--include-paths=' + DATAFILES_PATH.replace(
108132
'\\', '/'
109133
)
110134
stanc_opts = {'warn-uninitialized': True}
@@ -120,7 +144,7 @@ def test_opts_add_include_paths(self):
120144

121145
path2 = os.path.join(HERE, 'data', 'optimize')
122146
paths_str = ','.join([DATAFILES_PATH, path2]).replace('\\', '/')
123-
expect = 'STANCFLAGS+=--include_paths=' + paths_str
147+
expect = 'STANCFLAGS+=--include-paths=' + paths_str
124148
opts.add_include_path(path2)
125149
opts.validate()
126150
opts_list = opts.compose()
@@ -169,7 +193,7 @@ def test_user_header(self):
169193
header_file = os.path.join(DATAFILES_PATH, 'return_one.hpp')
170194
opts = CompilerOptions(user_header=header_file)
171195
opts.validate()
172-
self.assertTrue(opts.stanc_options['allow_undefined'])
196+
self.assertTrue(opts.stanc_options['allow-undefined'])
173197

174198
bad = os.path.join(DATAFILES_PATH, 'nonexistant.hpp')
175199
opts = CompilerOptions(user_header=bad)
@@ -209,20 +233,20 @@ def test_opts_add(self):
209233
self.assertTrue('STAN_OPENCL=FALSE' in opts_list)
210234
self.assertTrue('OPENCL_DEVICE_ID=2' in opts_list)
211235

212-
expect = 'STANCFLAGS+=--include_paths=' + DATAFILES_PATH.replace(
236+
expect = 'STANCFLAGS+=--include-paths=' + DATAFILES_PATH.replace(
213237
'\\', '/'
214238
)
215-
stanc_opts2 = {'include_paths': DATAFILES_PATH}
239+
stanc_opts2 = {'include-paths': DATAFILES_PATH}
216240
new_opts2 = CompilerOptions(stanc_options=stanc_opts2)
217241
opts.add(new_opts2)
218242
opts_list = opts.compose()
219243
self.assertTrue(expect in opts_list)
220244

221245
path2 = os.path.join(HERE, 'data', 'optimize')
222-
expect = 'STANCFLAGS+=--include_paths=' + ','.join(
246+
expect = 'STANCFLAGS+=--include-paths=' + ','.join(
223247
[DATAFILES_PATH, path2]
224248
).replace('\\', '/')
225-
stanc_opts3 = {'include_paths': path2}
249+
stanc_opts3 = {'include-paths': path2}
226250
new_opts3 = CompilerOptions(stanc_options=stanc_opts3)
227251
opts.add(new_opts3)
228252
opts_list = opts.compose()

test/test_model.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def test_model_bad(self):
129129
def test_stanc_options(self):
130130
opts = {
131131
'O': True,
132-
'allow_undefined': True,
132+
'allow-undefined': True,
133133
'use-opencl': True,
134134
'name': 'foo',
135135
}
@@ -138,7 +138,7 @@ def test_stanc_options(self):
138138
)
139139
stanc_opts = model.stanc_options
140140
self.assertTrue(stanc_opts['O'])
141-
self.assertTrue(stanc_opts['allow_undefined'])
141+
self.assertTrue(stanc_opts['allow-undefined'])
142142
self.assertTrue(stanc_opts['use-opencl'])
143143
self.assertTrue(stanc_opts['name'] == 'foo')
144144

@@ -151,12 +151,12 @@ def test_stanc_options(self):
151151
stan_file=BERN_STAN, compile=False, stanc_options=bad_opts
152152
)
153153
with self.assertRaises(ValueError):
154-
bad_opts = {'include_paths': True}
154+
bad_opts = {'include-paths': True}
155155
model = CmdStanModel(
156156
stan_file=BERN_STAN, compile=False, stanc_options=bad_opts
157157
)
158158
with self.assertRaises(ValueError):
159-
bad_opts = {'include_paths': 'lkjdf'}
159+
bad_opts = {'include-paths': 'lkjdf'}
160160
model = CmdStanModel(
161161
stan_file=BERN_STAN, compile=False, stanc_options=bad_opts
162162
)
@@ -190,6 +190,15 @@ def test_model_info(self):
190190
self.assertNotEqual(model_info, {})
191191
self.assertIn('theta', model_info['parameters'])
192192

193+
model_include = CmdStanModel(
194+
stan_file=os.path.join(DATAFILES_PATH, "bernoulli_include.stan"),
195+
compile=False,
196+
)
197+
model_info_include = model_include.src_info()
198+
self.assertNotEqual(model_info_include, {})
199+
self.assertIn('theta', model_info_include['parameters'])
200+
self.assertIn('included_files', model_info_include)
201+
193202
def test_compile_force(self):
194203
if os.path.exists(BERN_EXE):
195204
os.remove(BERN_EXE)
@@ -349,7 +358,7 @@ def test_model_includes_explicit(self):
349358
if os.path.exists(BERN_EXE):
350359
os.remove(BERN_EXE)
351360
model = CmdStanModel(
352-
stan_file=BERN_STAN, stanc_options={'include_paths': DATAFILES_PATH}
361+
stan_file=BERN_STAN, stanc_options={'include-paths': DATAFILES_PATH}
353362
)
354363
self.assertEqual(BERN_STAN, model.stan_file)
355364
self.assertPathsEqual(model.exe_file, BERN_EXE)

0 commit comments

Comments
 (0)