diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0de785b..dfb6b84b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,12 +56,16 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies (python) + - name: Install dependencies + run: python -m pip install --upgrade pip wheel build + + - name: Build package run: | - python -m pip install --upgrade pip wheel build - pip install -r requirements.txt - pip install -r requirements-test.txt - pip install codecov + python -m build + python -m pip install .[test] + + - name: Show libraries + run: python -m pip freeze - name: Install numpy, scipy nightlies if: matrix.python-version == '3.11' && matrix.os != 'windows-latest' @@ -75,22 +79,6 @@ jobs: pylint -v cmdstanpy test mypy cmdstanpy - - name: Build wheel - run: python -m build - - - name: Install wheel (Linux, macOS) - if: matrix.os != 'windows-latest' - run: pip install dist/*.whl - - - name: Install wheel (Windows) - if: matrix.os == 'windows-latest' - run: | - $whl = Get-ChildItem -Path dist -Filter *.whl | Select-Object -First 1 - pip install "$whl" - - - name: Show libraries - run: python -m pip freeze - - name: CmdStan installation cacheing id: cache-cmdstan if: ${{ !startswith(needs.get-cmdstan-version.outputs.version, 'git:') }} @@ -123,13 +111,8 @@ jobs: cd run_tests pytest -v ../test --cov=../cmdstanpy - - name: Run model with requirements-optional.txt - run: | - cd run_tests - python -m pip install -r ../requirements-optional.txt - python ../test/example_script.py - - name: Submit codecov run: | + pip install codecov cd run_tests codecov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a94dd20c..2a66021d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,9 +32,8 @@ jobs: - name: Install dependencies (python) run: | - python -m pip install --upgrade pip wheel twine codecov "sphinx>5,<6" nbsphinx ipython ipykernel "pydata-sphinx-theme<0.9" requests sphinx-copybutton xarray matplotlib - pip install -r requirements.txt - pip install -e . + python -m pip install --upgrade pip wheel build twine requests + pip install -e .[doc,test] - name: Install CmdStan run: | @@ -80,7 +79,7 @@ jobs: git push -f origin master - name: Build wheel - run: python setup.py sdist bdist_wheel + run: python -m build - name: Install bdist_wheel run: pip install dist/*.whl diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0556d912..58a42dd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,6 @@ repos: rev: v1.5.0 hooks: - id: mypy - # Copied from setup.cfg exclude: ^test/ additional_dependencies: [ numpy >= 1.22] # local uses the user-installed pylint, this allows dependency checking diff --git a/MANIFEST.in b/MANIFEST.in index f981c388..b3bec1f5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,4 @@ # This file is used to include extra files in source distributions. -# (Source distributions are generated by running `python setup.py sdist`.) -include requirements*.txt include LICENSE.md include cmdstanpy/py.typed diff --git a/cmdstanpy/__init__.py b/cmdstanpy/__init__.py index 815f1ab1..4e3c134e 100644 --- a/cmdstanpy/__init__.py +++ b/cmdstanpy/__init__.py @@ -22,7 +22,7 @@ def _cleanup_tmpdir() -> None: from ._version import __version__ # noqa -from .compilation import compile_stan_file +from .compilation import compile_stan_file, format_stan_file from .install_cmdstan import rebuild_cmdstan from .model import CmdStanModel from .stanfit import ( @@ -50,6 +50,7 @@ def _cleanup_tmpdir() -> None: 'set_make_env', 'install_cmdstan', 'compile_stan_file', + 'format_stan_file', 'CmdStanMCMC', 'CmdStanMLE', 'CmdStanGQ', diff --git a/cmdstanpy/compilation.py b/cmdstanpy/compilation.py index e096c9dc..4c21585a 100644 --- a/cmdstanpy/compilation.py +++ b/cmdstanpy/compilation.py @@ -9,11 +9,17 @@ import shutil import subprocess from copy import copy +from datetime import datetime from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Union from cmdstanpy.utils import get_logger -from cmdstanpy.utils.cmdstan import EXTENSION, cmdstan_path +from cmdstanpy.utils.cmdstan import ( + EXTENSION, + cmdstan_path, + cmdstan_version, + cmdstan_version_before, +) from cmdstanpy.utils.command import do_command from cmdstanpy.utils.filesystem import SanitizedOrTmpFilePath @@ -476,3 +482,98 @@ def compile_stan_file( f"Failed to compile Stan model '{src}'. " f"Console:\n{console}" ) return str(exe_target) + + +def format_stan_file( + stan_file: Union[str, os.PathLike], + *, + overwrite_file: bool = False, + canonicalize: Union[bool, str, Iterable[str]] = False, + max_line_length: int = 78, + backup: bool = True, + stanc_options: Optional[Dict[str, Any]] = None, +) -> None: + """ + Run stanc's auto-formatter on the model code. Either saves directly + back to the file or prints for inspection + + :param stan_file: Path to Stan program file. + :param overwrite_file: If True, save the updated code to disk, rather + than printing it. By default False + :param canonicalize: Whether or not the compiler should 'canonicalize' + the Stan model, removing things like deprecated syntax. Default is + False. If True, all canonicalizations are run. If it is a list of + strings, those options are passed to stanc (new in Stan 2.29) + :param max_line_length: Set the wrapping point for the formatter. The + default value is 78, which wraps most lines by the 80th character. + :param backup: If True, create a stanfile.bak backup before + writing to the file. Only disable this if you're sure you have other + copies of the file or are using a version control system like Git. + :param stanc_options: Additional options to pass to the stanc compiler. + """ + stan_file = Path(stan_file).resolve() + + if not stan_file.exists(): + raise ValueError(f'File does not exist: {stan_file}') + + try: + cmd = ( + [os.path.join(cmdstan_path(), 'bin', 'stanc' + EXTENSION)] + # handle include-paths, allow-undefined etc + + CompilerOptions(stanc_options=stanc_options).compose_stanc(None) + + [str(stan_file)] + ) + + if canonicalize: + if cmdstan_version_before(2, 29): + if isinstance(canonicalize, bool): + cmd.append('--print-canonical') + else: + raise ValueError( + "Invalid arguments passed for current CmdStan" + + " version({})\n".format( + cmdstan_version() or "Unknown" + ) + + "--canonicalize requires 2.29 or higher" + ) + else: + if isinstance(canonicalize, str): + cmd.append('--canonicalize=' + canonicalize) + elif isinstance(canonicalize, Iterable): + cmd.append('--canonicalize=' + ','.join(canonicalize)) + else: + cmd.append('--print-canonical') + + # before 2.29, having both --print-canonical + # and --auto-format printed twice + if not (cmdstan_version_before(2, 29) and canonicalize): + cmd.append('--auto-format') + + if not cmdstan_version_before(2, 29): + cmd.append(f'--max-line-length={max_line_length}') + elif max_line_length != 78: + raise ValueError( + "Invalid arguments passed for current CmdStan version" + + " ({})\n".format(cmdstan_version() or "Unknown") + + "--max-line-length requires 2.29 or higher" + ) + + out = subprocess.run(cmd, capture_output=True, text=True, check=True) + if out.stderr: + get_logger().warning(out.stderr) + result = out.stdout + if overwrite_file: + if result: + if backup: + shutil.copyfile( + stan_file, + str(stan_file) + + '.bak-' + + datetime.now().strftime("%Y%m%d%H%M%S"), + ) + stan_file.write_text(result) + else: + print(result) + + except (ValueError, RuntimeError) as e: + raise RuntimeError("Stanc formatting failed") from e diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py index 17be3b5e..0063d2cd 100644 --- a/cmdstanpy/model.py +++ b/cmdstanpy/model.py @@ -10,7 +10,6 @@ import threading from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor -from datetime import datetime from io import StringIO from multiprocessing import cpu_count from typing import ( @@ -57,9 +56,7 @@ from_csv, ) from cmdstanpy.utils import ( - EXTENSION, cmdstan_path, - cmdstan_version, cmdstan_version_before, do_command, get_logger, @@ -320,6 +317,7 @@ def src_info(self) -> Dict[str, Any]: return {} return compilation.src_info(str(self.stan_file), self._compiler_options) + # TODO(2.0) remove def format( self, overwrite_file: bool = False, @@ -329,6 +327,8 @@ def format( backup: bool = True, ) -> None: """ + Deprecated: Use :func:`cmdstanpy.format_stan_file()` instead. + Run stanc's auto-formatter on the model code. Either saves directly back to the file or prints for inspection @@ -345,72 +345,24 @@ def format( writing to the file. Only disable this if you're sure you have other copies of the file or are using a version control system like Git. """ - if self.stan_file is None or not os.path.isfile(self.stan_file): - raise ValueError("No Stan file found for this module") - try: - cmd = ( - [os.path.join(cmdstan_path(), 'bin', 'stanc' + EXTENSION)] - # handle include-paths, allow-undefined etc - + self._compiler_options.compose_stanc(None) - + [str(self.stan_file)] - ) - if canonicalize: - if cmdstan_version_before(2, 29): - if isinstance(canonicalize, bool): - cmd.append('--print-canonical') - else: - raise ValueError( - "Invalid arguments passed for current CmdStan" - + " version({})\n".format( - cmdstan_version() or "Unknown" - ) - + "--canonicalize requires 2.29 or higher" - ) - else: - if isinstance(canonicalize, str): - cmd.append('--canonicalize=' + canonicalize) - elif isinstance(canonicalize, Iterable): - cmd.append('--canonicalize=' + ','.join(canonicalize)) - else: - cmd.append('--print-canonical') - - # before 2.29, having both --print-canonical - # and --auto-format printed twice - if not (cmdstan_version_before(2, 29) and canonicalize): - cmd.append('--auto-format') - - if not cmdstan_version_before(2, 29): - cmd.append(f'--max-line-length={max_line_length}') - elif max_line_length != 78: - raise ValueError( - "Invalid arguments passed for current CmdStan version" - + " ({})\n".format(cmdstan_version() or "Unknown") - + "--max-line-length requires 2.29 or higher" - ) + get_logger().warning( + "CmdStanModel.format() is deprecated and will be " + "removed in the next major version.\n" + "Use cmdstanpy.format_stan_file() instead." + ) - out = subprocess.run( - cmd, capture_output=True, text=True, check=True - ) - if out.stderr: - get_logger().warning(out.stderr) - result = out.stdout - if overwrite_file: - if result: - if backup: - shutil.copyfile( - self.stan_file, - str(self.stan_file) - + '.bak-' - + datetime.now().strftime("%Y%m%d%H%M%S"), - ) - with open(self.stan_file, 'w') as file_handle: - file_handle.write(result) - else: - print(result) + if self.stan_file is None: + raise ValueError("No Stan file found for this module") - except (ValueError, RuntimeError) as e: - raise RuntimeError("Stanc formatting failed") from e + compilation.format_stan_file( + self.stan_file, + overwrite_file=overwrite_file, + max_line_length=max_line_length, + canonicalize=canonicalize, + backup=backup, + stanc_options=self.stanc_options, + ) @property def stanc_options(self) -> Dict[str, Union[bool, int, str]]: diff --git a/docsrc/api.rst b/docsrc/api.rst index 5ec5867c..ace78f27 100644 --- a/docsrc/api.rst +++ b/docsrc/api.rst @@ -84,6 +84,17 @@ CmdStanGQ Functions ********* +compile_stan_model +============= + +.. autofunction:: cmdstanpy.compile_stan_model + + +format_stan_model +============= + +.. autofunction:: cmdstanpy.format_stan_model + show_versions ============= diff --git a/docsrc/env.yml b/docsrc/env.yml index b15d9ef4..b3811514 100644 --- a/docsrc/env.yml +++ b/docsrc/env.yml @@ -5,18 +5,12 @@ channels: dependencies: - python=3.9 - ipykernel - - ipython - - ipywidgets - - numpy>=1.15 + - numpy - pandas - xarray - - sphinx>5,<6 - - nbsphinx - - pydata-sphinx-theme>0.7,<0.9 - - sphinx-copybutton - matplotlib-base - pip - cmdstan - tqdm - pip: - - -e ../ + - -e ../.[docs] diff --git a/pyproject.toml b/pyproject.toml index a1f4b649..77d9569e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,68 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "cmdstanpy" +description = "Python interface to CmdStan" +readme = "README.md" +license = { text = "BSD-3-Clause" } +authors = [{ name = "Stan Dev Team" }] +requires-python = ">=3.8" +dependencies = ["pandas", "numpy>=1.21", "tqdm", "stanio>=0.4.0,<2.0.0"] +dynamic = ["version"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Natural Language :: English", + "Programming Language :: Python", + "Topic :: Scientific/Engineering :: Information Analysis", +] + +[project.urls] +"Homepage" = "https://github.com/stan-dev/cmdstanpy" +"Bug Tracker" = "https://github.com/stan-dev/cmdstanpy/issues" + +[project.scripts] +install_cmdstan = "cmdstanpy.install_cmdstan:__main__" +install_cxx_toolchain = "cmdstanpy.install_cxx_toolchain:__main__" + +[tool.setuptools.dynamic] +version = { attr = "cmdstanpy._version.__version__" } + +[tool.setuptools] +packages = ["cmdstanpy", "cmdstanpy.stanfit", "cmdstanpy.utils"] + +[tool.setuptools.package-data] +"cmdstanpy" = ["py.typed"] + +[project.optional-dependencies] +all = ["xarray"] +test = [ + "flake8", + "pylint", + "pytest", + "pytest-cov", + "pytest-order", + "mypy", + "xarray", +] +docs = [ + "sphinx>5,<6", + "pydata-sphinx-theme<0.9", + "nbsphinx", + "ipython", + "ipykernel", + "ipywidgets", + "sphinx-copybutton", + "xarray", + "matplotlib", +] + + [tool.black] line-length = 80 skip-string-normalization = true @@ -23,8 +88,8 @@ strict_equality = true disallow_untyped_calls = true [[tool.mypy.overrides]] -module = [ - 'tqdm.auto', - 'pandas', - ] +module = ['tqdm.auto', 'pandas'] ignore_missing_imports = true + +[tool.coverage.run] +source = ["cmdstanpy"] diff --git a/requirements-optional.txt b/requirements-optional.txt deleted file mode 100644 index 14578525..00000000 --- a/requirements-optional.txt +++ /dev/null @@ -1 +0,0 @@ -xarray diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 021430af..00000000 --- a/requirements-test.txt +++ /dev/null @@ -1,8 +0,0 @@ -flake8 -pylint -pytest -pytest-cov -pytest-order -mypy -tqdm -xarray diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4a86cd26..00000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pandas -numpy>=1.21 -tqdm -stanio~=0.3.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 500dc555..00000000 --- a/setup.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python - -import os -import re -from typing import List - -import setuptools - -HERE = os.path.dirname(__file__) - - -def readme_contents() -> str: - with open(os.path.join(HERE, 'README.md'), 'r') as fd: - src = fd.read() - return src - - -def requirements() -> List[str]: - with open(os.path.join(HERE, 'requirements.txt'), 'r') as fd: - src = fd.read() - return src.splitlines() - - -def requirements_test() -> List[str]: - with open(os.path.join(HERE, 'requirements-test.txt'), 'r') as fd: - src = fd.read() - return src.splitlines() - - -def requirements_optional() -> List[str]: - with open(os.path.join(HERE, 'requirements-optional.txt'), 'r') as fd: - src = fd.read() - return src.splitlines() - - -def get_version() -> str: - version_file = open(os.path.join('cmdstanpy', '_version.py')) - version_contents = version_file.read() - return re.search("__version__ = '(.*?)'", version_contents).group(1) - - -_classifiers = """ -Programming Language :: Python :: 3 -License :: OSI Approved :: BSD License -Operating System :: OS Independent -Development Status :: 5 - Production/Stable -Intended Audience :: Science/Research -Natural Language :: English -Programming Language :: Python -Topic :: Scientific/Engineering :: Information Analysis -""" - -INSTALL_REQUIRES = requirements() - -EXTRAS_REQUIRE = { - 'all': requirements_optional(), - 'tests': requirements_test(), - 'docs': [ - 'sphinx', - 'sphinx-gallery', - 'sphinx_rtd_theme', - 'numpydoc', - 'matplotlib', - ], -} - -setuptools.setup( - name='cmdstanpy', - version=get_version(), - description='Python interface to CmdStan', - long_description=readme_contents(), - long_description_content_type="text/markdown", - author='Stan Dev Team', - url='https://github.com/stan-dev/cmdstanpy', - license_files=['LICENSE.md'], - packages=['cmdstanpy', 'cmdstanpy.stanfit', 'cmdstanpy.utils'], - package_data={ - 'cmdstanpy': ['py.typed'], - }, - entry_points={ - 'console_scripts': [ - 'install_cmdstan=cmdstanpy.install_cmdstan:__main__', - 'install_cxx_toolchain=cmdstanpy.install_cxx_toolchain:__main__', - ] - }, - install_requires=INSTALL_REQUIRES, - python_requires='>=3.8', - extras_require=EXTRAS_REQUIRE, - classifiers=_classifiers.strip().split('\n'), -)