by {{ cookiecutter.author }}
+ + + +Step 5: Pack cookiecutter into ZIP +---------------------------------- +There are many ways to run Cookiecutter templates, and they are described in details in `Usage chapterby Test web
+ + diff --git a/docs/usage.rst b/docs/usage.rst index 9b7b95fe5..872cbe2b7 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,7 +7,7 @@ Grab a Cookiecutter template First, clone a Cookiecutter project template:: - $ git clone git@github.com:audreyr/cookiecutter-pypackage.git + $ git clone https://github.com/audreyfeldroy/cookiecutter-pypackage.git Make your changes ----------------- @@ -40,14 +40,14 @@ Works directly with git and hg (mercurial) repos too To create a project from the cookiecutter-pypackage.git repo template:: - $ cookiecutter gh:audreyr/cookiecutter-pypackage + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage Cookiecutter knows abbreviations for Github (``gh``), Bitbucket (``bb``), and GitLab (``gl``) projects, but you can also give it the full URL to any repository:: - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git - $ cookiecutter git+ssh://git@github.com/audreyr/cookiecutter-pypackage.git + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git + $ cookiecutter git+ssh://git@github.com/audreyfeldroy/cookiecutter-pypackage.git $ cookiecutter hg+ssh://hg@bitbucket.org/audreyr/cookiecutter-pypackage You will be prompted to enter a bunch of project config values. (These are @@ -58,7 +58,7 @@ that you entered. It will be placed in your current directory. And if you want to specify a branch you can do that with:: - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git --checkout develop + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git --checkout develop Works with private repos ------------------------ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..25189967e --- /dev/null +++ b/noxfile.py @@ -0,0 +1,82 @@ +"""Nox tool configuration file. + +Nox is Tox tool replacement. +""" +import shutil +from pathlib import Path + +import nox + +nox.options.keywords = "not docs" + + +def base_install(session): + """Create basic environment setup for tests and linting.""" + session.install("-r", "test_requirements.txt") + session.install("-e", ".") + return session + + +@nox.session(python="3.10") +def lint(session): + """Run linting check locally.""" + session.install("pre-commit") + session.run("pre-commit", "run", "-a") + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def tests(session): + """Run test suite with pytest.""" + session = base_install(session) + session.run( + "pytest", + "--cov-report=html", + "--cov-report=xml", + "--cov-branch", + "--cov-fail-under=100", + ) + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def safety_tests(session): + """Run safety tests.""" + session = base_install(session) + session.run("safety", "check", "--full-report") + + +@nox.session(python="3.10") +def documentation_tests(session): + """Run documentation tests.""" + return docs(session, batch_run=True) + + +@nox.session(python="3.10") +def docs(session, batch_run: bool = False): + """Build the documentation or serve documentation interactively.""" + shutil.rmtree(Path("docs").joinpath("_build"), ignore_errors=True) + session.install("-r", "docs/requirements.txt") + session.install("-e", ".") + session.cd("docs") + sphinx_args = ["-b", "html", "-W", ".", "_build/html"] + + if not session.interactive or batch_run: + sphinx_cmd = "sphinx-build" + else: + sphinx_cmd = "sphinx-autobuild" + sphinx_args.extend( + [ + "--open-browser", + "--port", + "9812", + "--watch", + "../*.md", + "--watch", + "../*.rst", + "--watch", + "../*.py", + "--watch", + "../cookiecutter", + ] + ) + + session.run(sphinx_cmd, *sphinx_args) diff --git a/setup.cfg b/setup.cfg index 938b39de6..3f29fe9a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,14 +8,11 @@ statistics = 1 # black official is 88 max-line-length = 88 -[bdist_wheel] -universal = 1 - [tool:pytest] testpaths = tests addopts = -vvv --cov-report term-missing --cov=cookiecutter [doc8] -# TODO: Remove current max-line-lengh ignore in follow-up and adopt black limit. +# TODO: Remove current max-line-length ignore in follow-up and adopt black limit. # max-line-length = 88 ignore = D001 diff --git a/setup.py b/setup.py index 654010fa0..d61fbd6ba 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.0.0" +version = "2.1.2.dev0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() @@ -10,7 +9,7 @@ requirements = [ 'binaryornot>=0.4.4', 'Jinja2>=2.7,<4.0.0', - 'click>=7.0,<8.0.0', + 'click>=7.0,<9.0.0', 'pyyaml>=5.3.1', 'jinja2-time>=0.2.0', 'python-slugify>=4.0.0', @@ -34,7 +33,7 @@ package_dir={'cookiecutter': 'cookiecutter'}, entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, include_package_data=True, - python_requires='>=3.6', + python_requires='>=3.7', install_requires=requirements, license='BSD', zip_safe=False, @@ -46,10 +45,10 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", diff --git a/test_requirements.txt b/test_requirements.txt index 4607cefe1..febcb2ae3 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,3 +2,5 @@ pytest pytest-cov pytest-mock freezegun +safety +pre-commit diff --git a/tests/conftest.py b/tests/conftest.py index e21071304..0990c07d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,10 +12,6 @@ cookiecutters_dir: '{cookiecutters_dir}' replay_dir: '{replay_dir}' """ -# In YAML, double quotes mean to use escape sequences. -# Single quotes mean we will have unescaped backslahes. -# http://blogs.perl.org/users/tinita/2018/03/ -# strings-in-yaml---to-quote-or-not-to-quote.html @pytest.fixture(autouse=True) @@ -38,7 +34,7 @@ def backup_dir(original_dir, backup_dir): if not os.path.isdir(original_dir): return False - # Remove existing backups before backing up. If they exist, they're stale. + # Remove existing stale backups before backing up. if os.path.isdir(backup_dir): utils.rmtree(backup_dir) @@ -48,12 +44,9 @@ def backup_dir(original_dir, backup_dir): def restore_backup_dir(original_dir, backup_dir, original_dir_found): """Restore default contents.""" - # Carefully delete the created original_dir only in certain - # conditions. original_dir_is_dir = os.path.isdir(original_dir) if original_dir_found: - # Delete the created original_dir as long as a backup - # exists + # Delete original_dir if a backup exists if original_dir_is_dir and os.path.isdir(backup_dir): utils.rmtree(original_dir) else: diff --git a/tests/hooks-abort-render/hooks/post_gen_project.py b/tests/hooks-abort-render/hooks/post_gen_project.py index 706cc440d..d95ca59fa 100644 --- a/tests/hooks-abort-render/hooks/post_gen_project.py +++ b/tests/hooks-abort-render/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple post-gen hook for testing the handling of different exit codes.""" diff --git a/tests/hooks-abort-render/hooks/pre_gen_project.py b/tests/hooks-abort-render/hooks/pre_gen_project.py index a132af807..3bd59868c 100644 --- a/tests/hooks-abort-render/hooks/pre_gen_project.py +++ b/tests/hooks-abort-render/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple pre-gen hook for testing the handling of different exit codes.""" diff --git a/tests/replay/test_dump.py b/tests/replay/test_dump.py index ec8010b3e..57ad8ee74 100644 --- a/tests/replay/test_dump.py +++ b/tests/replay/test_dump.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) @@ -57,7 +57,9 @@ def mock_ensure_failure(mocker): Used to mock internal function and limit test scope. Always return expected value: False """ - return mocker.patch('cookiecutter.replay.make_sure_path_exists', return_value=False) + return mocker.patch( + 'cookiecutter.replay.make_sure_path_exists', side_effect=OSError + ) @pytest.fixture @@ -72,7 +74,7 @@ def mock_ensure_success(mocker): def test_ioerror_if_replay_dir_creation_fails(mock_ensure_failure, replay_test_dir): """Test that replay.dump raises when the replay_dir cannot be created.""" - with pytest.raises(IOError): + with pytest.raises(OSError): replay.dump(replay_test_dir, 'foo', {'cookiecutter': {'hello': 'world'}}) mock_ensure_failure.assert_called_once_with(replay_test_dir) diff --git a/tests/replay/test_load.py b/tests/replay/test_load.py index a64a285e1..c8bc453e0 100644 --- a/tests/replay/test_load.py +++ b/tests/replay/test_load.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py index 107e64aa3..3810668f6 100644 --- a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py +++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py @@ -1,5 +1,6 @@ """Tests around detection whether cookiecutter templates are cached locally.""" import os +from pathlib import Path import pytest @@ -20,7 +21,7 @@ def cloned_cookiecutter_path(user_config_data, template): cloned_template_path = os.path.join(cookiecutters_dir, template) os.mkdir(cloned_template_path) - open(os.path.join(cloned_template_path, 'cookiecutter.json'), 'w') + Path(cloned_template_path, "cookiecutter.json").touch() # creates file return cloned_template_path diff --git a/tests/repository/test_determine_repo_dir_finds_subdirectories.py b/tests/repository/test_determine_repo_dir_finds_subdirectories.py index f40e6063d..bcea1b387 100644 --- a/tests/repository/test_determine_repo_dir_finds_subdirectories.py +++ b/tests/repository/test_determine_repo_dir_finds_subdirectories.py @@ -1,5 +1,6 @@ """Tests around locally cached cookiecutter template repositories.""" import os +from pathlib import Path import pytest @@ -24,7 +25,7 @@ def cloned_cookiecutter_path(user_config_data, template): subdir_template_path = os.path.join(cloned_template_path, 'my-dir') if not os.path.exists(subdir_template_path): os.mkdir(subdir_template_path) - open(os.path.join(subdir_template_path, 'cookiecutter.json'), 'w') + Path(subdir_template_path, 'cookiecutter.json').touch() # creates file return subdir_template_path diff --git a/tests/repository/test_is_repo_url.py b/tests/repository/test_is_repo_url.py index 64238e02e..5591be0fc 100644 --- a/tests/repository/test_is_repo_url.py +++ b/tests/repository/test_is_repo_url.py @@ -25,8 +25,8 @@ def test_is_zip_file(zipfile): @pytest.fixture( params=[ 'gitolite@server:team/repo', - 'git@github.com:audreyr/cookiecutter.git', - 'https://github.com/audreyr/cookiecutter.git', + 'git@github.com:audreyfeldroy/cookiecutter.git', + 'https://github.com/cookiecutter/cookiecutter.git', 'git+https://private.com/gitrepo', 'hg+https://private.com/mercurialrepo', 'https://bitbucket.org/pokoli/cookiecutter.hg', @@ -65,7 +65,7 @@ def test_is_repo_url_for_local_urls(local_repo_url): def test_expand_abbreviations(): """Validate `repository.expand_abbreviations` correctly translate url.""" - template = 'gh:audreyr/cookiecutter-pypackage' + template = 'gh:audreyfeldroy/cookiecutter-pypackage' # This is not a valid repo url just yet! # First `repository.expand_abbreviations` needs to translate it diff --git a/tests/test-extensions/hello_extension/hello_extension.py b/tests/test-extensions/hello_extension/hello_extension.py index f54b6efdd..07f3753b9 100644 --- a/tests/test-extensions/hello_extension/hello_extension.py +++ b/tests/test-extensions/hello_extension/hello_extension.py @@ -6,15 +6,15 @@ class HelloExtension(Extension): """Simple jinja2 extension for cookiecutter test purposes.""" - tags = set(['hello']) + tags = {'hello'} def __init__(self, environment): """Hello Extension Constructor.""" - super(HelloExtension, self).__init__(environment) + super().__init__(environment) def _hello(self, name): """Do actual tag replace when invoked by parser.""" - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def parse(self, parser): """Work when something match `tags` variable.""" diff --git a/tests/test-extensions/local_extension/cookiecutter.json b/tests/test-extensions/local_extension/cookiecutter.json new file mode 100644 index 000000000..8141fd508 --- /dev/null +++ b/tests/test-extensions/local_extension/cookiecutter.json @@ -0,0 +1,10 @@ +{ + "project_slug": "Foobar", + "test_value_class_based": "{{cookiecutter.project_slug | foobar}}", + "test_value_function_based": "{{cookiecutter.project_slug | simplefilterextension}}", + "_extensions": [ + "local_extensions.simplefilterextension", + "local_extensions.FoobarExtension" + ] +} + diff --git a/tests/test-extensions/local_extension/local_extensions/__init__.py b/tests/test-extensions/local_extension/local_extensions/__init__.py new file mode 100644 index 000000000..94e854abd --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/__init__.py @@ -0,0 +1 @@ +from .main import FoobarExtension, simplefilterextension # noqa diff --git a/tests/test-extensions/local_extension/local_extensions/main.py b/tests/test-extensions/local_extension/local_extensions/main.py new file mode 100644 index 000000000..b18a25c91 --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/main.py @@ -0,0 +1,19 @@ +"""Provides custom extension, exposing a ``foobar`` filter.""" + +from jinja2.ext import Extension +from cookiecutter.utils import simple_filter + + +class FoobarExtension(Extension): + """Simple jinja2 extension for cookiecutter test purposes.""" + + def __init__(self, environment): + """Foobar Extension Constructor.""" + super().__init__(environment) + environment.filters['foobar'] = lambda v: v * 2 + + +@simple_filter +def simplefilterextension(v): + """Provide a simple function-based filter extension.""" + return v.upper() diff --git a/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst new file mode 100644 index 000000000..8bb7c6136 --- /dev/null +++ b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst @@ -0,0 +1,8 @@ +History +------- + +0.1.0 +----- + +First release of {{cookiecutter.test_value_class_based}} on PyPI. +{{cookiecutter.test_value_function_based}} diff --git a/tests/test-generate-context/nested_dict.json b/tests/test-generate-context/nested_dict.json new file mode 100644 index 000000000..c13ea10f3 --- /dev/null +++ b/tests/test-generate-context/nested_dict.json @@ -0,0 +1,10 @@ +{ + "full_name": "Raphael Pierzina", + "github_username": "hackebrot", + "project": { + "name": "Kivy Project", + "description": "Kivy Project", + "repo_name": "{{cookiecutter.project_name|lower}}", + "orientation": ["all", "landscape", "portrait"] + } +} \ No newline at end of file diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml new file mode 100644 index 000000000..a31cf752c --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml @@ -0,0 +1,2 @@ +--- +- name: {{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md new file mode 100644 index 000000000..0e74081d8 --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md @@ -0,0 +1,3 @@ +# Fake Project + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-pyhooks/hooks/post_gen_project.py b/tests/test-pyhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyhooks/hooks/post_gen_project.py +++ b/tests/test-pyhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyhooks/hooks/pre_gen_project.py b/tests/test-pyhooks/hooks/pre_gen_project.py index 4d84bd3ec..6f1887bd4 100644 --- a/tests/test-pyhooks/hooks/pre_gen_project.py +++ b/tests/test-pyhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/post_gen_project.py b/tests/test-pyshellhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyshellhooks/hooks/post_gen_project.py +++ b/tests/test-pyshellhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/pre_gen_project.py b/tests/test-pyshellhooks/hooks/pre_gen_project.py index db8bfc6a7..daeb59acb 100644 --- a/tests/test-pyshellhooks/hooks/pre_gen_project.py +++ b/tests/test-pyshellhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" diff --git a/tests/test-templates/extends/cookiecutter.json b/tests/test-templates/extends/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/extends/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/extends/templates/base-requirements.jinja b/tests/test-templates/extends/templates/base-requirements.jinja new file mode 100644 index 000000000..964fa3379 --- /dev/null +++ b/tests/test-templates/extends/templates/base-requirements.jinja @@ -0,0 +1,6 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +{% include 'pytest-requirements.jinja' %}{% endif %} +{% block dependencies %}{% endblock %} \ No newline at end of file diff --git a/tests/test-templates/extends/templates/click-requirements.jinja b/tests/test-templates/extends/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/extends/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/extends/templates/pytest-requirements.jinja b/tests/test-templates/extends/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/extends/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..910e3721e --- /dev/null +++ b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1 @@ +{% extends "base-requirements.jinja" %} \ No newline at end of file diff --git a/tests/test-templates/include/cookiecutter.json b/tests/test-templates/include/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/include/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/include/templates/click-requirements.jinja b/tests/test-templates/include/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/include/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/include/templates/pytest-requirements.jinja b/tests/test-templates/include/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/include/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..62645fd16 --- /dev/null +++ b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +{% include 'pytest-requirements.jinja' %}{% endif %} \ No newline at end of file diff --git a/tests/test-templates/no-templates/cookiecutter.json b/tests/test-templates/no-templates/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/no-templates/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..b8b92ac2b --- /dev/null +++ b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +Click{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +pytest{% endif %} \ No newline at end of file diff --git a/tests/test-templates/super/cookiecutter.json b/tests/test-templates/super/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/super/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/super/templates/base-requirements.jinja b/tests/test-templates/super/templates/base-requirements.jinja new file mode 100644 index 000000000..23c1ca511 --- /dev/null +++ b/tests/test-templates/super/templates/base-requirements.jinja @@ -0,0 +1,7 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{%- block dev_dependencies %} +{% if cookiecutter.use_pytest == 'y' -%}{% include 'pytest-requirements.jinja' %}{% endif %} +{%- endblock %} +{% block dependencies %}{% endblock %} \ No newline at end of file diff --git a/tests/test-templates/super/templates/click-requirements.jinja b/tests/test-templates/super/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/super/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/super/templates/pytest-requirements.jinja b/tests/test-templates/super/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/super/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..602b5772f --- /dev/null +++ b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,2 @@ +{% extends "base-requirements.jinja" %} +{% block dev_dependencies %}{{ super() }}{% endblock %} \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 19740ef26..0364e2cef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,13 +3,15 @@ import json import os import re - +from pathlib import Path import pytest from click.testing import CliRunner from cookiecutter import utils from cookiecutter.__main__ import main +from cookiecutter.environment import StrictEnvironment +from cookiecutter.exceptions import UnknownExtension from cookiecutter.main import cookiecutter @@ -70,8 +72,8 @@ def test_cli(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -80,8 +82,8 @@ def test_cli_verbose(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -107,6 +109,7 @@ def test_cli_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -133,6 +136,7 @@ def test_cli_replay_file(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -168,6 +172,7 @@ def test_cli_exit_on_noinput_and_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -203,6 +208,7 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay( password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -259,6 +265,7 @@ def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -303,6 +310,7 @@ def test_user_config(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -333,6 +341,7 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -358,6 +367,7 @@ def test_default_user_config(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -388,6 +398,7 @@ def test_echo_undefined_variable_error(output_dir, cli_runner): 'github_username': 'hackebrot', 'project_slug': 'testproject', '_template': template_path, + '_repo_dir': template_path, '_output_dir': output_dir, } } @@ -412,6 +423,34 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): assert 'Unable to load extension: ' in result.output +def test_local_extension(tmpdir, cli_runner): + """Test to verify correct work of extension, included in template.""" + output_dir = str(tmpdir.mkdir('output')) + template_path = 'tests/test-extensions/local_extension/' + + result = cli_runner( + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, + ) + assert result.exit_code == 0 + content = Path(output_dir, 'Foobar', 'HISTORY.rst').read_text() + assert 'FoobarFoobar' in content + assert 'FOOBAR' in content + + +def test_local_extension_not_available(tmpdir, cli_runner): + """Test handling of included but unavailable local extension.""" + context = {'cookiecutter': {'_extensions': ['foobar']}} + + with pytest.raises(UnknownExtension) as err: + StrictEnvironment(context=context, keep_trailing_newline=True) + + assert 'Unable to load extension: ' in str(err.value) + + @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_extra_context(cli_runner): """Cli invocation replace content if called with replacement pairs.""" @@ -423,8 +462,8 @@ def test_cli_extra_context(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Awesomez**' in f.read() + content = Path('fake-project', 'README.rst').read_text() + assert 'Project name: **Awesomez**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -505,13 +544,9 @@ def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path """Verify --list-installed command correct invocation.""" fake_template_dir = os.path.dirname(os.path.abspath('fake-project')) os.makedirs(os.path.dirname(user_config_path)) - with open(user_config_path, 'w') as config_file: - # In YAML, double quotes mean to use escape sequences. - # Single quotes mean we will have unescaped backslahes. - # http://blogs.perl.org/users/tinita/2018/03/ - # strings-in-yaml---to-quote-or-not-to-quote.html - config_file.write("cookiecutters_dir: '%s'" % fake_template_dir) - open(os.path.join('fake-project', 'cookiecutter.json'), 'w').write('{}') + # Single quotes in YAML will not parse escape codes (\). + Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'") + Path("fake-project", "cookiecutter.json").write_text('{}') result = cli_runner( '--list-installed', @@ -529,8 +564,7 @@ def test_debug_list_installed_templates_failure( ): """Verify --list-installed command error on invocation.""" os.makedirs(os.path.dirname(user_config_path)) - with open(user_config_path, 'w') as config_file: - config_file.write('cookiecutters_dir: "/notarealplace/"') + Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"') result = cli_runner( '--list-installed', '--config-file', user_config_path, str(debug_file) @@ -551,8 +585,8 @@ def test_directory_repo(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir("fake-project") - with open(os.path.join("fake-project", "README.rst")) as f: - assert "Project name: **Fake Project**" in f.read() + content = Path("fake-project", "README.rst").read_text() + assert "Project name: **Fake Project**" in content cli_accept_hook_arg_testdata = [ @@ -598,6 +632,7 @@ def test_cli_accept_hooks( directory=None, skip_if_file_exists=False, accept_hooks=expected, + keep_project_on_failure=False, ) diff --git a/tests/test_cookiecutter_local_no_input.py b/tests/test_cookiecutter_local_no_input.py index 0031cbcbf..9e89c9539 100644 --- a/tests/test_cookiecutter_local_no_input.py +++ b/tests/test_cookiecutter_local_no_input.py @@ -5,6 +5,7 @@ """ import os import textwrap +from pathlib import Path import pytest @@ -65,9 +66,8 @@ def test_cookiecutter_no_input_return_rendered_file(): """Verify Jinja2 templating correctly works in `cookiecutter.json` file.""" project_dir = main.cookiecutter('tests/fake-repo-pre', no_input=True) assert project_dir == os.path.abspath('fake-project') - with open(os.path.join(project_dir, 'README.rst')) as fh: - contents = fh.read() - assert "Project name: **Fake Project**" in contents + content = Path(project_dir, 'README.rst').read_text() + assert "Project name: **Fake Project**" in content @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') @@ -76,11 +76,9 @@ def test_cookiecutter_dict_values_in_context(): project_dir = main.cookiecutter('tests/fake-repo-dict', no_input=True) assert project_dir == os.path.abspath('fake-project-dict') - with open(os.path.join(project_dir, 'README.md')) as fh: - contents = fh.read() - + content = Path(project_dir, 'README.md').read_text() assert ( - contents + content == textwrap.dedent( """ # README diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index cd5f8aa3a..fdf2861be 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -4,8 +4,7 @@ Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ -import codecs -import os +from pathlib import Path import pytest @@ -18,7 +17,7 @@ ) def template(request): """Fixture. Allows to split pre and post hooks test directories.""" - return 'tests/test-extensions/' + request.param + return f"tests/test-extensions/{request.param}" @pytest.fixture(autouse=True) @@ -40,9 +39,5 @@ def test_hook_with_extension(template, output_dir): extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) - readme_file = os.path.join(project_dir, 'README.rst') - - with codecs.open(readme_file, encoding='utf8') as f: - readme = f.read().strip() - - assert readme == 'Hello Cookiemonster!' + readme = Path(project_dir, 'README.rst').read_text(encoding="utf-8") + assert readme.strip() == 'Hello Cookiemonster!' diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index e73ef9c1b..d229e3d8c 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -1,9 +1,10 @@ """Verify Jinja2 filters/extensions are available from pre-gen/post-gen hooks.""" import os +import uuid +from pathlib import Path import freezegun import pytest -import uuid from cookiecutter.main import cookiecutter @@ -25,7 +26,7 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.readlines() expected_lines = [ @@ -57,8 +58,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) - assert True diff --git a/tests/test_find.py b/tests/test_find.py index 761c02235..affbd806e 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -1,5 +1,5 @@ """Tests for `cookiecutter.find` module.""" -import os +from pathlib import Path import pytest @@ -9,12 +9,12 @@ @pytest.fixture(params=['fake-repo-pre', 'fake-repo-pre2']) def repo_dir(request): """Fixture returning path for `test_find_template` test.""" - return os.path.join('tests', request.param) + return Path('tests', request.param) def test_find_template(repo_dir): """Verify correctness of `find.find_template` path detection.""" template = find.find_template(repo_dir=repo_dir) - test_dir = os.path.join(repo_dir, '{{cookiecutter.repo_name}}') + test_dir = Path(repo_dir, '{{cookiecutter.repo_name}}') assert template == test_dir diff --git a/tests/test_generate_context.py b/tests/test_generate_context.py index 44ab99194..892a7588b 100644 --- a/tests/test_generate_context.py +++ b/tests/test_generate_context.py @@ -202,3 +202,45 @@ def test_apply_overwrites_sets_default_for_choice_variable(template_context): ) assert template_context['orientation'] == ['landscape', 'all', 'portrait'] + + +def test_apply_overwrites_in_nested_dict(): + """Verify nested dict in default content settings are correctly replaced.""" + expected_context = { + 'nested_dict': OrderedDict( + [ + ('full_name', 'Raphael Pierzina'), + ('github_username', 'hackebrot'), + ( + 'project', + OrderedDict( + [ + ('name', 'My Kivy Project'), + ('description', 'My Kivy Project'), + ('repo_name', '{{cookiecutter.project_name|lower}}'), + ('orientation', ["all", "landscape", "portrait"]), + ] + ), + ), + ] + ) + } + + generated_context = generate.generate_context( + context_file='tests/test-generate-context/nested_dict.json', + default_context={ + 'not_in_template': 'foobar', + 'project': { + 'description': 'My Kivy Project', + }, + }, + extra_context={ + 'also_not_in_template': 'foobar2', + 'github_username': 'hackebrot', + 'project': { + 'name': 'My Kivy Project', + }, + }, + ) + + assert generated_context == expected_context diff --git a/tests/test_generate_copy_without_render.py b/tests/test_generate_copy_without_render.py index 7d614824c..9e6039787 100644 --- a/tests/test_generate_copy_without_render.py +++ b/tests/test_generate_copy_without_render.py @@ -1,5 +1,6 @@ """Verify correct work of `_copy_without_render` context option.""" import os +from pathlib import Path import pytest @@ -43,33 +44,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with open('test_copy_without_render/README.txt') as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_1 = Path('test_copy_without_render/README.txt').read_text() + assert '{{cookiecutter.render_test}}' in file_1 - with open('test_copy_without_render/README.rst') as f: - assert 'I have been rendered!' in f.read() + file_2 = Path('test_copy_without_render/README.rst').read_text() + assert 'I have been rendered!' in file_2 - with open( + file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_3 - with open( + file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ) as f: - assert 'I have been rendered' in f.read() + ).read_text() + assert 'I have been rendered' in file_4 - with open( + file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_5 - with open('test_copy_without_render/rendered/not_rendered.yml') as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() + assert '{{cookiecutter.render_test}}' in file_6 - with open( + file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_7 diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py new file mode 100644 index 000000000..c2d836e3d --- /dev/null +++ b/tests/test_generate_copy_without_render_override.py @@ -0,0 +1,95 @@ +"""Verify correct work of `_copy_without_render` context option.""" +import os +from pathlib import Path + +import pytest + +from cookiecutter import generate +from cookiecutter import utils + + +@pytest.fixture +def remove_test_dir(): + """Fixture. Remove the folder that is created by the test.""" + yield + if os.path.exists('test_copy_without_render'): + utils.rmtree('test_copy_without_render') + + +@pytest.mark.usefixtures('clean_system', 'remove_test_dir') +def test_generate_copy_without_render_extensions(): + """Verify correct work of `_copy_without_render` context option. + + Some files/directories should be rendered during invocation, + some just copied, without any modification. + """ + # first run + generate.generate_files( + context={ + 'cookiecutter': { + 'repo_name': 'test_copy_without_render', + 'render_test': 'I have been rendered!', + '_copy_without_render': [ + '*not-rendered', + 'rendered/not_rendered.yml', + '*.txt', + '{{cookiecutter.repo_name}}-rendered/README.md', + ], + } + }, + repo_dir='tests/test-generate-copy-without-render-override', + ) + + # second run with override flag to True + generate.generate_files( + context={ + 'cookiecutter': { + 'repo_name': 'test_copy_without_render', + 'render_test': 'I have been rendered!', + '_copy_without_render': [ + '*not-rendered', + 'rendered/not_rendered.yml', + '*.txt', + '{{cookiecutter.repo_name}}-rendered/README.md', + ], + } + }, + overwrite_if_exists=True, + repo_dir='tests/test-generate-copy-without-render', + ) + + dir_contents = os.listdir('test_copy_without_render') + + assert 'test_copy_without_render-not-rendered' in dir_contents + assert 'test_copy_without_render-rendered' in dir_contents + + file_1 = Path('test_copy_without_render/README.txt').read_text() + assert '{{cookiecutter.render_test}}' in file_1 + + file_2 = Path('test_copy_without_render/README.rst').read_text() + assert 'I have been rendered!' in file_2 + + file_3 = Path( + 'test_copy_without_render/test_copy_without_render-rendered/README.txt' + ).read_text() + assert '{{cookiecutter.render_test}}' in file_3 + + file_4 = Path( + 'test_copy_without_render/test_copy_without_render-rendered/README.rst' + ).read_text() + assert 'I have been rendered' in file_4 + + file_5 = Path( + 'test_copy_without_render/' + 'test_copy_without_render-not-rendered/' + 'README.rst' + ).read_text() + assert '{{cookiecutter.render_test}}' in file_5 + + file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() + assert '{{cookiecutter.render_test}}' in file_6 + + file_7 = Path( + 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' + ).read_text() + assert '{{cookiecutter.render_test}}' in file_7 diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 2ca30dff2..9ff622168 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -1,6 +1,8 @@ """Tests for `generate_file` function, part of `generate_files` function workflow.""" import json import os +import re +from pathlib import Path import pytest from jinja2 import FileSystemLoader @@ -44,9 +46,8 @@ def test_generate_file(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert generated_text == 'Testing cheese' + generated_text = Path('tests/files/cheese.txt').read_text() + assert generated_text == 'Testing cheese' def test_generate_file_jsonify_filter(env): @@ -57,9 +58,8 @@ def test_generate_file_jsonify_filter(env): project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert json.loads(generated_text) == data + generated_text = Path('tests/files/cheese.txt').read_text() + assert json.loads(generated_text) == data @pytest.mark.parametrize("length", (10, 40)) @@ -71,9 +71,8 @@ def test_generate_file_random_ascii_string(env, length, punctuation): context = {"cookiecutter": data, "length": length, "punctuation": punctuation} generate.generate_file(project_dir=".", infile=infile, context=context, env=env) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert len(generated_text) == length + generated_text = Path('tests/files/cheese.txt').read_text() + assert len(generated_text) == length def test_generate_file_with_true_condition(env): @@ -91,9 +90,8 @@ def test_generate_file_with_true_condition(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert generated_text == 'Testing that generate_file was y' + generated_text = Path('tests/files/cheese.txt').read_text() + assert generated_text == 'Testing that generate_file was y' def test_generate_file_with_false_condition(env): @@ -114,17 +112,16 @@ def test_generate_file_with_false_condition(env): @pytest.fixture -def expected_msg(): +def expected_msg_regex(): """Fixture. Used to ensure that exception generated text contain full data.""" - msg = ( + return re.compile( 'Missing end of comment tag\n' - ' File "./tests/files/syntax_error.txt", line 1\n' - ' I eat {{ syntax_error }} {# this comment is not closed}' + ' {2}File "(.[/\\\\])*tests[/\\\\]files[/\\\\]syntax_error.txt", line 1\n' + ' {4}I eat {{ syntax_error }} {# this comment is not closed}' ) - return msg.replace("/", os.sep) -def test_generate_file_verbose_template_syntax_error(env, expected_msg): +def test_generate_file_verbose_template_syntax_error(env, expected_msg_regex): """Verify correct exception raised on syntax error in file before generation.""" with pytest.raises(TemplateSyntaxError) as exception: generate.generate_file( @@ -133,7 +130,7 @@ def test_generate_file_verbose_template_syntax_error(env, expected_msg): context={'syntax_error': 'syntax_error'}, env=env, ) - assert str(exception.value) == expected_msg + assert expected_msg_regex.match(str(exception.value)) def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): @@ -148,7 +145,7 @@ def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -166,7 +163,7 @@ def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 4e21c7053..bb4075cae 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -44,7 +44,7 @@ def test_generate_files(tmp_path): assert simple_file.exists() assert simple_file.is_file() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' @@ -60,7 +60,7 @@ def test_generate_files_with_linux_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -83,8 +83,8 @@ def test_generate_files_with_jinja2_environment(tmp_path): assert conditions_file.is_file() assert conditions_file.exists() - simple_text = conditions_file.open('rt', encoding='utf-8').read() - assert simple_text == u'I eat pizzä\n' + simple_text = conditions_file.read_text(encoding='utf-8') + assert simple_text == 'I eat pizzä\n' def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_path): @@ -100,7 +100,7 @@ def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_pat assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\r\n' assert f.newlines == '\r\n' @@ -118,7 +118,7 @@ def test_generate_files_with_windows_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' @@ -136,7 +136,7 @@ def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' @@ -202,7 +202,6 @@ def test_generate_files_permissions(tmp_path): output_dir=tmp_path, ) - assert Path(tmp_path, 'inputpermissions/simple.txt').exists() assert Path(tmp_path, 'inputpermissions/simple.txt').is_file() # Verify source simple.txt should still be 0o644 @@ -241,7 +240,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: + with Path(simple_file).open('w') as f: f.write('temp') generate.generate_files( @@ -257,7 +256,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -267,8 +266,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: - f.write('temp') + Path(simple_file).write_text('temp') with pytest.raises(exceptions.OutputDirExistsException): generate.generate_files( @@ -279,11 +277,10 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): ) assert Path(simple_file).is_file() - assert Path(simple_file).exists() assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -293,8 +290,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: - f.write('temp') + Path(simple_file).write_text('temp') generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, @@ -308,7 +304,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' @@ -382,7 +378,7 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context @@ -390,6 +386,18 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): assert not Path(output_dir).joinpath('testproject').exists() +def test_keep_project_dir_on_failure(output_dir, undefined_context): + """Verify correct error raised when directory name cannot be rendered.""" + with pytest.raises(exceptions.UndefinedVariableInTemplate): + generate.generate_files( + repo_dir='tests/undefined-variable/dir-name/', + output_dir=output_dir, + context=undefined_context, + keep_project_on_failure=True, + ) + assert Path(output_dir).joinpath('testproject').exists() + + def test_raise_undefined_variable_dir_name_existing_project( output_dir, undefined_context ): @@ -407,7 +415,7 @@ def test_raise_undefined_variable_dir_name_existing_project( error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context diff --git a/tests/test_generate_hooks.py b/tests/test_generate_hooks.py index 9624bb8ae..a57e0dbde 100644 --- a/tests/test_generate_hooks.py +++ b/tests/test_generate_hooks.py @@ -2,6 +2,7 @@ import errno import os import sys +from pathlib import Path import pytest @@ -123,7 +124,7 @@ def test_run_failing_hook_removes_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") @@ -152,7 +153,7 @@ def test_run_failing_hook_preserves_existing_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") diff --git a/tests/test_hooks.py b/tests/test_hooks.py index d8b55dff2..9abd66af2 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,9 +1,10 @@ """Tests for `cookiecutter.hooks` module.""" -import os import errno +import os import stat import sys import textwrap +from pathlib import Path import pytest @@ -18,10 +19,9 @@ def make_test_repo(name, multiple_hooks=False): os.mkdir(hook_dir) os.mkdir(template) - with open(os.path.join(template, 'README.rst'), 'w') as f: - f.write("foo\n===\n\nbar\n") + Path(template, 'README.rst').write_text("foo\n===\n\nbar\n") - with open(os.path.join(hook_dir, 'pre_gen_project.py'), 'w') as f: + with Path(hook_dir, 'pre_gen_project.py').open('w') as f: f.write("#!/usr/bin/env python\n") f.write("# -*- coding: utf-8 -*-\n") f.write("from __future__ import print_function\n") @@ -32,7 +32,7 @@ def make_test_repo(name, multiple_hooks=False): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(hook_dir, post), 'w') as f: + with Path(hook_dir, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -40,7 +40,7 @@ def make_test_repo(name, multiple_hooks=False): else: post = 'post_gen_project.sh' filename = os.path.join(hook_dir, post) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -52,7 +52,7 @@ def make_test_repo(name, multiple_hooks=False): if multiple_hooks: if sys.platform.startswith('win'): pre = 'pre_gen_project.bat' - with open(os.path.join(hook_dir, pre), 'w') as f: + with Path(hook_dir, pre).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -60,7 +60,7 @@ def make_test_repo(name, multiple_hooks=False): else: pre = 'pre_gen_project.sh' filename = os.path.join(hook_dir, pre) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -71,7 +71,7 @@ def make_test_repo(name, multiple_hooks=False): return post -class TestFindHooks(object): +class TestFindHooks: """Class to unite find hooks related tests in one place.""" repo_path = 'tests/test-hooks' @@ -91,7 +91,7 @@ def test_find_hook(self): actual_hook_path = hooks.find_hook('pre_gen_project') assert expected_pre == actual_hook_path[0] - expected_post = os.path.abspath('hooks/{}'.format(self.post_hook)) + expected_post = os.path.abspath(f'hooks/{self.post_hook}') actual_hook_path = hooks.find_hook('post_gen_project') assert expected_post == actual_hook_path[0] @@ -111,7 +111,7 @@ def test_hook_not_found(self): assert hooks.find_hook('unknown_hook') is None -class TestExternalHooks(object): +class TestExternalHooks: """Class to unite tests for hooks with different project paths.""" repo_path = os.path.abspath('tests/test-hooks/') @@ -154,7 +154,7 @@ def test_run_failing_script(self, mocker): with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) - assert 'Hook script failed (error: {})'.format(err) in str(excinfo.value) + assert f'Hook script failed (error: {err})' in str(excinfo.value) def test_run_failing_script_enoexec(self, mocker): """Test correct exception raise if run_script fails.""" @@ -182,13 +182,13 @@ def test_run_script_with_context(self): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(self.hooks_path, post), 'w') as f: + with Path(self.hooks_path, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") f.write("echo. >{{cookiecutter.file}}\n") else: - with open(hook_path, 'w') as fh: + with Path(hook_path).open('w') as fh: fh.write("#!/bin/bash\n") fh.write("\n") fh.write("echo 'post generation hook';\n") @@ -221,7 +221,7 @@ def test_run_failing_hook(self): hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py') tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") diff --git a/tests/test_output_folder.py b/tests/test_output_folder.py index 166a45005..00c4f7846 100644 --- a/tests/test_output_folder.py +++ b/tests/test_output_folder.py @@ -5,6 +5,7 @@ TestOutputFolder.test_output_folder """ import os +from pathlib import Path import pytest @@ -32,11 +33,11 @@ def test_output_folder(): something = """Hi! My name is Audrey Greenfeld. It is 2014.""" - something2 = open('output_folder/something.txt').read() + something2 = Path('output_folder/something.txt').read_text() assert something == something2 in_folder = "The color is green and the letter is D." - in_folder2 = open('output_folder/folder/in_folder.txt').read() + in_folder2 = Path('output_folder/folder/in_folder.txt').read_text() assert in_folder == in_folder2 assert os.path.isdir('output_folder/im_a.dir') diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 8187cb283..9e85bcd9e 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -21,7 +21,7 @@ class TestRenderVariable: 'raw_var, rendered_var', [ (1, '1'), - (True, 'True'), + (True, True), ('foo', 'foo'), ('{{cookiecutter.project}}', 'foobar'), (None, None), @@ -39,7 +39,7 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): assert result == rendered_var # Make sure that non None non str variables are converted beforehand - if raw_var is not None: + if raw_var is not None and not isinstance(raw_var, bool): if not isinstance(raw_var, str): raw_var = str(raw_var) from_string.assert_called_once_with(raw_var) @@ -49,10 +49,10 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): @pytest.mark.parametrize( 'raw_var, rendered_var', [ - ({1: True, 'foo': False}, {'1': 'True', 'foo': 'False'}), + ({1: True, 'foo': False}, {'1': True, 'foo': False}), ( {'{{cookiecutter.project}}': ['foo', 1], 'bar': False}, - {'foobar': ['foo', '1'], 'bar': 'False'}, + {'foobar': ['foo', '1'], 'bar': False}, ), (['foo', '{{cookiecutter.project}}', None], ['foo', 'foobar', None]), ], @@ -66,7 +66,7 @@ def test_convert_to_str_complex_variables(self, raw_var, rendered_var): assert result == rendered_var -class TestPrompt(object): +class TestPrompt: """Class to unite user prompt related tests.""" @pytest.mark.parametrize( @@ -210,11 +210,11 @@ def test_should_render_private_variables_with_two_underscores(self): [ ('foo', 'Hello world'), ('bar', 123), - ('rendered_foo', u'{{ cookiecutter.foo|lower }}'), + ('rendered_foo', '{{ cookiecutter.foo|lower }}'), ('rendered_bar', 123), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), - ('__rendered_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('__rendered_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('__rendered_hidden_bar', 123), ] ) @@ -226,7 +226,7 @@ def test_should_render_private_variables_with_two_underscores(self): ('bar', '123'), ('rendered_foo', 'hello world'), ('rendered_bar', '123'), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), ('__rendered_hidden_foo', 'hello world'), ('__rendered_hidden_bar', '123'), @@ -252,7 +252,7 @@ def test_should_not_render_private_variables(self): assert cookiecutter_dict == context['cookiecutter'] -class TestReadUserChoice(object): +class TestReadUserChoice: """Class to unite choices prompt related tests.""" def test_should_invoke_read_user_choice(self, mocker): @@ -332,7 +332,7 @@ def test_should_render_choices(self, mocker): assert cookiecutter_dict == expected -class TestPromptChoiceForConfig(object): +class TestPromptChoiceForConfig: """Class to unite choices prompt related tests with config test.""" @pytest.fixture @@ -380,6 +380,42 @@ def test_should_read_user_choice(self, mocker, choices, context): assert expected_choice == actual_choice +class TestReadUserYesNo(object): + """Class to unite boolean prompt related tests.""" + + @pytest.mark.parametrize( + 'run_as_docker', + ( + True, + False, + ), + ) + def test_should_invoke_read_user_yes_no(self, mocker, run_as_docker): + """Verify correct function called for boolean variables.""" + read_user_yes_no = mocker.patch('cookiecutter.prompt.read_user_yes_no') + read_user_yes_no.return_value = run_as_docker + + read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') + + context = {'cookiecutter': {'run_as_docker': run_as_docker}} + + cookiecutter_dict = prompt.prompt_for_config(context) + + assert not read_user_variable.called + read_user_yes_no.assert_called_once_with('run_as_docker', run_as_docker) + assert cookiecutter_dict == {'run_as_docker': run_as_docker} + + def test_boolean_parameter_no_input(self): + """Verify boolean parameter sent to prompt for config with no input.""" + context = { + 'cookiecutter': { + 'run_as_docker': True, + } + } + cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) + assert cookiecutter_dict == context['cookiecutter'] + + @pytest.mark.parametrize( 'context', ( diff --git a/tests/test_read_user_choice.py b/tests/test_read_user_choice.py index ef9ae62f4..f3573593c 100644 --- a/tests/test_read_user_choice.py +++ b/tests/test_read_user_choice.py @@ -24,7 +24,7 @@ def test_click_invocation(mocker, user_choice, expected_value): choice.return_value = click.Choice(OPTIONS) prompt = mocker.patch('click.prompt') - prompt.return_value = '{}'.format(user_choice) + prompt.return_value = f'{user_choice}' assert read_user_choice('varname', OPTIONS) == expected_value diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py index 56c9eda6a..c907f2855 100644 --- a/tests/test_specify_output_dir.py +++ b/tests/test_specify_output_dir.py @@ -57,6 +57,7 @@ def test_api_invocation(mocker, template, output_dir, context): skip_if_file_exists=False, output_dir=output_dir, accept_hooks=True, + keep_project_on_failure=False, ) @@ -73,4 +74,5 @@ def test_default_output_dir(mocker, template, context): skip_if_file_exists=False, output_dir='.', accept_hooks=True, + keep_project_on_failure=False, ) diff --git a/tests/test_templates.py b/tests/test_templates.py new file mode 100644 index 000000000..44b9475d2 --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,39 @@ +""" +test_custom_extension_in_hooks. + +Tests to ensure custom cookiecutter extensions are properly made available to +pre- and post-gen hooks. +""" +from pathlib import Path + +import pytest + +from cookiecutter import main + + +@pytest.fixture +def output_dir(tmpdir): + """Fixture. Create and return custom temp directory for test.""" + return str(tmpdir.mkdir('templates')) + + +@pytest.mark.parametrize("template", ["include", "no-templates", "extends", "super"]) +def test_build_templates(template, output_dir): + """ + Verify Templates Design keywords. + + no-templates is a compatibility tests for repo without `templates` directory + """ + project_dir = main.cookiecutter( + f'tests/test-templates/{template}', + no_input=True, + output_dir=output_dir, + ) + + readme = Path(project_dir, 'requirements.txt').read_text() + + assert readme.splitlines() == [ + "pip", + "Click", + "pytest", + ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 988fc3887..fdd3692e2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,9 +17,7 @@ def make_readonly(path): def test_force_delete(mocker, tmp_path): """Verify `utils.force_delete` makes files writable.""" ro_file = Path(tmp_path, 'bar') - - with open(ro_file, "w") as f: - f.write("Test data") + ro_file.write_text("Test data") make_readonly(ro_file) rmtree = mocker.Mock() @@ -33,9 +31,9 @@ def test_force_delete(mocker, tmp_path): def test_rmtree(tmp_path): """Verify `utils.rmtree` remove files marked as read-only.""" - with open(Path(tmp_path, 'bar'), "w") as f: - f.write("Test data") - make_readonly(Path(tmp_path, 'bar')) + file_path = Path(tmp_path, "bar") + file_path.write_text("Test data") + make_readonly(file_path) utils.rmtree(tmp_path) @@ -51,8 +49,8 @@ def test_make_sure_path_exists(tmp_path): existing_directory = tmp_path directory_to_create = Path(tmp_path, "not_yet_created") - assert utils.make_sure_path_exists(existing_directory) - assert utils.make_sure_path_exists(directory_to_create) + utils.make_sure_path_exists(existing_directory) + utils.make_sure_path_exists(directory_to_create) # Ensure by base system methods. assert existing_directory.is_dir() @@ -67,14 +65,10 @@ def test_make_sure_path_exists_correctly_handle_os_error(mocker): Should return True if directory exist or created. Should return False if impossible to create directory (for example protected) """ - - def raiser(*args, **kwargs): - raise OSError() - - mocker.patch("os.makedirs", raiser) - uncreatable_directory = Path('protected_path') - - assert not utils.make_sure_path_exists(uncreatable_directory) + mocker.patch("pathlib.Path.mkdir", side_effect=OSError) + with pytest.raises(OSError) as err: + utils.make_sure_path_exists(Path('protected_path')) + assert str(err.value) == "Unable to create directory at protected_path" def test_work_in(tmp_path): @@ -175,10 +169,7 @@ def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmp_path): cloned template repo, it should not be deleted.""" def answer(question, default): - if 'okay to delete' in question: - return False - else: - return True + return 'okay to delete' not in question mock_read_user = mocker.patch( 'cookiecutter.utils.read_user_yes_no', side_effect=answer, autospec=True diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index cd4ac13d7..ab9598ed8 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -28,11 +28,11 @@ def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): autospec=True, ) - vcs.clone('https://github.com/foo/bar/', clone_to_dir=str(clone_dir), no_input=True) + vcs.clone('https://github.com/foo/bar/', clone_to_dir=clone_dir, no_input=True) mock_subprocess.assert_called_once_with( ['git', 'clone', 'https://github.com/foo/bar'], - cwd=str(clone_dir), + cwd=clone_dir, stderr=subprocess.STDOUT, ) @@ -114,26 +114,32 @@ def test_clone_should_invoke_vcs_command( branch = 'foobar' repo_dir = vcs.clone( - repo_url, checkout=branch, clone_to_dir=str(clone_dir), no_input=True + repo_url, checkout=branch, clone_to_dir=clone_dir, no_input=True ) assert repo_dir == expected_repo_dir mock_subprocess.assert_any_call( - [repo_type, 'clone', repo_url], cwd=str(clone_dir), stderr=subprocess.STDOUT + [repo_type, 'clone', repo_url], cwd=clone_dir, stderr=subprocess.STDOUT ) + + branch_info = [branch] + # We sanitize branch information for Mercurial + if repo_type == "hg": + branch_info.insert(0, "--") + mock_subprocess.assert_any_call( - [repo_type, 'checkout', branch], cwd=expected_repo_dir, stderr=subprocess.STDOUT + [repo_type, 'checkout', *branch_info], + cwd=expected_repo_dir, + stderr=subprocess.STDOUT, ) @pytest.mark.parametrize( 'error_message', [ - ( - "fatal: repository 'https://github.com/hackebro/cookiedozer' not found" - ).encode('utf-8'), - 'hg: abort: HTTP Error 404: Not Found'.encode('utf-8'), + (b"fatal: repository 'https://github.com/hackebro/cookiedozer' not found"), + b'hg: abort: HTTP Error 404: Not Found', ], ) def test_clone_handles_repo_typo(mocker, clone_dir, error_message): @@ -153,17 +159,15 @@ def test_clone_handles_repo_typo(mocker, clone_dir, error_message): vcs.clone(repository_url, clone_to_dir=str(clone_dir), no_input=True) assert str(err.value) == ( - 'The repository {} could not be found, have you made a typo?' - ).format(repository_url) + f'The repository {repository_url} could not be found, have you made a typo?' + ) @pytest.mark.parametrize( 'error_message', [ - ( - "error: pathspec 'unknown_branch' did not match any file(s) known to git" - ).encode('utf-8'), - "hg: abort: unknown revision 'unknown_branch'!".encode('utf-8'), + b"error: pathspec 'unknown_branch' did not match any file(s) known to git", + b"hg: abort: unknown revision 'unknown_branch'!", ], ) def test_clone_handles_branch_typo(mocker, clone_dir, error_message): @@ -186,8 +190,8 @@ def test_clone_handles_branch_typo(mocker, clone_dir, error_message): assert str(err.value) == ( 'The unknown_branch branch of repository ' - '{} could not found, have you made a typo?' - ).format(repository_url) + f'{repository_url} could not found, have you made a typo?' + ) def test_clone_unknown_subprocess_error(mocker, clone_dir): @@ -196,9 +200,7 @@ def test_clone_unknown_subprocess_error(mocker, clone_dir): 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[ - subprocess.CalledProcessError( - -1, 'cmd', output='Something went wrong'.encode('utf-8') - ) + subprocess.CalledProcessError(-1, 'cmd', output=b'Something went wrong') ], ) diff --git a/tests/vcs/test_identify_repo.py b/tests/vcs/test_identify_repo.py index fe264edcd..bfb3d56a2 100644 --- a/tests/vcs/test_identify_repo.py +++ b/tests/vcs/test_identify_repo.py @@ -24,14 +24,14 @@ ), ('https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg'), ( - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', ), ( - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', ), ( 'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git', diff --git a/tests/zipfile/test_unzip.py b/tests/zipfile/test_unzip.py index 2bf58b00f..9d4448e59 100644 --- a/tests/zipfile/test_unzip.py +++ b/tests/zipfile/test_unzip.py @@ -1,8 +1,9 @@ """Tests for function unzip() from zipfile module.""" +import shutil import tempfile +from pathlib import Path import pytest -import shutil from cookiecutter import zipfile from cookiecutter.exceptions import InvalidZipRepository @@ -10,7 +11,7 @@ def mock_download(): """Fake download function.""" - with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk @@ -20,7 +21,7 @@ def mock_download(): def mock_download_with_empty_chunks(): """Fake download function.""" yield - with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk