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-pydataproject, your best source of information will be the official Cookiecutter documentation.If you just want to know what the
cc-pydataproject 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-pydatatemplate or (b) better understand the features and functionality embedded in the resultingcc-pydatatemplate, 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
unittestlibrary 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-pydataproject 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
toxto ensure that thecc-pydataproject functions correctly on several different versions of Python (those versions arepython 3.6,python 3.7, andpython 3.8as ofcc-pydata==v0.3.0at the time of my writing this).This
toxconfiguration also runs a documentation test build to ensure that thecc-pydataSphinx-based documentation renders successfully, and it runs a linter to ensure that the project code meets PEP 8 standards.Also, dependent on whether the
toxoption is enabled for the renderedcc-pydatatemplate, the rendered template itself will also contain atox-automated test-configuration. That in-templatetoxconfiguration 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-pydataproject are pushed to the GitHub-hosted remotedevelopormasterbranches 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-pydataproject’s Sphinx documentation to ensure docs build successfully;Run a
flake8linting test to ensure all of the Python syntax in this project meets PEP 8 standards; and…Run a full build of the
cc-pydatadefault template and then run that rendered template’s own automatedtoxtests (see details concerning thetests.toxtesttest 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
pytestplugin 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-pydatarendered 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-pydatatemplate build.tests.test_options: This sub-module focuses on testing versions of thecc-pydatatemplate 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.toxtestcontains a costly test to run because, it not only renders thecc-pydatatemplate, but it also invokes that template’s owntox.inito run all of its owntoxenvironments.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.toxtestfor 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-pydatacookiecutter project directory
-
tests.CCJSON= '/home/sedelmeyer/Code/cc-pydata/cookiecutter.json'¶ Define path to
cookiecutter.jsondefault 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
CCDIRextra_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
stringthat 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,
Trueif pattern found,Noneif not- Return type
bool, None
-
tests.get_default_template_args(filepath='/home/sedelmeyer/Code/cc-pydata/cookiecutter.json')¶ Load
cookiecutter.jsonto a dictionary object with rendered jinja- Parameters
filepath (str) – filepath to
cookiecutter.jsonfile, 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__.pyfileEnsure the recipe renders a cookiecutter template as expected
-
test_find_jinja_brackets()¶ Ensure
find_jinja_bracketsfinds all jinja bracket types
-
test_find_jinja_brackets_ignores_json()¶ Ensure
find_jinja_bracketsignores json and dict brackets
Ensure function returns
inputvalue when not of typestr
Ensure function removes ‘cookiecutter.’ substring only
-
test_get_default_template_args()¶ Ensure
get_default_template_argsreads json to a dictionary
-
test_read_template_file()¶ Ensure
read_template_filereturns file content
-
test_render_json_dict_jinja()¶ Ensure only double curley-bracket jinja values are rendered
-
test_working_directory()¶ Ensure
working_directorycontext 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-pydatacookiecutter template build-
setUp()¶ Render default
cc-pydatatemplate in temporary directory
-
test_cli_argparse_works()¶ Ensure template’s CLI default
argparsefunction 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_namefor 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_namefor default template
-
tests.test_defaults.package_name= 'nameless_cc_pydata'¶ Define
package_namefor 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
srcsubmodule 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
toxinstall 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.inifile
-
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.ymlfile
-
-
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.jsonto 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.