diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 4d190dd3..b07cfcb3 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -1,52 +1,132 @@ -name: CI-Tests +name: enterprise CI targets on: push: -# branches: -# - master + branches: [ master ] pull_request: + branches: [ master ] + release: + types: + - published + jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable - build: - runs-on: ubuntu-latest + tests: + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: [ubuntu-latest, macos-latest] python-version: [3.6, 3.7, 3.8] + steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Pre-install + - name: Install non-python dependencies on mac + if: runner.os == 'macOS' + run: | + brew unlink gcc && brew link gcc + brew install automake suite-sparse + curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh + - name: Install non-python dependencies on linux + if: runner.os == 'Linux' run: | sudo apt-get install libsuitesparse-dev curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh - python -m pip install --upgrade pip - - name: Install Dependencies + - name: Install dependencies and package run: | - cat requirements_dev.txt | xargs -n 1 -L 1 pip install - cat requirements.txt | xargs -n 1 -L 1 pip install - python setup.py install + python -m pip install --upgrade pip setuptools wheel + python -m pip install flake8 pytest black pytest-cov + python -m pip install numpy cython + python -m pip install -e . - name: Display Python, pip, setuptools, and all installed versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" - pip freeze - - name: Run Tests and Lint - run: | - pytest --cov-config=.coveragerc --cov=enterprise --cov-report=xml - make lint + python -m pip freeze + - name: Run lint + run: make lint + - name: Test with pytest + run: make test - name: Codecov uses: codecov/codecov-action@v1 + #with: + # fail_ci_if_error: true + + + build: + needs: [tests] + name: Build source distribution + runs-on: ubuntu-latest + if: github.event_name == 'release' + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 with: - fail_ci_if_error: true + python-version: "3.7" + - name: Install non-python dependencies on linux + run: | + sudo apt-get install libsuitesparse-dev + curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh + - name: Build + run: | + python -m pip install --upgrade pip setuptools wheel + pip install numpy==1.16.3 cython + pip install -r requirements.txt + make dist + - name: Test the sdist + run: | + mkdir tmp + cd tmp + python -m venv venv-sdist + venv-sdist/bin/python -m pip install --upgrade pip setuptools wheel + venv-sdist/bin/python -m pip install numpy cython + venv-sdist/bin/python -m pip install ../dist/enterprise*.tar.gz + venv-sdist/bin/python -c "import enterprise;print(enterprise.__version__)" + - name: Test the wheel + run: | + mkdir tmp2 + cd tmp2 + python -m venv venv-wheel + venv-wheel/bin/python -m pip install --upgrade pip setuptools + venv-wheel/bin/python -m pip install numpy cython + venv-wheel/bin/python -m pip install ../dist/enterprise*.whl + venv-wheel/bin/python -c "import enterprise;print(enterprise.__version__)" + - uses: actions/upload-artifact@v2 + with: + name: dist + path: dist/* + + + deploy: + needs: [tests, build] + runs-on: ubuntu-latest + if: github.event_name == 'release' + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Download wheel/dist from build + uses: actions/download-artifact@v2 + with: + name: dist + path: dist + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + twine upload dist/* \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..b52cc737 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,6 @@ +[settings] +include_trailing_comma=True +indent=' ' +dedup_headings=True +line_length=120 +multi_line_output=3 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6208b137..b200a92b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,3 +9,8 @@ repos: hooks: - id: flake8 args: ["--config=.flake8"] +- repo: https://github.com/timothycrosley/isort + rev: 5.6.1 + hooks: + - id: isort + diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 27469e39..ad6c38ec 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -72,14 +72,17 @@ Ready to contribute? Here's how to set up `enterprise` for local development. $ git pull upstream master -4. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +4. This is how you set up your fork for local development: + + .. note:: + You will need to have ``tempo`` and ``suitesparse`` installed before + running the commands below. + + :: - $ mkvirtualenv enterprise $ cd enterprise/ - $ pip install -r requirements_dev.txt - $ pip install -r requirements.txt - $ pip install libstempo --install-option="--with-tempo2=$TEMPO2" - $ python setup.py develop + $ make init + $ source .enterprise/bin/activate 5. Create a branch for local development:: diff --git a/Makefile b/Makefile index e15585cf..93f3e3cf 100644 --- a/Makefile +++ b/Makefile @@ -28,12 +28,14 @@ help: init: @python3 -m venv .enterprise --prompt enterprise - @./.enterprise/bin/python3 -m pip install numpy + @./.enterprise/bin/python3 -m pip install -U pip setuptools wheel + @./.enterprise/bin/python3 -m pip install numpy cython @./.enterprise/bin/python3 -m pip install -r requirements.txt -U @./.enterprise/bin/python3 -m pip install -r requirements_dev.txt -U - @./.enterprise/bin/python3 -m pip install libstempo --install-option="--with-tempo2=$(TEMPO2)" @./.enterprise/bin/python3 -m pre_commit install --install-hooks --overwrite @./.enterprise/bin/python3 -m pip install -e . + @echo "run source .enterprise/bin/activate to activate environment" + format: black . @@ -61,24 +63,20 @@ clean-test: ## remove test and coverage artifacts rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ + rm -rf coverage.xml -test: ## run tests quickly with the default Python - pytest -v --full-trace --cov-config .coveragerc --cov=enterprise tests - -#test-all: ## run tests on every Python version with tox -# tox - -coverage: ## check code coverage quickly with the default Python - coverage run --source enterprise setup.py test +COV_COVERAGE_PERCENT ?= 85 +test: lint ## run tests quickly with the default Python + pytest -v --durations=10 --full-trace --cov-report html --cov-report xml \ + --cov-config .coveragerc --cov-fail-under=$(COV_COVERAGE_PERCENT) \ + --cov=enterprise tests - coverage report -m - coverage html +coverage: test ## check code coverage quickly with the default Python $(BROWSER) htmlcov/index.html -jupyter-docs: +jupyter-docs: ## biuld jupyter notebook docs jupyter nbconvert --template docs/nb-rst.tpl --to rst docs/_static/notebooks/*.ipynb --output-dir docs/ cp -r docs/_static/notebooks/img docs/ - #jupyter nbconvert --template docs/nb-rst.tpl --to rst docs/_static/notebooks/tutorials/*.ipynb --output-dir docs/tutorials/ docs: ## generate Sphinx HTML documentation, including API docs rm -f docs/enterprise*.rst @@ -92,14 +90,7 @@ docs: ## generate Sphinx HTML documentation, including API docs servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . -release: clean ## package and upload a release - python setup.py sdist upload - python setup.py bdist_wheel upload - dist: clean ## builds source and wheel package python setup.py sdist python setup.py bdist_wheel ls -l dist - -install: clean ## install the package to the active Python's site-packages - python setup.py install diff --git a/enterprise/pulsar.py b/enterprise/pulsar.py index bcf69215..8f8c3a43 100644 --- a/enterprise/pulsar.py +++ b/enterprise/pulsar.py @@ -6,8 +6,8 @@ import logging import os -import astropy.units as u import astropy.constants as const +import astropy.units as u import numpy as np from ephem import Ecliptic, Equatorial @@ -19,19 +19,21 @@ except: import pickle +logger = logging.getLogger(__name__) + try: import libstempo as t2 except ImportError: - print("Ooh, no libstempo?") + logger.warning("libstempo not installed. Will use PINT instead.") # pragma: no cover t2 = None try: import pint - from pint.toa import TOAs - from pint.models import get_model_and_toas, TimingModel + from pint.models import TimingModel, get_model_and_toas from pint.residuals import Residuals as resids + from pint.toa import TOAs except ImportError: - print("Cannot import PINT? Meh...") + logger.warning("PINT not installed. Will use libstempo instead.") # pragma: no cover pint = None @@ -39,9 +41,6 @@ err_msg = "Must have either PINT or libstempo timing package installed" raise ImportError(err_msg) -# logging.basicConfig(format="%(levelname)s: %(name)s: %(message)s", level=logging.INFO) -logger = logging.getLogger(__name__) - def get_maxobs(timfile): """Utility function to return number of lines in tim file. @@ -119,7 +118,7 @@ def sort_data(self): """Sort data by time.""" if self._sort: self._isort = np.argsort(self._toas, kind="mergesort") - self._iisort = np.zeros(len(self._isort), dtype=np.int) + self._iisort = np.zeros(len(self._isort), dtype=int) for ii, p in enumerate(self._isort): self._iisort[p] = ii else: @@ -586,5 +585,5 @@ def Pulsar(*args, **kwargs): t2pulsar = t2.tempopulsar(relparfile, reltimfile, maxobs=maxobs, ephem=ephem, clk=clk) os.chdir(cwd) return Tempo2Pulsar(t2pulsar, sort=sort, drop_t2pulsar=drop_t2pulsar, planets=planets) - else: - print("Unknown arguments {}".format(args)) + + raise ValueError("Unknown arguments {}".format(args)) diff --git a/enterprise/signals/selections.py b/enterprise/signals/selections.py index f7de81d0..daeac4e9 100644 --- a/enterprise/signals/selections.py +++ b/enterprise/signals/selections.py @@ -2,7 +2,6 @@ """Contains various selection functions to mask parameters by backend flags, time-intervals, etc.""" -from __future__ import absolute_import, division, print_function, unicode_literals import functools import inspect diff --git a/enterprise/signals/signal_base.py b/enterprise/signals/signal_base.py index f4e3c07f..306db7e2 100644 --- a/enterprise/signals/signal_base.py +++ b/enterprise/signals/signal_base.py @@ -3,14 +3,13 @@ Defines the signal base classes and metaclasses. All signals will then be derived from these base classes. """ -from __future__ import absolute_import, division, print_function, unicode_literals - import collections try: from collections.abc import Sequence except: from collections import Sequence + import itertools import logging @@ -546,10 +545,11 @@ def _setpulsarcliques(self, slices, phis): try: self._cliques[slices[sc].start + phiind] = self._clcount self._clcount = self._clcount + 1 - except: - print(self._cliques.shape) - print("phiind", phiind, len(phiind)) - print(slices) + except Exception: # pragma: no cover + logger.exception("Exception raised in computing cliques") + logger.info(self._cliques.shape) + logger.info("phiind", phiind, len(phiind)) + logger.info(slices) raise def get_phi(self, params, cliques=False): @@ -684,7 +684,7 @@ def summary(self, include_params=True, to_stdout=False): summary += "Fixed params: {}\n".format(copcount) summary += "Number of pulsars: {}\n".format(len(self._signalcollections)) if to_stdout: - print(summary) + logger.info(summary) else: return summary diff --git a/enterprise/signals/utils.py b/enterprise/signals/utils.py index fe1f1ccc..045ff9d4 100644 --- a/enterprise/signals/utils.py +++ b/enterprise/signals/utils.py @@ -4,7 +4,7 @@ functions for use in other modules. """ -from __future__ import absolute_import, division, print_function, unicode_literals +import logging import numpy as np import scipy.linalg as sl @@ -16,22 +16,23 @@ import enterprise from enterprise import constants as const -from enterprise.signals.parameter import function -from enterprise.signals.gp_priors import powerlaw, turnover # noqa: F401 from enterprise import signals as sigs # noqa: F401 from enterprise.signals.gp_bases import ( # noqa: F401 - createfourierdesignmatrix_red, createfourierdesignmatrix_dm, createfourierdesignmatrix_env, - createfourierdesignmatrix_ephem, createfourierdesignmatrix_eph, + createfourierdesignmatrix_ephem, + createfourierdesignmatrix_red, ) +from enterprise.signals.gp_priors import powerlaw, turnover # noqa: F401 +from enterprise.signals.parameter import function +logger = logging.getLogger(__name__) try: from sksparse.cholmod import cholesky -except: - print("You'll need sksparse for get_coefficients() with common signals!") +except ImportError: # pragma no cover + logger.warning("sksparse not installed. You'll need sksparse for get_coefficients() with common signals!") def get_coefficients(pta, params, n=1, phiinv_method="cliques", common_sparse=False): diff --git a/enterprise/signals/white_signals.py b/enterprise/signals/white_signals.py index d6703c1d..95b78871 100644 --- a/enterprise/signals/white_signals.py +++ b/enterprise/signals/white_signals.py @@ -3,7 +3,6 @@ defined as the class of signals that only modifies the white noise matrix `N`. """ -from __future__ import absolute_import, division, print_function, unicode_literals import numpy as np import scipy.sparse diff --git a/pyproject.toml b/pyproject.toml index 571df8af..6dcc2e86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,11 @@ exclude = ''' | .enterprise | enterprise/src )/ - | foo.py # also separately exclude a file named foo.py in - # the root of the project ) -''' \ No newline at end of file +''' +[build-system] +requires = [ + "setuptools>=40.8.0", + "wheel", +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py index f1575ce0..5ab831a4 100644 --- a/setup.py +++ b/setup.py @@ -10,14 +10,21 @@ history = history_file.read() requirements = [ - # TODO: put package requirements here + "numpy>=1.16.3", + "scipy>=1.2.0", + "ephem>=3.7.6.0", + "jplephem==2.6", + "healpy>=1.14.0", + "scikit-sparse>=0.4.2", + "pint-pulsar>=0.8.2", + "libstempo>=2.4.0", ] test_requirements = [] setup( - name="enterprise", + name="enterprise-pulsar", version="3.0.0", description="ENTERPRISE (Enhanced Numerical Toolbox Enabling a Robust PulsaR Inference SuitE)", long_description=readme + "\n\n" + history, @@ -28,6 +35,7 @@ package_dir={"enterprise": "enterprise"}, include_package_data=True, package_data={"enterprise": ["datafiles/*", "datafiles/ephemeris/*", "datafiles/ng9/*", "datafiles/mdc_open1/*"]}, + python_requires=">=3.6, <3.9", install_requires=requirements, license="MIT license", zip_safe=False, diff --git a/tests/test_hierarchical_parameter.py b/tests/test_hierarchical_parameter.py index b84f7bc7..9e767a0c 100644 --- a/tests/test_hierarchical_parameter.py +++ b/tests/test_hierarchical_parameter.py @@ -8,7 +8,6 @@ Tests for hierarchical parameter functionality """ -from __future__ import division import unittest diff --git a/tests/test_pulsar.py b/tests/test_pulsar.py index 30acdfbf..acdee44b 100644 --- a/tests/test_pulsar.py +++ b/tests/test_pulsar.py @@ -10,6 +10,8 @@ """ +import os +import shutil import unittest import numpy as np @@ -31,6 +33,10 @@ def setUpClass(cls): # initialize Pulsar class cls.psr = Pulsar(datadir + "/B1855+09_NANOGrav_9yv1.gls.par", datadir + "/B1855+09_NANOGrav_9yv1.tim") + @classmethod + def tearDownClass(cls): + shutil.rmtree("pickle_dir") + def test_residuals(self): """Check Residual shape.""" @@ -117,6 +123,8 @@ def test_to_pickle(self): with open("B1855+09.pkl", "rb") as f: pkl_psr = pickle.load(f) + os.remove("B1855+09.pkl") + assert np.allclose(self.psr.residuals, pkl_psr.residuals, rtol=1e-10) self.psr.to_pickle("pickle_dir") @@ -134,6 +142,12 @@ def test_wrong_input(self): msg = "Cannot find parfile wrong.par or timfile wrong.tim!" self.assertTrue(msg in context.exception) + def test_value_error(self): + """Test exception when unknown argument is given""" + + with self.assertRaises(ValueError): + Pulsar(datadir + "/B1855+09_NANOGrav_9yv1.gls.par", datadir + "/B1855+09_NANOGrav_9yv1.time") + class TestPulsarPint(TestPulsar): @classmethod diff --git a/tests/test_vector_parameter.py b/tests/test_vector_parameter.py index 77ae9faf..3d4b31d6 100644 --- a/tests/test_vector_parameter.py +++ b/tests/test_vector_parameter.py @@ -8,7 +8,6 @@ Tests for vector parameter functionality """ -from __future__ import division import unittest