diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f71b9ab..d05db57 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,37 +9,40 @@ on: jobs: build-and-test-python: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.7, 3.8] + python-version: ["3.8"] + os: ["macos-latest", "ubuntu-latest", "windows-latest"] fail-fast: false steps: - - name: "Get OS version (Linux)" - if: startsWith(runner.os, 'Linux') + - name: "Set environment variables (Windows)" + if: startsWith(runner.os, 'Windows') + shell: pwsh run: | - echo "OS_VERSION=`lsb_release -sr`" >> $GITHUB_ENV - echo "PIP_DEFAULT_CACHE=$HOME/.cache/pip" >> $GITHUB_ENV - echo "DEFAULT_HOME=$HOME" >> $GITHUB_ENV - - uses: actions/checkout@v2 + (Get-ItemProperty "HKLM:System\CurrentControlSet\Control\FileSystem").LongPathsEnabled + $os_version = (Get-CimInstance Win32_OperatingSystem).version + Echo "OS_VERSION=$os_version" >> $env:GITHUB_ENV + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: "Restore pip cache" - id: cache-pip - uses: actions/cache@v2 + cache: pip + cache-dependency-path: "**/python/pyproject.toml" + - name: "Restore RTools40" + if: startsWith(runner.os, 'Windows') + id: cache-rtools + uses: actions/cache@v3 with: - path: ${{ env.PIP_DEFAULT_CACHE }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/python/requirements.txt') }}-v1 - restore-keys: | - ${{ runner.os }}-pip- + path: C:/rtools40 + key: ${{ runner.os }}-${{ env.OS_VERSION }}-rtools-v1 - name: Install and test run: | - pip install -U -r python/requirements.txt dask[dataframe] distributed cd python - python setup.py develop test + python -m pip install -U --editable ".[dev,parallel]" + python -m pytest prophet/tests/ build-and-test-r: @@ -55,7 +58,7 @@ jobs: RSPM: ${{ matrix.config.rspm }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up R uses: r-lib/actions/setup-r@v2 with: @@ -71,7 +74,7 @@ jobs: shell: Rscript {0} - name: Restore R package cache if: runner.os != 'Windows' - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.R_LIBS_USER }} key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} diff --git a/Dockerfile b/Dockerfile index ba96ebc..4d85d02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,17 +2,12 @@ FROM python:3.7-stretch RUN apt-get -y install libc-dev -RUN pip install pip==19.1.1 - -COPY python/requirements.txt . -RUN pip install -r requirements.txt -RUN pip install ipython==7.5.0 +RUN pip install pip==22.3.1 COPY . . WORKDIR python -RUN python setup.py install +RUN python -m pip install -e \".[dev, parallel]\" WORKDIR / - diff --git a/README.md b/README.md index 69ab124..6026166 100644 --- a/README.md +++ b/README.md @@ -88,14 +88,13 @@ To get the latest code changes as they are merged, you can clone this repo and b ```bash git clone https://github.com/facebook/prophet.git cd prophet/python -python -m pip install -r requirements.txt -python setup.py develop +python -m pip install -e . ``` By default, Prophet will use a fixed version of `cmdstan` (downloading and installing it if necessary) to compile the model executables. If this is undesired and you would like to use your own existing `cmdstan` installation, you can set the environment variable `PROPHET_REPACKAGE_CMDSTAN` to `False`: ```bash -export PROPHET_REPACKAGE_CMDSTAN=False; python setup.py develop +export PROPHET_REPACKAGE_CMDSTAN=False; python -m pip install -e . ``` ### Linux diff --git a/docs/_docs/contributing.md b/docs/_docs/contributing.md index 4aa39d0..240ec74 100644 --- a/docs/_docs/contributing.md +++ b/docs/_docs/contributing.md @@ -46,12 +46,10 @@ $ cd python # with Anaconda $ conda create -n prophet $ conda activate prophet -$ pip install -r requirements.txt # with venv $ python3 -m venv prophet $ source prophet/bin/activate -$ pip install -r requirements.txt ``` ### R @@ -86,7 +84,7 @@ The next step is to build and install the development version of prophet in the ### Python ```bash -$ python setup.py develop +$ python -m pip install -e ".[dev,parallel]" ``` You should be able to import *prophet* from your locally built version: @@ -95,7 +93,7 @@ You should be able to import *prophet* from your locally built version: $ python # start an interpreter >>> import prophet >>> prophet.__version__ -'1.0' # whatever the current github version is +'1.1.2' # whatever the current github version is ``` This will create the new environment, and not touch any of your existing environments, @@ -154,11 +152,11 @@ Adding tests is one of the most common requests after code is pushed to prophet. ### Python -Prophet uses the ``unittest`` package for running tests in Python and ``testthat`` package for testing in R. All tests should go into the tests subdirectory in either the Python or R folders. +Prophet uses the ``pytest`` package for running tests in Python and ``testthat`` package for testing in R. All tests should go into the tests subdirectory in either the Python or R folders. The entire test suite can be run by typing: ```bash -$ python setup.py test +$ python -m pytest prophet/tests/ ``` ### R diff --git a/python/MANIFEST.in b/python/MANIFEST.in index 450a487..72a6fbe 100644 --- a/python/MANIFEST.in +++ b/python/MANIFEST.in @@ -1,6 +1,6 @@ include stan/*.stan include LICENSE -include requirements.txt +include pyproject.toml # Ensure in-place built models do not get included in the source dist. prune prophet/stan_model diff --git a/python/README.md b/python/README.md index 708ba4b..9feac57 100644 --- a/python/README.md +++ b/python/README.md @@ -30,7 +30,7 @@ See [Installation in Python - Development version](https://github.com/facebook/p Simply type `make build` and if everything is fine you should be able to `make shell` or alternative jump directly to `make py-shell`. -To run the tests, inside the container `cd python/prophet` and then `python -m unittest` +To run the tests, inside the container `cd python/prophet` and then `python -m pytest prophet/tests/` ### Example usage diff --git a/python/prophet/__version__.py b/python/prophet/__version__.py index b911960..72f26f5 100644 --- a/python/prophet/__version__.py +++ b/python/prophet/__version__.py @@ -1,7 +1 @@ -__title__ = "prophet" -__description__ = "Automatic Forecasting Procedure" -__url__ = "https://facebook.github.io/prophet/" -__version__ = "1.1.1" -__author__ = "Sean J. Taylor , Ben Letham " -__author_email__ = "sjtz@pm.me" -__license__ = "MIT" +__version__ = "1.1.2" diff --git a/python/prophet/tests/conftest.py b/python/prophet/tests/conftest.py new file mode 100644 index 0000000..c17d448 --- /dev/null +++ b/python/prophet/tests/conftest.py @@ -0,0 +1,18 @@ +import pytest + + +def pytest_configure(config): + config.addinivalue_line("markers", "slow: mark tests as slow (include in run with --test-slow)") + + +def pytest_addoption(parser): + parser.addoption("--test-slow", action="store_true", default=False, help="Run slow tests") + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--test-slow"): + return + skip_slow = pytest.mark.skip(reason="Skipped due to the lack of '--test-slow' argument") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) diff --git a/python/prophet/tests/test_prophet.py b/python/prophet/tests/test_prophet.py index ab619e8..92d8ba5 100644 --- a/python/prophet/tests/test_prophet.py +++ b/python/prophet/tests/test_prophet.py @@ -9,11 +9,11 @@ from __future__ import print_function from __future__ import unicode_literals import os -import sys -from unittest import TestCase, skipUnless +from unittest import TestCase import numpy as np import pandas as pd +import pytest from prophet import Prophet from prophet.utilities import warm_start_params @@ -66,7 +66,7 @@ class TestProphet(TestCase): res = self.rmse(future['yhat'], test['y']) self.assertAlmostEqual(res, 23.44, places=2, msg="backend: {}".format(forecaster.stan_backend)) - @skipUnless("--test-slow" in sys.argv, "Skipped due to the lack of '--test-slow' argument") + @pytest.mark.slow def test_fit_sampling_predict(self): days = 30 N = DATA.shape[0] @@ -106,7 +106,7 @@ class TestProphet(TestCase): forecaster.fit(train) forecaster.predict(future) - @skipUnless("--test-slow" in sys.argv, "Skipped due to the lack of '--test-slow' argument") + @pytest.mark.slow def test_fit_predict_no_changepoints_mcmc(self): N = DATA.shape[0] train = DATA.head(N // 2) diff --git a/python/pyproject.toml b/python/pyproject.toml index 839bc68..b38a429 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,7 +1,61 @@ [build-system] requires = [ - "setuptools>=42", - "wheel", - "cmdstanpy>=1.0.4", + "setuptools>=64", + "wheel", + "cmdstanpy>=1.0.4", ] build-backend = "setuptools.build_meta" + +[project] +name = "prophet" +dynamic = ["version"] +description = "Automatic Forecasting Procedure" +readme = "README.md" +requires-python = ">=3.7" +dependencies = [ + "cmdstanpy>=1.0.4", + "numpy>=1.15.4", + "matplotlib>=2.0.0", + "pandas>=1.0.4", + "LunarCalendar>=0.0.9", + "convertdate>=2.1.2", + "holidays>=0.14.2", + "python-dateutil>=2.8.0", + "tqdm>=4.36.1", +] +authors = [ + {name = "Sean J. Taylor", email = "sjtz@pm.me"}, + {name = "Ben Letham", email = "bletham@fb.com"}, +] +maintainers = [ + {name = "Cuong Duong", email = "cuong.duong242@gmail.com"}, +] +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +[project.optional-dependencies] +dev = [ + "setuptools>=64", + "wheel", + "pytest", + "jupyterlab", + "nbconvert", + "plotly", +] +parallel = [ + "dask[dataframe]", + "distributed", +] + +[project.urls] +homepage = "https://facebook.github.io/prophet/" +documentation = "https://facebook.github.io/prophet/" +repository = "https://github.com/facebook/prophet" diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index 7181cc9..0000000 --- a/python/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -cmdstanpy>=1.0.4 -numpy>=1.15.4 -pandas>=1.0.4 -matplotlib>=2.0.0 -LunarCalendar>=0.0.9 -convertdate>=2.1.2 -holidays>=0.14.2 -setuptools>=42 -setuptools-git>=1.2 -python-dateutil>=2.8.0 -tqdm>=4.36.1 -wheel>=0.37.0 diff --git a/python/setup.py b/python/setup.py index ba44e86..5ee83ea 100644 --- a/python/setup.py +++ b/python/setup.py @@ -4,18 +4,15 @@ # LICENSE file in the root directory of this source tree. import os -import sys import platform from pathlib import Path from shutil import copy, copytree, rmtree from typing import List -from pkg_resources import add_activation_listener, normalize_path, require, working_set from setuptools import find_packages, setup, Extension from setuptools.command.build_ext import build_ext from setuptools.command.build_py import build_py -from setuptools.command.develop import develop -from setuptools.command.test import test as test_command +from setuptools.command.editable_wheel import editable_wheel MODEL_DIR = "stan" @@ -52,6 +49,7 @@ def prune_cmdstan(cmdstan_dir: str) -> None: rmtree(original_dir) temp_dir.rename(original_dir) + def repackage_cmdstan(): return os.environ.get("PROPHET_REPACKAGE_CMDSTAN", "").lower() not in ["false", "0"] @@ -59,6 +57,7 @@ def repackage_cmdstan(): def maybe_install_cmdstan_toolchain(): """Install C++ compilers required to build stan models on Windows machines.""" import cmdstanpy + try: cmdstanpy.utils.cxx_toolchain_path() except Exception: @@ -122,7 +121,6 @@ def build_cmdstan_model(target_dir): prune_cmdstan(cmdstan_dir) - def get_backends_from_env() -> List[str]: return os.environ.get("STAN_BACKEND", "CMDSTANPY").split(",") @@ -131,9 +129,10 @@ def build_models(target_dir): print("Compiling cmdstanpy model") build_cmdstan_model(target_dir) - if 'PYSTAN' in get_backends_from_env(): + if "PYSTAN" in get_backends_from_env(): raise ValueError("PyStan backend is not supported for Prophet >= 1.1") + class BuildPyCommand(build_py): """Custom build command to pre-compile Stan models.""" @@ -153,117 +152,33 @@ class BuildExtCommand(build_ext): pass -class DevelopCommand(develop): +class EditableWheel(editable_wheel): """Custom develop command to pre-compile Stan models in-place.""" def run(self): if not self.dry_run: - target_dir = os.path.join(self.setup_path, MODEL_TARGET_DIR) + target_dir = os.path.join(self.project_dir, MODEL_TARGET_DIR) self.mkpath(target_dir) build_models(target_dir) - develop.run(self) + editable_wheel.run(self) -class TestCommand(test_command): - user_options = [ - ("test-module=", "m", "Run 'test_suite' in specified module"), - ( - "test-suite=", - "s", - "Run single test, case or suite (e.g. 'module.test_suite')", - ), - ("test-runner=", "r", "Test runner to use"), - ("test-slow", "w", "Test slow suites (default off)"), - ] - test_slow = None - - def initialize_options(self): - super(TestCommand, self).initialize_options() - self.test_slow = False - - def finalize_options(self): - super(TestCommand, self).finalize_options() - if self.test_slow is None: - self.test_slow = getattr(self.distribution, "test_slow", False) - - """We must run tests on the build directory, not source.""" - - def with_project_on_sys_path(self, func): - # Ensure metadata is up-to-date - self.reinitialize_command("build_py", inplace=0) - self.run_command("build_py") - bpy_cmd = self.get_finalized_command("build_py") - build_path = normalize_path(bpy_cmd.build_lib) - - # Build extensions - self.reinitialize_command("egg_info", egg_base=build_path) - self.run_command("egg_info") - - self.reinitialize_command("build_ext", inplace=0) - self.run_command("build_ext") - - ei_cmd = self.get_finalized_command("egg_info") - - old_path = sys.path[:] - old_modules = sys.modules.copy() - - try: - sys.path.insert(0, normalize_path(ei_cmd.egg_base)) - working_set.__init__() - add_activation_listener(lambda dist: dist.activate()) - require("%s==%s" % (ei_cmd.egg_name, ei_cmd.egg_version)) - func() - finally: - sys.path[:] = old_path - sys.modules.clear() - sys.modules.update(old_modules) - working_set.__init__() - - -with open("README.md", "r", encoding="utf-8") as f: - long_description = f.read() - -with open("requirements.txt", "r") as f: - install_requires = f.read().splitlines() - about = {} here = Path(__file__).parent.resolve() -with open(here / "prophet" / "__version__.py", "r") as f: +with open(here / "prophet" / "__version__.py", "r") as f: exec(f.read(), about) setup( - name=about["__title__"], version=about["__version__"], - description=about["__description__"], - url=about["__url__"], - project_urls={ - "Source": "https://github.com/facebook/prophet", - }, - author=about["__author__"], - author_email=about["__author_email__"], - license=about["__license__"], packages=find_packages(), - install_requires=install_requires, - python_requires=">=3.7", zip_safe=False, include_package_data=True, ext_modules=[Extension("prophet.stan_model", [])], cmdclass={ "build_ext": BuildExtCommand, "build_py": BuildPyCommand, - "develop": DevelopCommand, - "test": TestCommand, + "editable_wheel": EditableWheel, }, test_suite="prophet.tests", - classifiers=[ - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - long_description=long_description, - long_description_content_type="text/markdown", )