About this project¶
This page contains information to help you learn a bit more about the cc-pydata
project and how it is configured.
If your goal is to learn more about the Cookiecutter tool used as the basis for the
cc-pydata
project, your best source of information will be the official Cookiecutter documentation.If you just want to know what the
cc-pydata
project is, please see the README documentation for this project.If you want to (a) learn more about the how to generate or use the resulting
cc-pydata
template or (b) better understand the features and functionality embedded in the resultingcc-pydata
template, please see the Tutorial for this project.
Development philosophy¶
As is discussed in the Design decisions section of the cc-pydata
documentation, I have sought to balance Python best practices and standards with my own needs across a wide range of projects. What’s more, I’ve also sought to embed structural characteristics of other great Cookiecutter templates (see Inspiration and sources).
Starting from those initial principals, I hope to have built a Cookiecutter template that is flexible and robust enough for others to find useful for their own Python data science projects.
I have taken the time to carefully test and document the cc-pydata
project, both for my own sake, as well as for the sake of others who might find this project and wish to use it themselves.
I hope that the Tutorial for this project not only enlightens the user, but provides (a) the blueprint needed for those who may not be familiar with all of the tools and methods employed in the template, and (b) enough of a foundation for those who wish to fork and modify the project to better suit their own needs.
License¶
The cc-pydata
Cookiecutter template is offered for use under the MIT open source license.
The content of this license is shown below and can also be found in the LICENSE file contained in the project repository on GitHub:
The MIT License (MIT)
Copyright (c) 2020 Michael Sedelmeyer
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Contributing¶
As is mentioned above in the Development philosophy section of this page, I have sought to balance best practices and features with my own particular needs. Therefore, I am not actively seeking contributions to the cc-pydata
project from others. My primary reason being that, as soon as others begin adding features to this project, the more likely it will be that the template no longer fits my specific needs.
If however, you do see opportunities to improve this project and its resulting template, I am still interested in hearing from you.
If you’d like to suggest changes or to get in touch regarding the cc-pydata
project, please feel free to open an issue and we can discuss.
I am not quite sure how firm I am on my position of “no contributions.” Therefore, if you have great ideas on how to improve this project…there’s a very good chance that I can be convinced!
Project testing and test API¶
It has been my experience that testing a Cookiecutter template presents its own set of unique challenges.
Not only do you need to:
Test that the template renders, but you also need to…
Test that the template renders appropriately according to whatever sorts of build options you provide when invoking the template, and you also need to…
Test that the default functionality baked into template after it’s been rendered functions appropriately as well (i.e. you need to perform “tests within tests”).
Thus far, I have been unable to find any clear documentation online on how to do this correctly.
Therefore, I am providing more detail than might otherwise be warranted on how I have configured my cc-pydata
project tests to deal with (what I can only imagine are) common challenges encountered by anyone who has ever sought to thoroughly test their own custom Cookiecutter template.
In this section
Using pipenv
to install the required development dependencies¶
Prior to running any of the tests outlined below, you will need to first install the Pipenv virtual environment required for development of the cc-pydata
Cookiecutter template.
For details regarding Pipenv and its usage, please see the packaging section section of the Tutorial.
Assuming you already have Pipenv installed on your development machine, you should be able to install the required dependencies and virtual environment by running the following commands from the cc-pydata
repository’s base directory:
pipenv install --dev
pipenv shell
Once you are working from your active pipenv
shell, you will be able to run all other testing commands outlined in the sections below.
Testing tools employed for this project¶
I have sought to develop a thorough set of tests to ensure that the cc-pydata
project and resulting template function as expected. Programmatic tests for this project can be found in the tests
directory, where I seek to test each unique feature of this template.
Tests for this project occur in several ways.
There are the programmatic tests using Python’s standard
unittest
library to author test cases for each template feature I wish to test.My governing principal here, is that I should not add any additional functionality into the template, or the means by which the overarching
cc-pydata
project renders the template, without also developing a test (or set of tests) to test that functionality.There are additionally unit tests built into the rendered template produced by this project. Those in-template tests ensure that the baseline package provided in that rendered template functions correctly (presenting the opportunity for tests within tests).
There are a set of automated tests configured using
tox
to ensure that thecc-pydata
project functions correctly on several different versions of Python (those versions arepython 3.6
,python 3.7
, andpython 3.8
as ofcc-pydata==v0.3.0
at the time of my writing this).This
tox
configuration also runs a documentation test build to ensure that thecc-pydata
Sphinx-based documentation renders successfully, and it runs a linter to ensure that the project code meets PEP 8 standards.Also, dependent on whether the
tox
option is enabled for the renderedcc-pydata
template, the rendered template itself will also contain atox
-automated test-configuration. That in-templatetox
configuration will perform similar tasks for the resulting rendered template (and here is our true layer of tests within tests).
Lastly, continuous integration is implemented using Travis-CI (and soon to also be implemented using GitHub Actions) to run and test automated builds each time committed revisions to the
cc-pydata
project are pushed to the GitHub-hosted remotedevelop
ormaster
branches for the project.
tox
test matrix and automation¶
If you are not familiar with Python’s test automation tool Tox, learning to use it is well worth the investment in time.
If you review the tox.ini
configuration file contained in the cc-pydata
project repository, you will see that tox
automation for this project is configured to:
Run tests on several different versions of Python to ensure compatibility with each of those versions;
Run a test build of the
cc-pydata
project’s Sphinx documentation to ensure docs build successfully;Run a
flake8
linting test to ensure all of the Python syntax in this project meets PEP 8 standards; and…Run a full build of the
cc-pydata
default template and then run that rendered template’s own automatedtox
tests (see details concerning thetests.toxtest
test module outlined in this section)
To run these automated tox
tests, you simply run the tox
command from within your active pipenv
development environment.
Additionally, you can run individual tox
environments (instead of all at once) by explcitly specifying the environment you wish to run, such as:
tox -e docs
Continuous integration test builds with Travis-CI¶
Continuous integration (CI) build tests are set to run via Travis-CI every time a change is pushed to either the master
or develop
branches.
These CI tests ensure that the cc-pydata
tox
-automated test matrix runs successfully on a Linux system.
Please see the cc-pydata
project’s .travis.yml
configuration file for more detail.
Note
There are currently plans to migrate this CI automation from Travis-CI over to GitHub’s native GitHub Actions service.
The primary reason for this planned change is that GitHub Actions offers Windows OS images for CI testing, while Travis-CI does not.
During this planned CI service migration, MacOS builds will also be added to the CI build matrix.
I occasionally use all three of these operating systems for my development work and would appreciate the added assurance that my project and rendered templates run successfully on all three platforms.
Custom tests
module using unittest
and the pytest
test-runner¶
To run unit tests for the cc-pydata
project, enter your development environment by running pipenv shell
and invoke the pytest
test-runner by running the following command from your top-level project directory:
pytest
You should see output similar to this:
============================ test session starts =============================
platform linux -- Python 3.7.5, pytest-5.4.3, py-1.8.2, pluggy-0.13.1
rootdir: /home/Code/cookiecutter-pydata, inifile: setup.cfg, testpaths: tests
plugins: cov-2.10.0
collected 28 items
tests/test_defaults.py .......... [ 35%]
tests/test_options.py ......... [ 67%]
tests/test_testutils.py ......... [100%]
============================= 28 passed in 5.55s =============================
Testing rendered Cookiecutter templates¶
Before getting into the details of the tests
test module, it is probably worth acknowledging that:
There exists a
pytest
plugin for testing Cookiecutter templates. That plug-in is namedpytest-cookies
, and it provides a boilerplate-free experience for building and testing Cookiecutter templates.However, I DO NOT use that plug-in in any way for testing this project.
If you’d like to learn more about the pytest-cookies
plugin for your own use, please see that project’s documentation.
While I use pytest
as the test-runner for this project, I do not use the pytest
framework for writing my tests. I have attempted to keep my tests written entirely using unittest
from the Python standard library. This approach requires a bit more boilerplate in my test code, but it also helps to ensure that I am not locked into pytest
as a testing requirement. Besides, I have also found a simple-enough approach to building and testing my Cookiecutter template using just unittest
test cases. As a result, I haven’t felt a need to use pytest
or the pytest-cookies
plug-in.
If you examine the TestCase
classes in the tests.test_default
and tests.test_options
test modules, you will see that each TestCase
contains a setUp()
method that:
Uses the
contextlib.ExitStack()
context manager to generate atempfile.TemporaryDirectory()
temporary directory in which I can build each of mycc-pydata
rendered templates for testing; and…Contains a call to
self.addCleanup(stack.pop_all().close)
to ensure the temporary directory gets cleaned up after each test, regardless of whether the test setup fails in any way.
Each test case additionally uses cookiecutter.main.cookiecutter()
, the main entry point to the cookiecutter command, which allows you to easily initiate a template build directly from your Python code.
By putting these pieces together, cc-pydata
template builds become predictable and easy to manage for testing.
The structure of the tests
module¶
As you will see in the tests
module API documentation below, cc-pydata
tests are split among several submodules.
tests
: At the highest level are a set of utility functions that make testing easier and reduce boilerplate code in each test case. These utility functions themselves are tested intests.test_testutils
.tests.test_defaults
: This sub-module contains a set of tests focused on the defaultcc-pydata
template build.tests.test_options
: This sub-module focuses on testing versions of thecc-pydata
template produced when using the optional arguments available when rendering the template.tests.toxtest
: Finally, this is a sub-module that only runs when explicitly called using the commandpytest -s tests/test_toxtest.py
.tests.toxtest
contains a costly test to run because, it not only renders thecc-pydata
template, but it also invokes that template’s owntox.ini
to run all of its owntox
environments.Think of it as tests-within-tests-within-tests, all running in their own embedded temporary environments.
This sub-module can take several minutes to run, so considered yourself warned.
See
tests.toxtest
for more detail.
Located below is the full API documentation for each of these modules.
API documentation for the tests
module¶
Module contents
tests¶
This module contains global variables and utility functions required for
testing the cc-pydata
cookiecutter template.
Unit tests for these functions can be found in tests.test_testutils
module
Module Variables:
Define absolute path to |
|
Define path to |
|
Define regex string required to identify all jinja-related brackets |
Module Functions:
|
Generate the cookiecutter template defined in this project repository |
|
Find all instances of input |
|
Load |
|
Change working directory temporarily with context manager |
-
tests.
CCDIR
= '/home/sedelmeyer/Code/cc-pydata'¶ Define absolute path to
cc-pydata
cookiecutter project directory
-
tests.
CCJSON
= '/home/sedelmeyer/Code/cc-pydata/cookiecutter.json'¶ Define path to
cookiecutter.json
default choice variables file
-
tests.
JINJA_REGEX
= '(\\{{|\\}}|\\{%|\\%}|\\{#|\\#})'¶ Define regex string required to identify all jinja-related brackets
Generate the cookiecutter template defined in this project repository
- Parameters
output_dir (str) – directory path in which to render the template
template (str) – name of cookiecutter template, defaults to
CCDIR
extra_context (dict) – dictionary of non-default arguments for cookiecutter template build, defaults to None
- Returns
path to rendered cookiecutter template directory
- Return type
str
-
tests.
find_jinja_brackets
(string, regex='(\\{{|\\}}|\\{%|\\%}|\\{#|\\#})')¶ Find all instances of input
string
that contains jinja brackets- Parameters
string (str) – text within which to search for jinja brackets
regex (str) – regular expression pattern, defaults to
JINJA_REGEX
- Returns
regex search result,
True
if pattern found,None
if not- Return type
bool, None
-
tests.
get_default_template_args
(filepath='/home/sedelmeyer/Code/cc-pydata/cookiecutter.json')¶ Load
cookiecutter.json
to a dictionary object with rendered jinja- Parameters
filepath (str) – filepath to
cookiecutter.json
file, defaults toCCJSON
- Returns
dictionary of cookiecutter default arguments
- Return type
dict
-
tests.
read_template_file
(builtdir, filename)¶ Read the contents of a file contained in the baked cookiecutter template
- Parameters
builtdir (str) – file path to the rendered cookiecutter template
filename (str) – name of the target file within the cookiecutter template
- Returns
text content contained within the target file
- Return type
str
-
tests.
working_directory
(directory)¶ Change working directory temporarily with context manager
- Parameters
directory (str) – path to desired working directory
tests.test_testutils¶
This module contains unit tests for utility functions in the tests
module
-
class
tests.test_testutils.
TestTestsUtilityFunctions
(methodName='runTest')¶ Test utility functions contained in the
tests/__init__.py
fileEnsure the recipe renders a cookiecutter template as expected
-
test_find_jinja_brackets
()¶ Ensure
find_jinja_brackets
finds all jinja bracket types
-
test_find_jinja_brackets_ignores_json
()¶ Ensure
find_jinja_brackets
ignores json and dict brackets
Ensure function returns
input
value when not of typestr
Ensure function removes ‘cookiecutter.’ substring only
-
test_get_default_template_args
()¶ Ensure
get_default_template_args
reads json to a dictionary
-
test_read_template_file
()¶ Ensure
read_template_file
returns file content
-
test_render_json_dict_jinja
()¶ Ensure only double curley-bracket jinja values are rendered
-
test_working_directory
()¶ Ensure
working_directory
context manager works as expected
tests.test_defaults¶
This module contains tests for the default cc-pydata
template build
-
class
tests.test_defaults.
TestBuildDefaultTemplate
(methodName='runTest')¶ Test default
cc-pydata
cookiecutter template build-
setUp
()¶ Render default
cc-pydata
template in temporary directory
-
test_cli_argparse_works
()¶ Ensure template’s CLI default
argparse
function works
-
test_default_docs_build
()¶ Ensure default Sphinx docs build in rendered template
-
test_default_docs_make_html
()¶ Ensure default Sphinx docs build in rendered template
-
test_default_tests_pass
()¶ Ensure all default unit-tests pass in rendered template
-
test_files_exist
()¶ Ensure specified top-level files exist
-
test_jinja_rendered_dirs
()¶ Ensire no jinja brackets exist after rendering directory names
-
test_jinja_rendered_files
()¶ Ensure no jinja brackets exist after rendering template files
-
test_project_exists
()¶ Ensure rendered template directory exists in temporary directory
-
test_setup_py
()¶ Ensure rendered template package setup.py returns version number
-
test_subdirs_exist
()¶ Ensure all expected sub-directories exist
-
-
tests.test_defaults.
command_line_interface_bin_name
= 'nameless-cc-pydata'¶ Define
command_line_interface_bin_name
for default template
-
tests.test_defaults.
json_dict
= {'_extensions': ['jinja2_time.TimeExtension'], 'command_line_interface': ['argparse'], 'command_line_interface_bin_name': 'nameless-cc-pydata', 'distribution_name': 'nameless-cc-pydata', 'email': 'Enter your email', 'full_name': 'Enter your full name', 'github_username': 'Enter your GitHub username', 'license': ['MIT license', 'BSD 2-Clause License', 'BSD 3-Clause License', 'ISC license', 'Apache Software License 2.0', 'Not open source'], 'linter': ['flake8'], 'package_name': 'nameless_cc_pydata', 'project_name': 'nameless cc-pydata', 'project_short_description': 'Enter a short description of this project.', 'release_date': 'today', 'repo_name': 'nameless-cc-pydata', 'scm_versioning': ['yes'], 'test_runner': ['pytest'], 'tox': ['yes', 'no'], 'travis': ['yes', 'no'], 'version': '0.0.0', 'website': 'Enter your website', 'year_from': ['2020', '2019', '2018', '2017'], 'year_to': "{% now 'utc', '%Y' %}"}¶ Define
project_name
for default template
-
tests.test_defaults.
package_name
= 'nameless_cc_pydata'¶ Define
package_name
for default template
-
tests.test_defaults.
template_directories
= ['data', 'data/raw', 'data/interim', 'data/processed', 'docs', 'docs/_static/figures', 'docs/_templates', 'models', 'notebooks', 'references', 'references/third-party', 'reports', 'reports/figures', 'src', 'src/nameless_cc_pydata/data', 'src/nameless_cc_pydata/features', 'src/nameless_cc_pydata/logger', 'src/nameless_cc_pydata/models', 'src/nameless_cc_pydata/visualizations', 'tests', 'tests/data', 'tests/features', 'tests/logger', 'tests/models', 'tests/visualizations']¶ Define list of sub-directories expected in default template
-
tests.test_defaults.
template_files
= ['.editorconfig', '.env', '.gitignore', '.travis.yml', 'CHANGELOG.rst', 'LICENSE', 'logging.json', 'Pipfile', 'README.rst', 'setup.cfg', 'setup.py']¶ Define list of top-level files expected in default template
-
tests.test_defaults.
template_submodules
= ['data', 'features', 'logger', 'models', 'visualizations']¶ Define list of
src
submodule directories expected in default template
tests.test_options¶
This module contains tests for optional cc-pydata
template build options
-
class
tests.test_options.
TestBuildTemplateOption
(methodName='runTest')¶ Test optional cookiecutter template build arguments
-
setUp
()¶ Generate temporary directory in which to render templates
-
test_build_pre_hook_invalid_package_name_fails
()¶ Ensure template build fails with invalid
package_name
-
test_license_not_open_source
()¶ Ensure ‘Not open source’ license option builds correctly
-
test_license_open_source_options
()¶ Ensure open source license options build correctly
-
test_tox_no_ini
()¶ Ensure tox ‘no’ option removes
tox.ini
-
test_tox_no_pipfile
()¶ Ensure tox ‘no’ option removes
tox
install fromPipfile
-
test_tox_no_travis_yes_yaml
()¶ Ensure tox ‘no’ option builds with correct
.travis.yml
-
test_tox_yes_ini
()¶ Ensure tox ‘yes’ option builds with
tox.ini
file
-
test_tox_yes_pipfile
()¶ Ensure tox ‘yes’ option adds tox install to
Pipfile
-
test_tox_yes_travis_yes_yaml
()¶ Ensure tox and travis ‘yes’ option builds correct
.travis.yml
-
test_travis_no_badge
()¶ Ensure travis ‘no’ option does not include badge in docs
-
test_travis_no_yaml
()¶ Ensure travis ‘no’ option removes
.travis.yml
-
test_travis_yes_badge
()¶ Ensure travis ‘yes’ option includes badge in docs
-
test_travis_yes_yaml
()¶ Ensure travis ‘yes’ option builds with
travis.yml
file
-
-
tests.test_options.
json_dict
= {'_extensions': ['jinja2_time.TimeExtension'], 'command_line_interface': ['argparse'], 'command_line_interface_bin_name': 'nameless-cc-pydata', 'distribution_name': 'nameless-cc-pydata', 'email': 'Enter your email', 'full_name': 'Enter your full name', 'github_username': 'Enter your GitHub username', 'license': ['MIT license', 'BSD 2-Clause License', 'BSD 3-Clause License', 'ISC license', 'Apache Software License 2.0', 'Not open source'], 'linter': ['flake8'], 'package_name': 'nameless_cc_pydata', 'project_name': 'nameless cc-pydata', 'project_short_description': 'Enter a short description of this project.', 'release_date': 'today', 'repo_name': 'nameless-cc-pydata', 'scm_versioning': ['yes'], 'test_runner': ['pytest'], 'tox': ['yes', 'no'], 'travis': ['yes', 'no'], 'version': '0.0.0', 'website': 'Enter your website', 'year_from': ['2020', '2019', '2018', '2017'], 'year_to': "{% now 'utc', '%Y' %}"}¶ Load
cookicutter.json
to dictionary
-
tests.test_options.
license_list
= ['MIT license', 'BSD 2-Clause License', 'BSD 3-Clause License', 'ISC license', 'Apache Software License 2.0', 'Not open source']¶ Define
'license'
choice variable argument options
tests.toxtest¶
This module contains tests to ensure the cc-pydata
template’s tox
tests
run without error.
This is accomplished by rendering the cc-pydata
template to a temporary
directory, then running that template’s default tox
configuration to
ensure all tox
environment tests pass without error.
Warning
Tests contained in this module can take considerable time to run and will,
by default, be ignored when the pytest
test-runner command is invoked.
This “ignore” behavior is the result of the naming convention used for this module.
In order to run the tests contained in this module, this module will need to
be explicitly specified while invoking the pytest
command.
For example, use this command to run tests contained in this module:
pytest -s tests/toxtest.py
The -s
option will disable all stdout
capturing by pytest
, ensuring
that the underlying tox
processes are made visible in stdout
while
the template’s tox
script runs.