Skip to content

Commit

Permalink
use pathlib
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel.eades committed Apr 22, 2024
1 parent 464c6b5 commit 24f099e
Show file tree
Hide file tree
Showing 12 changed files with 88 additions and 82 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Expand Up @@ -17,7 +17,7 @@ repos:
require_serial: true

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: mixed-line-ending
Expand Down Expand Up @@ -57,7 +57,7 @@ repos:
- id: check-manifest

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.5
rev: v0.4.0
hooks:
- id: ruff
args: [--fix]
Expand Down
10 changes: 4 additions & 6 deletions cookiecutter/config.py
Expand Up @@ -6,18 +6,16 @@
import copy
import logging
import os
from typing import TYPE_CHECKING, Any
from pathlib import Path
from typing import Any

import yaml

from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration

if TYPE_CHECKING:
from pathlib import Path

logger = logging.getLogger(__name__)

USER_CONFIG_PATH = os.path.expanduser('~/.cookiecutterrc')
USER_CONFIG_PATH = Path('~/.cookiecutterrc').expanduser()

BUILTIN_ABBREVIATIONS = {
'gh': 'https://github.com/{0}.git',
Expand Down Expand Up @@ -120,7 +118,7 @@ def get_user_config(
return copy.copy(DEFAULT_CONFIG)

# Load the given config file
if config_file and config_file is not USER_CONFIG_PATH:
if config_file and config_file is not str(USER_CONFIG_PATH):
logger.debug("Loading custom config from %s.", config_file)
return get_config(config_file)

Expand Down
6 changes: 4 additions & 2 deletions tests/replay/conftest.py
@@ -1,5 +1,7 @@
"""pytest fixtures for testing cookiecutter's replay feature."""

from pathlib import Path

import pytest


Expand All @@ -17,9 +19,9 @@ def context():


@pytest.fixture
def replay_test_dir() -> str:
def replay_test_dir() -> Path:
"""Fixture to test directory."""
return 'tests/test-replay/'
return Path('tests/test-replay/')


@pytest.fixture
Expand Down
17 changes: 8 additions & 9 deletions tests/replay/test_dump.py
@@ -1,7 +1,7 @@
"""test_dump."""

import json
import os
from pathlib import Path

import pytest

Expand All @@ -15,19 +15,18 @@ def template_name() -> str:


@pytest.fixture
def replay_file(replay_test_dir, template_name):
def replay_file(replay_test_dir: Path, template_name: str) -> Path:
"""Fixture to return a actual file name of the dump."""
file_name = f'{template_name}.json'
return os.path.join(replay_test_dir, file_name)
return replay_test_dir / f'{template_name}.json'


@pytest.fixture(autouse=True)
def remove_replay_dump(request, replay_file) -> None:
def remove_replay_dump(request, replay_file: Path) -> None:
"""Remove the replay file created by tests."""

def fin_remove_replay_file() -> None:
if os.path.exists(replay_file):
os.remove(replay_file)
if replay_file.exists():
replay_file.unlink()

request.addfinalizer(fin_remove_replay_file)

Expand Down Expand Up @@ -78,7 +77,7 @@ def test_run_json_dump(
template_name,
context,
replay_test_dir,
replay_file,
replay_file: Path,
) -> None:
"""Test that replay.dump runs json.dump under the hood and that the context \
is correctly written to the expected file in the replay_dir."""
Expand All @@ -94,5 +93,5 @@ def test_run_json_dump(

assert mock_json_dump.call_count == 1
(dumped_context, outfile_handler), _kwargs = mock_json_dump.call_args
assert outfile_handler.name == replay_file
assert outfile_handler.name == str(replay_file)
assert dumped_context == context
8 changes: 4 additions & 4 deletions tests/test_cli.py
Expand Up @@ -317,9 +317,9 @@ def test_cli_help(cli_runner, help_cli_flag) -> None:


@pytest.fixture
def user_config_path(tmp_path):
"""Pytest fixture return `user_config` argument as string."""
return str(tmp_path.joinpath("tests", "config.yaml"))
def user_config_path(tmp_path: Path) -> Path:
"""Pytest fixture return `user_config` argument as Path."""
return tmp_path.joinpath("tests", "config.yaml")


def test_user_config(mocker, cli_runner, user_config_path) -> None:
Expand Down Expand Up @@ -583,7 +583,7 @@ def test_debug_list_installed_templates(
cli_runner, debug_file, user_config_path
) -> None:
"""Verify --list-installed command correct invocation."""
fake_template_dir = os.path.dirname(os.path.abspath('fake-project'))
fake_template_dir = Path('fake-project').resolve().parent
os.makedirs(os.path.dirname(user_config_path))
# Single quotes in YAML will not parse escape codes (\).
Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'")
Expand Down
19 changes: 9 additions & 10 deletions tests/test_cookiecutter_local_no_input.py
Expand Up @@ -18,16 +18,15 @@ def remove_additional_dirs(request) -> None:
"""Fixture. Remove special directories which are created during the tests."""

def fin_remove_additional_dirs() -> None:
if os.path.isdir('fake-project'):
utils.rmtree('fake-project')
if os.path.isdir('fake-project-extra'):
utils.rmtree('fake-project-extra')
if os.path.isdir('fake-project-templated'):
utils.rmtree('fake-project-templated')
if os.path.isdir('fake-project-dict'):
utils.rmtree('fake-project-dict')
if os.path.isdir('fake-tmp'):
utils.rmtree('fake-tmp')
for project in {
'fake-project',
'fake-project-extra',
'fake-project-templated',
'fake-project-dict',
'fake-tmp',
}:
if Path(project).is_dir():
utils.rmtree(project)

request.addfinalizer(fin_remove_additional_dirs)

Expand Down
12 changes: 7 additions & 5 deletions tests/test_default_extensions.py
Expand Up @@ -21,13 +21,15 @@ def freeze():

def test_jinja2_time_extension(tmp_path) -> None:
"""Verify Jinja2 time extension work correctly."""
project_dir = cookiecutter(
'tests/test-extensions/default/', no_input=True, output_dir=str(tmp_path)
project_dir = Path(
cookiecutter(
'tests/test-extensions/default/', no_input=True, output_dir=str(tmp_path)
)
)
changelog_file = os.path.join(project_dir, 'HISTORY.rst')
assert os.path.isfile(changelog_file)
changelog_file = project_dir / 'HISTORY.rst'
assert changelog_file.exists()

with Path(changelog_file).open(encoding='utf-8') as f:
with changelog_file.open(encoding='utf-8') as f:
changelog_lines = f.readlines()

expected_lines = [
Expand Down
5 changes: 3 additions & 2 deletions tests/test_generate_file.py
Expand Up @@ -50,8 +50,9 @@ def test_generate_file(env) -> None:
context={'cookiecutter': {'generate_file': 'cheese'}},
env=env,
)
assert os.path.isfile('tests/files/cheese.txt')
generated_text = Path('tests/files/cheese.txt').read_text()
path = Path('tests/files/cheese.txt')
assert path.exists()
generated_text = path.read_text()
assert generated_text == 'Testing cheese'


Expand Down
7 changes: 4 additions & 3 deletions tests/test_get_user_config.py
Expand Up @@ -2,6 +2,7 @@

import os
import shutil
from pathlib import Path

import pytest

Expand All @@ -10,9 +11,9 @@


@pytest.fixture(scope='module')
def user_config_path():
def user_config_path() -> Path:
"""Fixture. Return user config path for current user."""
return os.path.expanduser('~/.cookiecutterrc')
return Path('~/.cookiecutterrc').expanduser()


@pytest.fixture(scope='function')
Expand Down Expand Up @@ -106,7 +107,7 @@ def test_specify_config_path(mocker, custom_config_path, custom_config) -> None:
assert user_config == custom_config


def test_default_config_path(user_config_path) -> None:
def test_default_config_path(user_config_path: Path) -> None:
"""Validate app configuration. User config path should match default path."""
assert user_config_path == config.USER_CONFIG_PATH

Expand Down
60 changes: 31 additions & 29 deletions tests/test_hooks.py
Expand Up @@ -12,7 +12,7 @@
from cookiecutter import exceptions, hooks, utils


def make_test_repo(name: str, multiple_hooks: bool = False) -> str:
def make_test_repo(name: Path, multiple_hooks: bool = False) -> str:
"""Create test repository for test setup methods."""
hook_dir = os.path.join(name, 'hooks')
template = os.path.join(name, 'input{{hooks}}')
Expand Down Expand Up @@ -75,7 +75,7 @@ def make_test_repo(name: str, multiple_hooks: bool = False) -> str:
class TestFindHooks:
"""Class to unite find hooks related tests in one place."""

repo_path = 'tests/test-hooks'
repo_path = Path('tests/test-hooks')

def setup_method(self, _method) -> None:
"""Find hooks related tests setup fixture."""
Expand Down Expand Up @@ -117,8 +117,8 @@ def test_hook_not_found(self) -> None:
class TestExternalHooks:
"""Class to unite tests for hooks with different project paths."""

repo_path = os.path.abspath('tests/test-hooks/')
hooks_path = os.path.abspath('tests/test-hooks/hooks')
repo_path = Path('tests/test-hooks/').resolve()
hooks_path = Path('tests/test-hooks/hooks').resolve()

def setup_method(self, _method) -> None:
"""External hooks related tests setup fixture."""
Expand All @@ -128,20 +128,22 @@ def teardown_method(self, _method) -> None:
"""External hooks related tests teardown fixture."""
utils.rmtree(self.repo_path)

if os.path.exists('python_pre.txt'):
os.remove('python_pre.txt')
if os.path.exists('shell_post.txt'):
os.remove('shell_post.txt')
if os.path.exists('shell_pre.txt'):
os.remove('shell_pre.txt')
if os.path.exists('tests/shell_post.txt'):
os.remove('tests/shell_post.txt')
if os.path.exists('tests/test-hooks/input{{hooks}}/python_pre.txt'):
os.remove('tests/test-hooks/input{{hooks}}/python_pre.txt')
if os.path.exists('tests/test-hooks/input{{hooks}}/shell_post.txt'):
os.remove('tests/test-hooks/input{{hooks}}/shell_post.txt')
if os.path.exists('tests/context_post.txt'):
os.remove('tests/context_post.txt')
for path in {
'python_pre.txt',
'shell_post.txt',
'shell_pre.txt',
'tests/shell_post.txt',
'tests/test-hooks/input{{hooks}}/python_pre.txt',
'tests/test-hooks/input{{hooks}}/shell_post.txt',
'tests/context_post.txt',
}:
if sys.version_info < (3, 8):
# remove when python 3.7 is dropped
path = Path(path)
if path.exists():
path.unlink()
else:
Path(path).unlink(missing_ok=True)

def test_run_script(self) -> None:
"""Execute a hook script, independently of project generation."""
Expand Down Expand Up @@ -181,7 +183,7 @@ def test_run_script_cwd(self) -> None:

def test_run_script_with_context(self) -> None:
"""Execute a hook script, passing a context."""
hook_path = os.path.join(self.hooks_path, 'post_gen_project.sh')
hook_path = self.hooks_path / 'post_gen_project.sh'

if sys.platform.startswith('win'):
post = 'post_gen_project.bat'
Expand All @@ -191,16 +193,16 @@ def test_run_script_with_context(self) -> None:
f.write("echo post generation hook\n")
f.write("echo. >{{cookiecutter.file}}\n")
else:
with Path(hook_path).open('w') as fh:
with hook_path.open('w') as fh:
fh.write("#!/bin/bash\n")
fh.write("\n")
fh.write("echo 'post generation hook';\n")
fh.write("touch 'shell_post.txt'\n")
fh.write("touch '{{cookiecutter.file}}'\n")
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IXUSR)
hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR)

hooks.run_script_with_context(
os.path.join(self.hooks_path, self.post_hook),
self.hooks_path / self.post_hook,
'tests',
{'cookiecutter': {'file': 'context_post.txt'}},
)
Expand All @@ -210,21 +212,21 @@ def test_run_script_with_context(self) -> None:
def test_run_hook(self) -> None:
"""Execute hook from specified template in specified output \
directory."""
tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')
tests_dir = self.repo_path / 'input{{hooks}}'
with utils.work_in(self.repo_path):
hooks.run_hook('pre_gen_project', tests_dir, {})
assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt'))
assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt'))
assert (tests_dir / 'python_pre.txt').exists()
assert (tests_dir / 'shell_pre.txt').exists()

hooks.run_hook('post_gen_project', tests_dir, {})
assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt'))
assert (tests_dir / 'shell_post.txt').exists()

def test_run_failing_hook(self) -> None:
"""Test correct exception raise if hook exit code is not zero."""
hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py')
tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')
hook_path = self.hooks_path / 'pre_gen_project.py'
tests_dir = self.repo_path / 'input{{hooks}}'

with Path(hook_path).open('w') as f:
with hook_path.open('w') as f:
f.write("#!/usr/bin/env python\n")
f.write("import sys; sys.exit(1)\n")

Expand Down
17 changes: 9 additions & 8 deletions tests/test_output_folder.py
Expand Up @@ -5,7 +5,6 @@
TestOutputFolder.test_output_folder
"""

import os
from pathlib import Path

import pytest
Expand All @@ -17,8 +16,9 @@
def remove_output_folder():
"""Remove the output folder after test."""
yield
if os.path.exists('output_folder'):
utils.rmtree('output_folder')
output_folder = Path('output_folder')
if output_folder.exists():
utils.rmtree(output_folder)


@pytest.mark.usefixtures('clean_system', 'remove_output_folder')
Expand All @@ -40,8 +40,9 @@ def test_output_folder() -> None:
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')
assert os.path.isfile('output_folder/im_a.dir/im_a.file.py')
dir = Path('output_folder/im_a.dir')
assert dir.is_dir()
assert (dir / 'im_a.file.py').is_file()


@pytest.mark.usefixtures('clean_system', 'remove_output_folder')
Expand All @@ -50,9 +51,9 @@ def test_exception_when_output_folder_exists() -> None:
context = generate.generate_context(
context_file='tests/test-output-folder/cookiecutter.json'
)
output_folder = context['cookiecutter']['test_name']
output_folder = Path(context['cookiecutter']['test_name'])

if not os.path.exists(output_folder):
os.makedirs(output_folder)
if not output_folder.exists():
output_folder.mkdir()
with pytest.raises(exceptions.OutputDirExistsException):
generate.generate_files(context=context, repo_dir='tests/test-output-folder')

0 comments on commit 24f099e

Please sign in to comment.