This is a follow up on my previous blog, How to Submit a Package to PyPI. As the blog gained a lot of requests and feedback on Reddit, I reviewed all the comments and made improvements to our previous package.
Refactoring
While new features are added to a software or library, the code base becomes bigger and maintaining it becomes harder. This time, developers will start to notice code smells which might cause issues in the long run. To solve this problem, we need to refactor our code. Code refactoring is a process of restructuring a code base without changing the behavior of a software, but fixing possible bugs, removing duplicates and improving readability, among others. As a result, the code base will be easier to maintain.
There are several principles that we can use when refactoring. One of which is the Single Responsibility Principle. This means that a module, class or function should be doing one thing only over the entire functionality of the software.
We will begin by converting our module into a package. First is to create a directory amortization
and inside this directory, write a file __init__.py
and move the function calculate_amortization_amount()
into it.
#!/usr/bin/env python
def calculate_amortization_amount(principal, interest_rate, period):
"""
Calculates Amortization Amount per period
:param principal: Principal amount
:param interest_rate: Interest rate per period
:param period: Total number of periods
:return: Amortization amount per period
"""
x = (1 + interest_rate) ** period
return principal * (interest_rate * x) / (x - 1)
The second step is to write a file amortization/schedule.py
and move the function amortization_schedule()
.
#!/usr/bin/env python
from amortization import calculate_amortization_amount
def amortization_schedule(principal, interest_rate, period):
"""
Generates amortization schedule
:param principal: Principal amount
:param interest_rate: Interest rate per period
:param period: Total number of periods
:return: Rows containing period, interest, principal, balance, etc
"""
amortization_amount = calculate_amortization_amount(principal, interest_rate, period)
number = 1
balance = principal
while number <= period:
interest = balance * interest_rate
principal = amortization_amount - interest
balance -= principal
yield number, amortization_amount, interest, principal, balance if balance > 0 else 0
number += 1
Next step is to write another file, amortization/amortize.py. Here we will move the main()
function.
#!/usr/bin/env python
from amortization import calculate_amortization_amount
from amortization.schedule import amortization_schedule
def main(): # pragma: no cover
import argparse
from tabulate import tabulate
parser = argparse.ArgumentParser(
description='Python library for calculating amortizations and generating amortization schedules')
# required parameters
required = parser.add_argument_group('required arguments')
required.add_argument('-P', '--principal', dest='principal', type=float, required=True, help='Principal amount')
required.add_argument('-n', '--period', dest='period', type=int, required=True, help='Total number of periods')
required.add_argument('-r', '--interest-rate', dest='interest_rate', type=float, required=True,
help='Interest rate per period')
# optional parameters
parser.add_argument('-s', '--schedule', dest='schedule', default=False, action='store_true',
help='Generate amortization schedule')
arguments = parser.parse_args()
if arguments.schedule:
table = (x for x in amortization_schedule(arguments.principal, arguments.interest_rate, arguments.period))
print(
tabulate(
table,
headers=["Number", "Amount", "Interest", "Principal", "Balance"],
floatfmt=",.2f",
numalign="right"
)
)
else:
amount = calculate_amortization_amount(arguments.principal, arguments.interest_rate, arguments.period)
print("Amortization amount: {:,.2f}".format(amount))
Imports to our tests/test_amortization.py
should be updated too.
#!/usr/bin/env python
from amortization import calculate_amortization_amount
from amortization.schedule import amortization_schedule
Test again!
We need to make sure that our tests will still pass. Run pytest
.
tests\test_amortization.py .. [100%]
========================== 2 passed in 0.11 seconds ===========================
Moving definitions to setup.cfg
We can move all the package definitions to setup.cfg like this.
[metadata]
name=amortization
version=0.1.1
download_url=https://github.com/roniemartinez/amortization/tarball/0.1.1
description=Python library for calculating amortizations and generating amortization schedules
long_description=file:README.md
long_description_content_type=text/markdown
author=Ronie Martinez
author_email=ronmarti18@gmail.com
url=https://github.com/roniemartinez/amortization
license=MIT
license_files=LICENSE
classifiers=
Development Status :: 4 - Beta
License :: OSI Approved :: MIT License
Topic :: Office/Business :: Financial
Topic :: Scientific/Engineering :: Mathematics
Topic :: Software Development :: Libraries :: Python Modules
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: Implementation :: CPython
[options]
packages=
amortization
install_requires=
tabulate ~= 0.8.3
[options.entry_points]
console_scripts=
amortize=amortization.amortize:main
[bdist_wheel]
universal=1
This will leave setup.py with only a few lines of code.
#!/usr/bin/env python
from setuptools import setup
setup()
Why not use scripts in the definition?
As of this writing, scripts
does not create an executable on Windows. We will retain using entry_points
.
Bonus: Supporting other platforms
Adding PyPy
To support PyPy, we only need to add pypy
to our .travis.yml
file.
python:
- 2.7
- 3.5
- 3.6
- pypy
Supporting Windows
We will use AppVeyor to test our library on Windows. AppVeyor is another CI/CD platform similar to Travis CI that supports Windows environments. Register to AppVeyor using your Github account and create a new project. After adding a new project, go to https://ci.appveyor.com/project/<owner>/<repo>/settings
, click Environment
and click Add variable
to add the CODECOV_TOKEN
that we obtained from CodeCov (see previous blog).
The last step is to include an appveyor.yml
file to our project. The contents are almost similar to our Travis CI settings except that we are testing on 32-bit and 64-bit platforms.
environment:
matrix:
- PYTHON: "C:\\Python27"
- PYTHON: "C:\\Python27-x64"
- PYTHON: "C:\\Python35"
- PYTHON: "C:\\Python35-x64"
- PYTHON: "C:\\Python36"
- PYTHON: "C:\\Python36-x64"
- PYTHON: "C:\\Python37"
- PYTHON: "C:\\Python37-x64"
branches:
except:
- /^[0-9]+\.[0-9]+\.[0-9]+/
install:
- "%PYTHON%\\Scripts\\pip.exe install pipenv"
- "%PYTHON%\\Scripts\\pipenv.exe install --dev --skip-lock"
build: off
cache:
- '%LOCALAPPDATA%\pip\Cache'
test_script:
- "%PYTHON%\\Scripts\\pipenv.exe run pytest --cov=amortization --cov-report=xml -v"
on_success:
- "%PYTHON%\\Scripts\\pipenv.exe run codecov -f coverage.xml"
Key takeaways
- Code refactoring is one important process that software developers have to do in order to maintain a working software.
- We can move Python package definitions from
setup.py
tosetup.cfg
with minimal effort.
Thanks to all the Redditors who gave their feedback!