Skip to content

Commit 742b409

Browse files
authored
Merge pull request #740 from stan-dev/fix/move-format-functionality
Extract format functionality from Model class
2 parents e0b6477 + bccb119 commit 742b409

File tree

4 files changed

+133
-68
lines changed

4 files changed

+133
-68
lines changed

cmdstanpy/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _cleanup_tmpdir() -> None:
2222

2323

2424
from ._version import __version__ # noqa
25-
from .compilation import compile_stan_file
25+
from .compilation import compile_stan_file, format_stan_file
2626
from .install_cmdstan import rebuild_cmdstan
2727
from .model import CmdStanModel
2828
from .stanfit import (
@@ -50,6 +50,7 @@ def _cleanup_tmpdir() -> None:
5050
'set_make_env',
5151
'install_cmdstan',
5252
'compile_stan_file',
53+
'format_stan_file',
5354
'CmdStanMCMC',
5455
'CmdStanMLE',
5556
'CmdStanGQ',

cmdstanpy/compilation.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
import shutil
1010
import subprocess
1111
from copy import copy
12+
from datetime import datetime
1213
from pathlib import Path
1314
from typing import Any, Dict, Iterable, List, Optional, Union
1415

1516
from cmdstanpy.utils import get_logger
16-
from cmdstanpy.utils.cmdstan import EXTENSION, cmdstan_path
17+
from cmdstanpy.utils.cmdstan import (
18+
EXTENSION,
19+
cmdstan_path,
20+
cmdstan_version,
21+
cmdstan_version_before,
22+
)
1723
from cmdstanpy.utils.command import do_command
1824
from cmdstanpy.utils.filesystem import SanitizedOrTmpFilePath
1925

@@ -476,3 +482,98 @@ def compile_stan_file(
476482
f"Failed to compile Stan model '{src}'. " f"Console:\n{console}"
477483
)
478484
return str(exe_target)
485+
486+
487+
def format_stan_file(
488+
stan_file: Union[str, os.PathLike],
489+
*,
490+
overwrite_file: bool = False,
491+
canonicalize: Union[bool, str, Iterable[str]] = False,
492+
max_line_length: int = 78,
493+
backup: bool = True,
494+
stanc_options: Optional[Dict[str, Any]] = None,
495+
) -> None:
496+
"""
497+
Run stanc's auto-formatter on the model code. Either saves directly
498+
back to the file or prints for inspection
499+
500+
:param stan_file: Path to Stan program file.
501+
:param overwrite_file: If True, save the updated code to disk, rather
502+
than printing it. By default False
503+
:param canonicalize: Whether or not the compiler should 'canonicalize'
504+
the Stan model, removing things like deprecated syntax. Default is
505+
False. If True, all canonicalizations are run. If it is a list of
506+
strings, those options are passed to stanc (new in Stan 2.29)
507+
:param max_line_length: Set the wrapping point for the formatter. The
508+
default value is 78, which wraps most lines by the 80th character.
509+
:param backup: If True, create a stanfile.bak backup before
510+
writing to the file. Only disable this if you're sure you have other
511+
copies of the file or are using a version control system like Git.
512+
:param stanc_options: Additional options to pass to the stanc compiler.
513+
"""
514+
stan_file = Path(stan_file).resolve()
515+
516+
if not stan_file.exists():
517+
raise ValueError(f'File does not exist: {stan_file}')
518+
519+
try:
520+
cmd = (
521+
[os.path.join(cmdstan_path(), 'bin', 'stanc' + EXTENSION)]
522+
# handle include-paths, allow-undefined etc
523+
+ CompilerOptions(stanc_options=stanc_options).compose_stanc(None)
524+
+ [str(stan_file)]
525+
)
526+
527+
if canonicalize:
528+
if cmdstan_version_before(2, 29):
529+
if isinstance(canonicalize, bool):
530+
cmd.append('--print-canonical')
531+
else:
532+
raise ValueError(
533+
"Invalid arguments passed for current CmdStan"
534+
+ " version({})\n".format(
535+
cmdstan_version() or "Unknown"
536+
)
537+
+ "--canonicalize requires 2.29 or higher"
538+
)
539+
else:
540+
if isinstance(canonicalize, str):
541+
cmd.append('--canonicalize=' + canonicalize)
542+
elif isinstance(canonicalize, Iterable):
543+
cmd.append('--canonicalize=' + ','.join(canonicalize))
544+
else:
545+
cmd.append('--print-canonical')
546+
547+
# before 2.29, having both --print-canonical
548+
# and --auto-format printed twice
549+
if not (cmdstan_version_before(2, 29) and canonicalize):
550+
cmd.append('--auto-format')
551+
552+
if not cmdstan_version_before(2, 29):
553+
cmd.append(f'--max-line-length={max_line_length}')
554+
elif max_line_length != 78:
555+
raise ValueError(
556+
"Invalid arguments passed for current CmdStan version"
557+
+ " ({})\n".format(cmdstan_version() or "Unknown")
558+
+ "--max-line-length requires 2.29 or higher"
559+
)
560+
561+
out = subprocess.run(cmd, capture_output=True, text=True, check=True)
562+
if out.stderr:
563+
get_logger().warning(out.stderr)
564+
result = out.stdout
565+
if overwrite_file:
566+
if result:
567+
if backup:
568+
shutil.copyfile(
569+
stan_file,
570+
str(stan_file)
571+
+ '.bak-'
572+
+ datetime.now().strftime("%Y%m%d%H%M%S"),
573+
)
574+
stan_file.write_text(result)
575+
else:
576+
print(result)
577+
578+
except (ValueError, RuntimeError) as e:
579+
raise RuntimeError("Stanc formatting failed") from e

cmdstanpy/model.py

Lines changed: 18 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import threading
1111
from collections import OrderedDict
1212
from concurrent.futures import ThreadPoolExecutor
13-
from datetime import datetime
1413
from io import StringIO
1514
from multiprocessing import cpu_count
1615
from typing import (
@@ -57,9 +56,7 @@
5756
from_csv,
5857
)
5958
from cmdstanpy.utils import (
60-
EXTENSION,
6159
cmdstan_path,
62-
cmdstan_version,
6360
cmdstan_version_before,
6461
do_command,
6562
get_logger,
@@ -320,6 +317,7 @@ def src_info(self) -> Dict[str, Any]:
320317
return {}
321318
return compilation.src_info(str(self.stan_file), self._compiler_options)
322319

320+
# TODO(2.0) remove
323321
def format(
324322
self,
325323
overwrite_file: bool = False,
@@ -329,6 +327,8 @@ def format(
329327
backup: bool = True,
330328
) -> None:
331329
"""
330+
Deprecated: Use :func:`cmdstanpy.format_stan_file()` instead.
331+
332332
Run stanc's auto-formatter on the model code. Either saves directly
333333
back to the file or prints for inspection
334334
@@ -345,72 +345,24 @@ def format(
345345
writing to the file. Only disable this if you're sure you have other
346346
copies of the file or are using a version control system like Git.
347347
"""
348-
if self.stan_file is None or not os.path.isfile(self.stan_file):
349-
raise ValueError("No Stan file found for this module")
350-
try:
351-
cmd = (
352-
[os.path.join(cmdstan_path(), 'bin', 'stanc' + EXTENSION)]
353-
# handle include-paths, allow-undefined etc
354-
+ self._compiler_options.compose_stanc(None)
355-
+ [str(self.stan_file)]
356-
)
357348

358-
if canonicalize:
359-
if cmdstan_version_before(2, 29):
360-
if isinstance(canonicalize, bool):
361-
cmd.append('--print-canonical')
362-
else:
363-
raise ValueError(
364-
"Invalid arguments passed for current CmdStan"
365-
+ " version({})\n".format(
366-
cmdstan_version() or "Unknown"
367-
)
368-
+ "--canonicalize requires 2.29 or higher"
369-
)
370-
else:
371-
if isinstance(canonicalize, str):
372-
cmd.append('--canonicalize=' + canonicalize)
373-
elif isinstance(canonicalize, Iterable):
374-
cmd.append('--canonicalize=' + ','.join(canonicalize))
375-
else:
376-
cmd.append('--print-canonical')
377-
378-
# before 2.29, having both --print-canonical
379-
# and --auto-format printed twice
380-
if not (cmdstan_version_before(2, 29) and canonicalize):
381-
cmd.append('--auto-format')
382-
383-
if not cmdstan_version_before(2, 29):
384-
cmd.append(f'--max-line-length={max_line_length}')
385-
elif max_line_length != 78:
386-
raise ValueError(
387-
"Invalid arguments passed for current CmdStan version"
388-
+ " ({})\n".format(cmdstan_version() or "Unknown")
389-
+ "--max-line-length requires 2.29 or higher"
390-
)
349+
get_logger().warning(
350+
"CmdStanModel.format() is deprecated and will be "
351+
"removed in the next major version.\n"
352+
"Use cmdstanpy.format_stan_file() instead."
353+
)
391354

392-
out = subprocess.run(
393-
cmd, capture_output=True, text=True, check=True
394-
)
395-
if out.stderr:
396-
get_logger().warning(out.stderr)
397-
result = out.stdout
398-
if overwrite_file:
399-
if result:
400-
if backup:
401-
shutil.copyfile(
402-
self.stan_file,
403-
str(self.stan_file)
404-
+ '.bak-'
405-
+ datetime.now().strftime("%Y%m%d%H%M%S"),
406-
)
407-
with open(self.stan_file, 'w') as file_handle:
408-
file_handle.write(result)
409-
else:
410-
print(result)
355+
if self.stan_file is None:
356+
raise ValueError("No Stan file found for this module")
411357

412-
except (ValueError, RuntimeError) as e:
413-
raise RuntimeError("Stanc formatting failed") from e
358+
compilation.format_stan_file(
359+
self.stan_file,
360+
overwrite_file=overwrite_file,
361+
max_line_length=max_line_length,
362+
canonicalize=canonicalize,
363+
backup=backup,
364+
stanc_options=self.stanc_options,
365+
)
414366

415367
@property
416368
def stanc_options(self) -> Dict[str, Union[bool, int, str]]:

docsrc/api.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ CmdStanGQ
8484
Functions
8585
*********
8686

87+
compile_stan_model
88+
=============
89+
90+
.. autofunction:: cmdstanpy.compile_stan_model
91+
92+
93+
format_stan_model
94+
=============
95+
96+
.. autofunction:: cmdstanpy.format_stan_model
97+
8798
show_versions
8899
=============
89100

0 commit comments

Comments
 (0)