Skip to content

Commit

Permalink
Merge pull request #1 from lewinfox/develop
Browse files Browse the repository at this point in the history
Add unit tests
  • Loading branch information
lewinfox committed Nov 29, 2021
2 parents 7193bbf + 8ec7ec6 commit aec4d4c
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 32 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/run_unit_tests.yml
@@ -0,0 +1,35 @@
name: Run unit tests

on:
push:
branches:
- main
- develop

pull_request:
branches:
- main

workflow_dispatch:

jobs:
run_tests:
name: Test with Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version:
- "3.x"
- "pypy-3.6"
- "pypy-3.7"

steps:
- uses: actions/checkout@v2

- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Run tests
run: make test
151 changes: 142 additions & 9 deletions .gitignore
@@ -1,16 +1,149 @@
# IDE stuff
.venv/
.vscode/
# From https://github.com/github/gitignore/blob/master/Python.gitignore

# Package stuff
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
*.py[cod]
__pycache__/
*.so
*~
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# pyenv
.python-version

# vscode
.vscode

# Don't want to have the .dat file under VCS
# Data files
*.dat
39 changes: 28 additions & 11 deletions Makefile
@@ -1,33 +1,50 @@
.RECIPEPREFIX = >

# Remove build artefacts
.PHONY: clean
clean:
> @echo Cleaning
> @rm -rf build

# Rebuild the pickled `.dat` file from `rmsfact.txt`
.PHONY: build_binary_data
build_binary_data:
> rm -f rmsfact/data/rmsfact.dat
> python rmsfact/data/build_rmsfact.py
> @echo Building binary data
> @rm -f rmsfact/data/rmsfact.dat
> @python rmsfact/data/build_rmsfact.py

# Build the package in wheel and source form
.PHONY: build
build: build_binary_data
> rm -rf dist
> python -m build
build: clean build_binary_data
> @echo Building package
> @python -m build

# Build the package and install locally for development
.PHONY: install_dev
install_dev: build
> python -m pip install -e .
> @echo Installing locally
> @python -m pip install -e .

# Install
.PHONY: install
install: build
> python -m pip install .
> @echo Installing
> @python -m pip install .

# Build the package and upload to TestPyPI
.PHONY: upload_test
upload_test: build
> python -m twine upload --repository testpypi dist/*
upload_test: test build
> @echo Uploading to testpypi
> @python -m twine upload --repository testpypi dist/*

# Build the package and upload to PyPI
.PHONY: upload
upload: build
> python -m twine upload dist/*
upload: test build
> @echo Uploading to PyPi
> @python -m twine upload dist/*

# Run unit tests
.PHONY: test
test: clean build_binary_data
> @echo Running tests
> @python test
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -73,3 +73,5 @@ make build
## Other makefile targets

* `make install_dev`: Install with `pip -e`
* `make test`: Run unit tests
* `make clean`: Remove build artifacts
4 changes: 2 additions & 2 deletions rmsfact/data/build_rmsfact.py
Expand Up @@ -4,14 +4,14 @@

def _build_rmsfact():
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
TEXT_FILE = os.path.normpath(f"{THIS_DIR}/rmsfact.txt")
TEXT_FILE = os.path.normpath("{THIS_DIR}/rmsfact.txt".format(THIS_DIR=THIS_DIR))

with open(TEXT_FILE, "r") as f:
lines = f.readlines()
facts = [line.strip("\n")
for line in lines if not line.startswith("#")]

OUTFILE = os.path.normpath(f"{THIS_DIR}/rmsfact.dat")
OUTFILE = os.path.normpath("{THIS_DIR}/rmsfact.dat".format(THIS_DIR=THIS_DIR))

with open(OUTFILE, "wb") as f:
pickle.dump(facts, f)
Expand Down
29 changes: 20 additions & 9 deletions rmsfact/new_rmsfact.py
@@ -1,6 +1,8 @@
import random
import os
import pickle
import random
from typing import Callable


# Rather than having the `rmsfact()` function parse the source file each time, using a closure like
# this allows us to offload the parsing and data validation (not that there is any at the moment)
Expand All @@ -10,35 +12,44 @@
# object rather than parsing the text file every time the package is loaded?


def _new_rmsfact():
def _new_rmsfact() -> Callable:
"""
Generate an `rmsfact()` function
This function runs when the package is imported. It parses the source file containing the facts,
removes comments and creates a list of facts.
This function runs when the package is imported. It loads the "facts" from their binary store
and returns a function that will retrieve a random fact.
Returns
-------
function : A function that, when called, returns a random fact.
function
A function that, when called, returns a random fact.
Examples
--------
>>> f = _new_rmsfact()
>>> fact = f()
"""
# TODO: Is there a more Pythonic way of referring to the file?
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.normpath(f"{ROOT_DIR}/data")
FACT_FILE = os.path.normpath(f"{DATA_DIR}/rmsfact.dat")
DATA_DIR = os.path.normpath("{ROOT_DIR}/data".format(ROOT_DIR=ROOT_DIR))
FACT_FILE = os.path.normpath("{DATA_DIR}/rmsfact.dat".format(DATA_DIR=DATA_DIR))
# TODO: Error handling needed here?
with open(FACT_FILE, "rb") as f:
facts = pickle.load(f)

n_facts = len(facts)

def rmsfact():
def rmsfact() -> str:
"""
Return a random fact about Richard M. Stallman
Returns
-------
string : A randomly-selected fact.
str
A randomly-selected fact.
"""
idx = random.randint(0, n_facts)
return facts[idx]
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = rmsfact
version = 0.4.1
version = 0.4.2
author = Lewin Appleton-Fox
author_email = lewin.a.f@gmail.com
description = Display a randomly selected quote about Richard M. Stallman.
Expand Down
Empty file added test/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions test/__main__.py
@@ -0,0 +1,17 @@
import sys
import unittest

# Make sure the package is importable
sys.path.append("../rmsfact")

loader = unittest.TestLoader()
test_suite = loader.discover("test")
test_runner = unittest.TextTestRunner()
result = test_runner.run(test_suite)

# Ensure that if any tests fail we return a failure code from the process. This is useful for things
# like `make` and CI/CD pipelines.
if result.wasSuccessful():
exit(0)

exit(1)
19 changes: 19 additions & 0 deletions test/test_new_rmsfact.py
@@ -0,0 +1,19 @@
import unittest

import rmsfact


class TestNewRMSFact(unittest.TestCase):

# The `_new_rmsfact()` function should return a function
def test_new_rms_fact_returns_function(self):
self.assert_(hasattr(rmsfact._new_rmsfact(), "__call__"))

# `rmsfact.rmsfact()` should be a function
def test_rmsfact_is_function(self):
self.assert_(hasattr(rmsfact.rmsfact, "__call__"))

# `rmsfact.rmsfact()` should return a string
def test_returns_string(self):
fact = rmsfact.rmsfact()
self.assertIsInstance(fact, str)

0 comments on commit aec4d4c

Please sign in to comment.