diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 7ed2347..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Lint - -on: - push: - branches: - - "*" - pull_request: - branches: - - main - -permissions: - contents: read # to fetch code (actions/checkout) - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - test_lint: - name: Lint - # If using act to run CI locally the github object does not exist and the usual skipping should not be enforced - if: "github.repository == 'numpy/numpy-financial' || github.repository == ''" - runs-on: ubuntu-22.04 - strategy: - matrix: - python-version: ['3.11'] - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Python packages - run: | - python -m pip install --upgrade pip poetry - poetry env use ${{ matrix.python-version }} - poetry install --with=lint - - - name: Lint with Ruff - run: | - set -euo pipefail - # Tell us what version we are using - poetry run ruff version - # Check the source file, ignore type annotations (ANN) for now. - poetry run ruff check numpy_financial/ --ignore F403 --select E,F,B,I - # Check the test and benchmark files - poetry run ruff check tests/ benchmarks/ --select E,F,B,I diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index e5b576c..2812b50 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,10 +1,13 @@ -name: Python package +name: Test package on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -el {0} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -12,14 +15,27 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: conda-incubator/setup-miniconda@v3 with: + auto-update-conda: true python-version: ${{ matrix.python-version }} - - name: Install dependencies + activate-environment: numpy-financial-dev + environment-file: environment.yml + auto-activate-base: false + - name: Conda metadata run: | - python -m pip install --upgrade pip poetry - poetry env use ${{ matrix.python-version }} - poetry install --with=test + conda info + conda list + - name: Lint + run: | + set -euo pipefail + # Tell us what version we are using + ruff version + # Check the source file, ignore type annotations (ANN) for now. + ruff check numpy_financial/ benchmarks/ --ignore F403 --select E,F,B,I + - name: Build project + run: | + spin build -v - name: Test with pytest run: | - poetry run pytest --doctest-modules + spin test -- --doctest-modules diff --git a/.gitignore b/.gitignore index 8b3cdae..f495f60 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ GTAGS ################ # setup.py working directory build +build-install # sphinx build directory _build # setup.py dist directory @@ -112,3 +113,7 @@ poetry.lock # Things specific to this project # ################################### doc/source/_api_stubs + +# Misc files that we do not require # +.asv/ +.hypothesis/ diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..921cec8 --- /dev/null +++ b/environment.yml @@ -0,0 +1,31 @@ +# To use: +# $ conda env create -f environment.yml # `mamba` works too for this command +# $ conda activate numpy-financial-dev +# +name: numpy-financial-dev +channels: + - conda-forge +dependencies: + # Runtime dependencies + - python + - numpy + # Build + - cython>=3.0.9 + - compilers + - meson + - meson-python + - ninja + # Tests + - pytest + - pytest-xdist + - asv>=0.6.0 + - hypothesis + # Docs + - myst-parser + - numpydoc + - pydata-sphinx-theme>=0.15.0 + - spin + # Lint + - ruff>=0.3.0 + # Benchmarks + - asv>=0.6.0 diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..cb6a213 --- /dev/null +++ b/meson.build @@ -0,0 +1,19 @@ +project( + 'numpy-financial', + 'cython', 'c', + version: '2.0.0.dev', + license: 'BSD-3', + meson_version: '>=1.4.0', +) + +cc = meson.get_compiler('c') +cy = meson.get_compiler('cython') + +if not cy.version().version_compare('>=3.0.9') + error('NumPy-Financial requires Cython >= 3.0.9') +endif + +py = import('python').find_installation(pure: false) +py_dep = py.dependency() + +subdir('numpy_financial') diff --git a/numpy_financial/_cfinancial.pyx b/numpy_financial/_cfinancial.pyx new file mode 100644 index 0000000..77c195b --- /dev/null +++ b/numpy_financial/_cfinancial.pyx @@ -0,0 +1,21 @@ +from libc.math cimport NAN +cimport cython + +@cython.boundscheck(False) +@cython.cdivision(True) +def npv(const double[::1] rates, const double[:, ::1] values, double[:, ::1] out): + cdef: + Py_ssize_t i, j, t + double acc + + with nogil: + for i in range(rates.shape[0]): + for j in range(values.shape[0]): + acc = 0.0 + for t in range(values.shape[1]): + if rates[i] == -1.0: + acc = NAN + break + else: + acc = acc + values[j, t] / ((1.0 + rates[i]) ** t) + out[i, j] = acc \ No newline at end of file diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 534230f..03f3f1b 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -13,9 +13,10 @@ from decimal import Decimal -import numba as nb import numpy as np +from . import _cfinancial + __all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate', 'irr', 'npv', 'mirr', 'NoRealSolutionError', 'IterationsExceededError'] @@ -844,20 +845,6 @@ def irr(values, *, raise_exceptions=False): return eirr[np.argmin(abs_eirr)] -@nb.njit -def _npv_native(rates, values, out): - for i in range(rates.shape[0]): - for j in range(values.shape[0]): - acc = 0.0 - for t in range(values.shape[1]): - if rates[i] == -1.0: - acc = np.nan - break - else: - acc += values[j, t] / ((1.0 + rates[i]) ** t) - out[i, j] = acc - - def npv(rate, values): r"""Return the NPV (Net Present Value) of a cash flow series. @@ -935,8 +922,8 @@ def npv(rate, values): [-2798.19, -3612.24], [-2884.3 , -3710.74]]) """ - values_inner = np.atleast_2d(values) - rate_inner = np.atleast_1d(rate) + values_inner = np.atleast_2d(values).astype(np.float64) + rate_inner = np.atleast_1d(rate).astype(np.float64) if rate_inner.ndim != 1: msg = "invalid shape for rates. Rate must be either a scalar or 1d array" @@ -948,7 +935,7 @@ def npv(rate, values): output_shape = _get_output_array_shape(rate_inner, values_inner) out = np.empty(output_shape) - _npv_native(rate_inner, values_inner, out) + _cfinancial.npv(rate_inner, values_inner, out) return _ufunc_like(out) diff --git a/numpy_financial/meson.build b/numpy_financial/meson.build new file mode 100644 index 0000000..6fe3ce5 --- /dev/null +++ b/numpy_financial/meson.build @@ -0,0 +1,18 @@ +py.extension_module( + '_cfinancial', + '_cfinancial.pyx', + install: true, + subdir: 'numpy_financial', +) + +python_sources = [ + '__init__.py', + '_financial.py', +] + +py.install_sources( + python_sources, + subdir: 'numpy_financial', +) + +install_subdir('tests', install_dir: py.get_install_dir() / 'numpy_financial') diff --git a/tests/test_financial.py b/numpy_financial/tests/test_financial.py similarity index 100% rename from tests/test_financial.py rename to numpy_financial/tests/test_financial.py diff --git a/pyproject.toml b/pyproject.toml index 517910f..d1cfc52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,15 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - +build-backend = "mesonpy" +requires = [ + "meson-python>=0.15.0", + "Cython>=3.0.9", + "numpy>=1.23.5", +] -[tool.poetry] +[project] name = "numpy-financial" version = "2.0.0" +requires-python = ">=3.10" description = "Simple financial functions" license = "BSD-3-Clause" authors = ["Travis E. Oliphant et al."] @@ -34,30 +38,41 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", ] -packages = [{include = "numpy_financial"}] - -[tool.poetry.dependencies] -python = "^3.10" -numpy = "^1.23" -numba = "^0.59.1" - -[tool.poetry.group.test.dependencies] -pytest = "^8.0" -hypothesis = {extras = ["numpy"], version = "^6.99.11"} -pytest-xdist = {extras = ["psutil"], version = "^3.5.0"} - - -[tool.poetry.group.docs.dependencies] -sphinx = "^7.0" -numpydoc = "^1.5" -pydata-sphinx-theme = "^0.15" -myst-parser = "^2.0.0" - +[project.optional-dependencies] +test = [ + "pytest", + "pytest-xdist", + "hypothesis", +] +doc = [ + "sphinx>=7.0", + "numpydoc>=1.5", + "pydata-sphinx-theme>=0.15", + "myst-parser>=2.0.0", +] +dev = [ + "ruff>=0.3.0", + "asv>=0.6.0", +] -[tool.poetry.group.lint.dependencies] -ruff = "^0.3" +[tool.spin] +package = 'numpy_financial' -[tool.poetry.group.bench.dependencies] -asv = "^0.6" +[tool.spin.commands] +"Build" = [ + "spin.cmds.meson.build", + "spin.cmds.meson.test", + "spin.cmds.build.sdist", + "spin.cmds.pip.install", +] +"Documentation" = [ + "spin.cmds.meson.docs" +] +"Environments" = [ + "spin.cmds.meson.shell", + "spin.cmds.meson.ipython", + "spin.cmds.meson.python", + "spin.cmds.meson.run" +] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000