Skip to content

CI/STY: Move to Ruff #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ jobs:
run: |
python -m pip install --upgrade pip poetry
poetry env use ${{ matrix.python-version }}
poetry install --with=test --with=lint
poetry install --with=lint

- name: Lint with flake8
- name: Lint with Ruff
run: |
set -euo pipefail
# stop the build if there are Python syntax errors or undefined names
poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# The GitHub editor is 127 chars wide
poetry run flake8 . --ignore=F401,F403,W503,E226 --count --max-complexity=10 --max-line-length=127 --statistics
# 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,Q000,PLR0913,ERA001,TRY003,EM101,EM102,RET505,D203,D213,ANN --select ALL
5 changes: 5 additions & 0 deletions numpy_financial/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""__init__ file.

This file allows us to import the public functions from NumPy-Financial and
tells us the version we are using.
"""

__version__ = "1.1.0.dev0"

Expand Down
128 changes: 63 additions & 65 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Some simple financial calculations
"""Some simple financial calculations.

patterned after spreadsheet computations.

Expand All @@ -10,15 +10,14 @@
Functions support the :class:`decimal.Decimal` type unless
otherwise stated.
"""
from __future__ import absolute_import, division, print_function

from decimal import Decimal

import numpy as np

__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
'irr', 'npv', 'mirr',
'NoRealSolutionException', 'IterationsExceededException']
'NoRealSolutionError', 'IterationsExceededError']

_when_to_num = {'end': 0, 'begin': 1,
'e': 0, 'b': 1,
Expand All @@ -28,14 +27,12 @@
'finish': 0}


class NoRealSolutionException(Exception):
""" No real solution to the problem. """
pass
class NoRealSolutionError(Exception):
"""No real solution to the problem."""


class IterationsExceededException(Exception):
""" Maximum number of iterations reached. """
pass
class IterationsExceededError(Exception):
"""Maximum number of iterations reached."""


def _convert_when(when):
Expand All @@ -50,8 +47,7 @@ def _convert_when(when):


def fv(rate, nper, pmt, pv, when='end'):
"""
Compute the future value.
"""Compute the future value.

Given:
* a present value, `pv`
Expand Down Expand Up @@ -143,11 +139,11 @@ def fv(rate, nper, pmt, pv, when='end'):
fv_array[zero] = -(pv[zero] + pmt[zero] * nper[zero])

rate_nonzero = rate[nonzero]
temp = (1 + rate_nonzero)**nper[nonzero]
temp = (1 + rate_nonzero) ** nper[nonzero]
fv_array[nonzero] = (
- pv[nonzero] * temp
- pmt[nonzero] * (1 + rate_nonzero * when[nonzero]) / rate_nonzero
* (temp - 1)
- pv[nonzero] * temp
- pmt[nonzero] * (1 + rate_nonzero * when[nonzero]) / rate_nonzero
* (temp - 1)
)

if np.ndim(fv_array) == 0:
Expand All @@ -158,8 +154,7 @@ def fv(rate, nper, pmt, pv, when='end'):


def pmt(rate, nper, pv, fv=0, when='end'):
"""
Compute the payment against loan principal plus interest.
"""Compute the payment against loan principal plus interest.

Given:
* a present value, `pv` (e.g., an amount borrowed)
Expand Down Expand Up @@ -244,17 +239,16 @@ def pmt(rate, nper, pv, fv=0, when='end'):
"""
when = _convert_when(when)
(rate, nper, pv, fv, when) = map(np.array, [rate, nper, pv, fv, when])
temp = (1 + rate)**nper
temp = (1 + rate) ** nper
mask = (rate == 0)
masked_rate = np.where(mask, 1, rate)
fact = np.where(mask != 0, nper,
(1 + masked_rate*when)*(temp - 1)/masked_rate)
return -(fv + pv*temp) / fact
(1 + masked_rate * when) * (temp - 1) / masked_rate)
return -(fv + pv * temp) / fact


def nper(rate, pmt, pv, fv=0, when='end'):
"""
Compute the number of periodic payments.
"""Compute the number of periodic payments.

:class:`decimal.Decimal` type is not supported.

Expand Down Expand Up @@ -321,8 +315,8 @@ def nper(rate, pmt, pv, fv=0, when='end'):
nonzero_rate = rate[nonzero]
z = pmt[nonzero] * (1 + nonzero_rate * when[nonzero]) / nonzero_rate
nper_array[nonzero] = (
np.log((-fv[nonzero] + z) / (pv[nonzero] + z))
/ np.log(1 + nonzero_rate)
np.log((-fv[nonzero] + z) / (pv[nonzero] + z))
/ np.log(1 + nonzero_rate)
)

return nper_array
Expand All @@ -332,13 +326,11 @@ def _value_like(arr, value):
entry = arr.item(0)
if isinstance(entry, Decimal):
return Decimal(value)
else:
return np.array(value, dtype=arr.dtype).item(0)
return np.array(value, dtype=arr.dtype).item(0)


def ipmt(rate, per, nper, pv, fv=0, when='end'):
"""
Compute the interest portion of a payment.
"""Compute the interest portion of a payment.

Parameters
----------
Expand Down Expand Up @@ -439,7 +431,7 @@ def ipmt(rate, per, nper, pv, fv=0, when='end'):
# If paying at the beginning we need to discount by one period.
per_gt_1_and_begin = (when == 1) & (per > 1)
ipmt_array[per_gt_1_and_begin] = (
ipmt_array[per_gt_1_and_begin] / (1 + rate[per_gt_1_and_begin])
ipmt_array[per_gt_1_and_begin] / (1 + rate[per_gt_1_and_begin])
)

if np.ndim(ipmt_array) == 0:
Expand All @@ -450,7 +442,8 @@ def ipmt(rate, per, nper, pv, fv=0, when='end'):


def _rbl(rate, per, pmt, pv, when):
"""
"""Remaining balance on loan.

This function is here to simply have a different name for the 'fv'
function to not interfere with the 'fv' keyword argument within the 'ipmt'
function. It is the 'remaining balance on loan' which might be useful as
Expand All @@ -460,8 +453,7 @@ def _rbl(rate, per, pmt, pv, when):


def ppmt(rate, per, nper, pv, fv=0, when='end'):
"""
Compute the payment against loan principal.
"""Compute the payment against loan principle.

Parameters
----------
Expand Down Expand Up @@ -489,8 +481,7 @@ def ppmt(rate, per, nper, pv, fv=0, when='end'):


def pv(rate, nper, pmt, fv=0, when='end'):
"""
Compute the present value.
"""Compute the present value.

Given:
* a future value, `fv`
Expand Down Expand Up @@ -579,9 +570,10 @@ def pv(rate, nper, pmt, fv=0, when='end'):
"""
when = _convert_when(when)
(rate, nper, pmt, fv, when) = map(np.asarray, [rate, nper, pmt, fv, when])
temp = (1+rate)**nper
fact = np.where(rate == 0, nper, (1+rate*when)*(temp-1)/rate)
return -(fv + pmt*fact)/temp
temp = (1 + rate) ** nper
fact = np.where(rate == 0, nper, (1 + rate * when) * (temp - 1) / rate)
return -(fv + pmt * fact) / temp


# Computed with Sage
# (y + (r + 1)^n*x + p*((r + 1)^n - 1)*(r*w + 1)/r)/(n*(r + 1)^(n - 1)*x -
Expand All @@ -592,13 +584,13 @@ def pv(rate, nper, pmt, fv=0, when='end'):
def _g_div_gp(r, n, p, x, y, w):
# Evaluate g(r_n)/g'(r_n), where g =
# fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate * ((1+rate)**nper - 1)
t1 = (r+1)**n
t2 = (r+1)**(n-1)
g = y + t1*x + p*(t1 - 1) * (r*w + 1) / r
gp = (n*t2*x
- p*(t1 - 1) * (r*w + 1) / (r**2)
+ n*p*t2 * (r*w + 1) / r
+ p*(t1 - 1) * w/r)
t1 = (r + 1) ** n
t2 = (r + 1) ** (n - 1)
g = y + t1 * x + p * (t1 - 1) * (r * w + 1) / r
gp = (n * t2 * x
- p * (t1 - 1) * (r * w + 1) / (r ** 2)
+ n * p * t2 * (r * w + 1) / r
+ p * (t1 - 1) * w / r)
return g / gp


Expand All @@ -609,9 +601,18 @@ def _g_div_gp(r, n, p, x, y, w):
# where
# g(r) is the formula
# g'(r) is the derivative with respect to r.
def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100, *, raise_exceptions=False):
"""
Compute the rate of interest per period.
def rate(
nper,
pmt,
pv,
fv,
when='end',
guess=None,
tol=None,
maxiter=100,
*,
raise_exceptions=False):
"""Compute the rate of interest per period.

Parameters
----------
Expand Down Expand Up @@ -675,29 +676,28 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100, *, ra
close = False
while (iterator < maxiter) and not np.all(close):
rnp1 = rn - _g_div_gp(rn, nper, pmt, pv, fv, when)
diff = abs(rnp1-rn)
diff = abs(rnp1 - rn)
close = diff < tol
iterator += 1
rn = rnp1

if not np.all(close):
if np.isscalar(rn):
if raise_exceptions:
raise IterationsExceededException('Maximum number of iterations exceeded.')
raise IterationsExceededError('Maximum number of iterations exceeded.')
return default_type(np.nan)
else:
# Return nan's in array of the same shape as rn
# where the solution is not close to tol.
if raise_exceptions:
raise IterationsExceededException(f'Maximum number of iterations exceeded in '
f'{len(close)-close.sum()} rate(s).')
raise IterationsExceededError(f'Maximum iterations exceeded in '
f'{len(close) - close.sum()} rate(s).')
rn[~close] = np.nan
return rn


def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
"""
Return the Internal Rate of Return (IRR).
r"""Return the Internal Rate of Return (IRR).

This is the "average" periodically compounded rate of return
that gives a net present value of 0.0; for a more complete explanation,
Expand Down Expand Up @@ -781,8 +781,8 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
same_sign = np.all(values > 0) if values[0] > 0 else np.all(values < 0)
if same_sign:
if raise_exceptions:
raise NoRealSolutionException('No real solution exists for IRR since all '
'cashflows are of the same sign.')
raise NoRealSolutionError('No real solution exists for IRR since all '
'cashflows are of the same sign.')
return np.nan

# If no value is passed for `guess`, then make a heuristic estimate
Expand Down Expand Up @@ -820,14 +820,13 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
g -= delta

if raise_exceptions:
raise IterationsExceededException('Maximum number of iterations exceeded.')
raise IterationsExceededError('Maximum number of iterations exceeded.')

return np.nan


def npv(rate, values):
"""
Returns the NPV (Net Present Value) of a cash flow series.
r"""Return the NPV (Net Present Value) of a cash flow series.

Parameters
----------
Expand Down Expand Up @@ -905,8 +904,7 @@ def npv(rate, values):


def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
"""
Modified internal rate of return.
r"""Return the modified internal rate of return.

Parameters
----------
Expand Down Expand Up @@ -943,9 +941,9 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
neg = values < 0
if not (pos.any() and neg.any()):
if raise_exceptions:
raise NoRealSolutionException('No real solution exists for MIRR since'
' all cashflows are of the same sign.')
raise NoRealSolutionError('No real solution exists for MIRR since'
' all cashflows are of the same sign.')
return np.nan
numer = np.abs(npv(reinvest_rate, values*pos))
denom = np.abs(npv(finance_rate, values*neg))
return (numer/denom)**(1/(n - 1))*(1 + reinvest_rate) - 1
numer = np.abs(npv(reinvest_rate, values * pos))
denom = np.abs(npv(finance_rate, values * neg))
return (numer / denom) ** (1 / (n - 1)) * (1 + reinvest_rate) - 1
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ numpydoc = "^1.5"
pydata-sphinx-theme = "^0.14.3"



[tool.poetry.group.lint.dependencies]
flake8 = "*"
ruff = "^0.1.6"

10 changes: 5 additions & 5 deletions tests/test_financial.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def test_rate_maximum_iterations_exception_scalar(self):
# Test that if the maximum number of iterations is reached,
# then npf.rate returns IterationsExceededException
# when raise_exceptions is set to True.
assert_raises(npf.IterationsExceededException, npf.rate, Decimal(12.0),
assert_raises(npf.IterationsExceededError, npf.rate, Decimal(12.0),
Decimal(400.0), Decimal(10000.0), Decimal(5000.0),
raise_exceptions=True)

Expand All @@ -152,7 +152,7 @@ def test_rate_maximum_iterations_exception_array(self):
pmt = 0
pv = [-593.06, -4725.38, -662.05, -428.78, -13.65]
fv = [214.07, 4509.97, 224.11, 686.29, -329.67]
assert_raises(npf.IterationsExceededException, npf.rate, nper,
assert_raises(npf.IterationsExceededError, npf.rate, nper,
pmt, pv, fv,
raise_exceptions=True)

Expand Down Expand Up @@ -285,7 +285,7 @@ def test_mirr_no_real_solution_exception(self):
# have the same sign, then npf.mirr returns NoRealSolutionException
# when raise_exceptions is set to True.
val = [39000, 30000, 21000, 37000, 46000]
assert_raises(npf.NoRealSolutionException, npf.mirr, val, 0.10, 0.12, raise_exceptions=True)
assert_raises(npf.NoRealSolutionError, npf.mirr, val, 0.10, 0.12, raise_exceptions=True)


class TestNper:
Expand Down Expand Up @@ -717,12 +717,12 @@ def test_irr_no_real_solution_exception(self):
# have the same sign, then npf.irr returns NoRealSolutionException
# when raise_exceptions is set to True.
cashflows = numpy.array([40000, 5000, 8000, 12000, 30000])
assert_raises(npf.NoRealSolutionException, npf.irr, cashflows, raise_exceptions=True)
assert_raises(npf.NoRealSolutionError, npf.irr, cashflows, raise_exceptions=True)

def test_irr_maximum_iterations_exception(self):
# Test that if the maximum number of iterations is reached,
# then npf.irr returns IterationsExceededException
# when raise_exceptions is set to True.
cashflows = numpy.array([-40000, 5000, 8000, 12000, 30000])
assert_raises(npf.IterationsExceededException, npf.irr, cashflows,
assert_raises(npf.IterationsExceededError, npf.irr, cashflows,
maxiter=1, raise_exceptions=True)