diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a819abb..82df604 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 \ No newline at end of file + # 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 diff --git a/numpy_financial/__init__.py b/numpy_financial/__init__.py index 00ad3b1..4e2b39e 100644 --- a/numpy_financial/__init__.py +++ b/numpy_financial/__init__.py @@ -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" diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 432e32e..8bd280e 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -1,4 +1,4 @@ -"""Some simple financial calculations +"""Some simple financial calculations. patterned after spreadsheet computations. @@ -10,7 +10,6 @@ Functions support the :class:`decimal.Decimal` type unless otherwise stated. """ -from __future__ import absolute_import, division, print_function from decimal import Decimal @@ -18,7 +17,7 @@ __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, @@ -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): @@ -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` @@ -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: @@ -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) @@ -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. @@ -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 @@ -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 ---------- @@ -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: @@ -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 @@ -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 ---------- @@ -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` @@ -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 - @@ -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 @@ -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 ---------- @@ -675,7 +676,7 @@ 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 @@ -683,21 +684,20 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100, *, ra 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, @@ -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 @@ -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 ---------- @@ -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 ---------- @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 6e17759..059c137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,5 +52,7 @@ numpydoc = "^1.5" pydata-sphinx-theme = "^0.14.3" + [tool.poetry.group.lint.dependencies] -flake8 = "*" +ruff = "^0.1.6" + diff --git a/tests/test_financial.py b/tests/test_financial.py index af7aded..d6986de 100644 --- a/tests/test_financial.py +++ b/tests/test_financial.py @@ -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) @@ -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) @@ -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: @@ -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)