diff --git a/copier/main.py b/copier/main.py index 499cd95d4..70c0afbf0 100644 --- a/copier/main.py +++ b/copier/main.py @@ -1,4 +1,5 @@ """Main functions and classes, used to generate or update projects.""" +from __future__ import annotations import os import platform @@ -15,13 +16,9 @@ from typing import ( Callable, Iterable, - List, Literal, Mapping, - Optional, Sequence, - Set, - Union, get_args, ) from unicodedata import normalize @@ -169,9 +166,9 @@ class Worker: See [unsafe][] """ - src_path: Optional[str] = None + src_path: str | None = None dst_path: Path = Path(".") - answers_file: Optional[RelativePath] = None + answers_file: RelativePath | None = None vcs_ref: OptStr = None data: AnyByStrDict = field(default_factory=dict) exclude: StrSeq = () @@ -189,7 +186,7 @@ class Worker: skip_answered: bool = False answers: AnswersMap = field(default_factory=AnswersMap, init=False) - _cleanup_hooks: List[Callable] = field(default_factory=list, init=False) + _cleanup_hooks: list[Callable] = field(default_factory=list, init=False) def __enter__(self): """Allow using worker as a context manager.""" @@ -215,7 +212,7 @@ def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: """Check whether a template uses unsafe features.""" if self.unsafe: return - features: Set[str] = set() + features: set[str] = set() if self.template.jinja_extensions: features.add("jinja_extensions") if self.template.tasks: @@ -353,7 +350,7 @@ def _render_allowed( dst_relpath: Path, is_dir: bool = False, is_symlink: bool = False, - expected_contents: Union[bytes, Path] = b"", + expected_contents: bytes | Path = b"", ) -> bool: """Determine if a file or directory can be rendered. @@ -373,7 +370,7 @@ def _render_allowed( dst_abspath = Path(self.subproject.local_abspath, dst_relpath) previous_is_symlink = dst_abspath.is_symlink() try: - previous_content: Union[bytes, Path] + previous_content: bytes | Path if previous_is_symlink: previous_content = readlink(dst_abspath) else: @@ -646,7 +643,7 @@ def _render_folder(self, src_abspath: Path) -> None: else: self._render_file(file) - def _render_path(self, relpath: Path) -> Optional[Path]: + def _render_path(self, relpath: Path) -> Path | None: """Render one relative path. Args: @@ -1010,7 +1007,7 @@ def _git_initialize_repo(self): def run_copy( src_path: str, dst_path: StrOrPath = ".", - data: Optional[AnyByStrDict] = None, + data: AnyByStrDict | None = None, **kwargs, ) -> Worker: """Copy a template to a destination, from zero. @@ -1027,7 +1024,7 @@ def run_copy( def run_recopy( - dst_path: StrOrPath = ".", data: Optional[AnyByStrDict] = None, **kwargs + dst_path: StrOrPath = ".", data: AnyByStrDict | None = None, **kwargs ) -> Worker: """Update a subproject from its template, discarding subproject evolution. @@ -1044,7 +1041,7 @@ def run_recopy( def run_update( dst_path: StrOrPath = ".", - data: Optional[AnyByStrDict] = None, + data: AnyByStrDict | None = None, **kwargs, ) -> Worker: """Update a subproject, from its template. diff --git a/copier/subproject.py b/copier/subproject.py index 3040bebb7..3f30888d2 100644 --- a/copier/subproject.py +++ b/copier/subproject.py @@ -2,11 +2,12 @@ A *subproject* is a project that gets rendered and/or updated with Copier. """ +from __future__ import annotations from dataclasses import field from functools import cached_property from pathlib import Path -from typing import Callable, List, Optional +from typing import Callable import yaml from plumbum.machines import local @@ -32,7 +33,7 @@ class Subproject: local_abspath: AbsolutePath answers_relpath: Path = Path(".copier-answers.yml") - _cleanup_hooks: List[Callable] = field(default_factory=list, init=False) + _cleanup_hooks: list[Callable] = field(default_factory=list, init=False) def is_dirty(self) -> bool: """Indicate if the local template root is dirty. @@ -69,7 +70,7 @@ def last_answers(self) -> AnyByStrDict: } @cached_property - def template(self) -> Optional[Template]: + def template(self) -> Template | None: """Template, as it was used the last time.""" last_url = self.last_answers.get("_src_path") last_ref = self.last_answers.get("_commit") @@ -79,7 +80,7 @@ def template(self) -> Optional[Template]: return result @cached_property - def vcs(self) -> Optional[VCSTypes]: + def vcs(self) -> VCSTypes | None: """VCS type of the subproject.""" if is_in_git_repo(self.local_abspath): return "git" diff --git a/copier/template.py b/copier/template.py index dacd7a318..f903dc186 100644 --- a/copier/template.py +++ b/copier/template.py @@ -1,4 +1,6 @@ """Tools related to template management.""" +from __future__ import annotations + import re import sys from collections import ChainMap, defaultdict @@ -7,7 +9,7 @@ from functools import cached_property from pathlib import Path from shutil import rmtree -from typing import List, Literal, Mapping, Optional, Sequence, Set, Tuple +from typing import Literal, Mapping, Sequence from warnings import warn import dunamai @@ -31,7 +33,7 @@ from .vcs import checkout_latest_tag, clone, get_git, get_repo # Default list of files in the template to exclude from the rendered project -DEFAULT_EXCLUDE: Tuple[str, ...] = ( +DEFAULT_EXCLUDE: tuple[str, ...] = ( "copier.yaml", "copier.yml", "~*", @@ -45,7 +47,7 @@ DEFAULT_TEMPLATES_SUFFIX = ".jinja" -def filter_config(data: AnyByStrDict) -> Tuple[AnyByStrDict, AnyByStrDict]: +def filter_config(data: AnyByStrDict) -> tuple[AnyByStrDict, AnyByStrDict]: """Separates config and questions data.""" config_data: AnyByStrDict = {} questions_data = {} @@ -212,7 +214,7 @@ def _cleanup(self) -> None: onerror=handle_remove_readonly, ) - def _temp_clone(self) -> Optional[Path]: + def _temp_clone(self) -> Path | None: """Get the path to the temporary clone of the template. If the template hasn't yet been cloned, or if it was a local template, @@ -297,7 +299,7 @@ def envops(self) -> Mapping: return result @cached_property - def exclude(self) -> Tuple[str, ...]: + def exclude(self) -> tuple[str, ...]: """Get exclusions specified in the template, or default ones. See [exclude][]. @@ -310,7 +312,7 @@ def exclude(self) -> Tuple[str, ...]: ) @cached_property - def jinja_extensions(self) -> Tuple[str, ...]: + def jinja_extensions(self) -> tuple[str, ...]: """Get Jinja2 extensions specified in the template, or `()`. See [jinja_extensions][]. @@ -350,7 +352,7 @@ def metadata(self) -> AnyByStrDict: return result def migration_tasks( - self, stage: Literal["before", "after"], from_template: "Template" + self, stage: Literal["before", "after"], from_template: Template ) -> Sequence[Task]: """Get migration objects that match current version spec. @@ -362,7 +364,7 @@ def migration_tasks( stage: A valid stage name to find tasks for. from_template: Original template, from which we are migrating. """ - result: List[Task] = [] + result: list[Task] = [] if not (self.version and from_template.version): return result extra_env: Env = { @@ -386,7 +388,7 @@ def migration_tasks( return result @cached_property - def min_copier_version(self) -> Optional[Version]: + def min_copier_version(self) -> Version | None: """Get minimal copier version for the template and validates it. See [min_copier_version][]. @@ -409,7 +411,7 @@ def questions_data(self) -> AnyByStrDict: return result @cached_property - def secret_questions(self) -> Set[str]: + def secret_questions(self) -> set[str]: """Get names of secret questions from the template. These questions shouldn't be saved into the answers file. @@ -503,7 +505,7 @@ def url_expanded(self) -> str: return get_repo(self.url) or self.url @cached_property - def version(self) -> Optional[Version]: + def version(self) -> Version | None: """PEP440-compliant version object.""" if self.vcs != "git" or not self.commit: return None @@ -531,7 +533,7 @@ def version(self) -> Optional[Version]: return None @cached_property - def vcs(self) -> Optional[VCSTypes]: + def vcs(self) -> VCSTypes | None: """Get VCS system used by the template, if any.""" if get_repo(self.url): return "git" diff --git a/copier/tools.py b/copier/tools.py index 780f53990..f4eb61c65 100644 --- a/copier/tools.py +++ b/copier/tools.py @@ -1,4 +1,5 @@ """Some utility functions.""" +from __future__ import annotations import errno import os @@ -12,7 +13,7 @@ from importlib.metadata import version from pathlib import Path from types import TracebackType -from typing import Any, Callable, Literal, Optional, TextIO, Tuple, Type, Union, cast +from typing import Any, Callable, Literal, TextIO, cast import colorama from packaging.version import Version @@ -36,7 +37,7 @@ class Style: INDENT = " " * 2 HLINE = "-" * 42 -OS: Optional[Literal["linux", "macos", "windows"]] = cast( +OS: Literal["linux", "macos", "windows"] | None = cast( Any, { "Linux": "linux", @@ -63,11 +64,11 @@ def copier_version() -> Version: def printf( action: str, msg: Any = "", - style: Optional[IntSeq] = None, + style: IntSeq | None = None, indent: int = 10, - quiet: Union[bool, StrictBool] = False, + quiet: bool | StrictBool = False, file_: TextIO = sys.stdout, -) -> Optional[str]: +) -> str | None: """Print string with common format.""" if quiet: return None # HACK: Satisfy MyPy @@ -152,7 +153,7 @@ def handle_remove_readonly( func: Callable, path: str, # TODO: Change this union to simply `BaseException` when Python 3.11 support is dropped - exc: Union[BaseException, Tuple[Type[BaseException], BaseException, TracebackType]], + exc: BaseException | tuple[type[BaseException], BaseException, TracebackType], ) -> None: """Handle errors when trying to remove read-only files through `shutil.rmtree`. diff --git a/copier/user_data.py b/copier/user_data.py index 3422dae3b..6d63e980d 100644 --- a/copier/user_data.py +++ b/copier/user_data.py @@ -1,4 +1,6 @@ """Functions used to load user data.""" +from __future__ import annotations + import json import warnings from collections import ChainMap @@ -8,7 +10,7 @@ from hashlib import sha512 from os import urandom from pathlib import Path -from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Set, Union +from typing import Any, Callable, Mapping, Sequence import yaml from jinja2 import UndefinedError @@ -84,7 +86,7 @@ class AnswersMap: """ # Private - hidden: Set[str] = field(default_factory=set, init=False) + hidden: set[str] = field(default_factory=set, init=False) # Public user: AnyByStrDict = field(default_factory=dict) @@ -176,16 +178,16 @@ class Question: var_name: str answers: AnswersMap jinja_env: SandboxedEnvironment - choices: Union[Sequence[Any], Dict[Any, Any]] = field(default_factory=list) + choices: Sequence[Any] | dict[Any, Any] = field(default_factory=list) multiselect: bool = False default: Any = MISSING help: str = "" - multiline: Union[str, bool] = False + multiline: str | bool = False placeholder: str = "" secret: bool = False type: str = Field(default="", validate_default=True) validator: str = "" - when: Union[str, bool] = True + when: str | bool = True @field_validator("var_name") @classmethod @@ -246,7 +248,7 @@ def get_default(self) -> Any: result = self.cast_answer(result) return result - def get_default_rendered(self) -> Union[bool, str, Choice, None, MissingType]: + def get_default_rendered(self) -> bool | str | Choice | None | MissingType: """Get default answer rendered for the questionary lib. The questionary lib expects some specific data types, and returns @@ -416,7 +418,7 @@ def get_when(self) -> bool: return cast_to_bool(self.render_value(self.when)) def render_value( - self, value: Any, extra_answers: Optional[AnyByStrDict] = None + self, value: Any, extra_answers: AnyByStrDict | None = None ) -> str: """Render a single templated value using Jinja. diff --git a/poetry.lock b/poetry.lock index 8d679d139..b1ed944f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -263,6 +263,20 @@ files = [ [package.dependencies] packaging = ">=20.9" +[[package]] +name = "eval-type-backport" +version = "0.1.3" +description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." +optional = false +python-versions = ">=3.7" +files = [ + {file = "eval_type_backport-0.1.3-py3-none-any.whl", hash = "sha256:519d2a993b3da286df9f90e17f503f66435106ad870cf26620c5720e2158ddf2"}, + {file = "eval_type_backport-0.1.3.tar.gz", hash = "sha256:d83ee225331dfa009493cec1f3608a71550b515ee4749abe78da14e3c5e314f5"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "exceptiongroup" version = "1.1.0" @@ -1630,4 +1644,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "e77e027798e8d257d07fee377fb2581be217bbcd761326d043f3b5006e6e870f" +content-hash = "f362279db680309008ea6241cae313e374a88df324efdf88d028e626d2b7a0c0" diff --git a/pyproject.toml b/pyproject.toml index b353a8231..2260989d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ pyyaml = ">=5.3.1" pyyaml-include = ">=1.2" questionary = ">=1.8.1" typing-extensions = { version = ">=3.7.4,<5.0.0", python = "<3.9" } +eval-type-backport = { version = "^0.1.3", python = "<3.10" } [tool.poetry.group.dev] optional = true @@ -109,7 +110,7 @@ style = "pep440" vcs = "git" [tool.ruff.lint] -extend-select = ["B", "D", "E", "F", "I", "PGH", "UP"] +extend-select = ["B", "D", "E", "F", "FA", "I", "PGH", "UP"] extend-ignore = ['B028', "B904", "D105", "D107", "E501"] [tool.ruff.lint.per-file-ignores] diff --git a/tests/conftest.py b/tests/conftest.py index a71137c81..445c7f3d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import platform import sys -from typing import Iterator, Optional, Tuple +from typing import Iterator import pytest from coverage.tracer import CTracer @@ -21,7 +23,7 @@ def spawn() -> Spawn: "pexpect fails on Windows", ) - def _spawn(cmd: Tuple[str, ...], *, timeout: Optional[int] = None) -> PopenSpawn: + def _spawn(cmd: tuple[str, ...], *, timeout: int | None = None) -> PopenSpawn: # Disable subprocess timeout if debugging (except coverage), for commodity # See https://stackoverflow.com/a/67065084/1468388 tracer = getattr(sys, "gettrace", lambda: None)() diff --git a/tests/helpers.py b/tests/helpers.py index 3a579f956..848a46bcc 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import filecmp import json import os @@ -6,7 +8,7 @@ from enum import Enum from hashlib import sha1 from pathlib import Path -from typing import Mapping, Optional, Protocol, Tuple, Union +from typing import Mapping, Protocol from pexpect.popen_spawn import PopenSpawn from plumbum import local @@ -59,7 +61,7 @@ class Spawn(Protocol): - def __call__(self, cmd: Tuple[str, ...], *, timeout: Optional[int]) -> PopenSpawn: + def __call__(self, cmd: tuple[str, ...], *, timeout: int | None) -> PopenSpawn: ... @@ -94,9 +96,7 @@ def assert_file(tmp_path: Path, *path: str) -> None: assert filecmp.cmp(p1, p2) -def build_file_tree( - spec: Mapping[StrOrPath, Union[str, bytes, Path]], dedent: bool = True -): +def build_file_tree(spec: Mapping[StrOrPath, str | bytes | Path], dedent: bool = True): """Builds a file tree based on the received spec. Params: @@ -134,7 +134,7 @@ def expect_prompt( def git_save( - dst: StrOrPath = ".", message: str = "Test commit", tag: Optional[str] = None + dst: StrOrPath = ".", message: str = "Test commit", tag: str | None = None ): """Save the current repo state in git. diff --git a/tests/test_answersfile_templating.py b/tests/test_answersfile_templating.py index d06d17a51..dbf68bdfa 100644 --- a/tests/test_answersfile_templating.py +++ b/tests/test_answersfile_templating.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from pathlib import Path -from typing import Optional import pytest @@ -14,13 +15,11 @@ def template_path(tmp_path_factory: pytest.TempPathFactory) -> str: root = tmp_path_factory.mktemp("template") build_file_tree( { - root - / "{{ _copier_conf.answers_file }}.jinja": """\ + root / "{{ _copier_conf.answers_file }}.jinja": """\ # Changes here will be overwritten by Copier {{ _copier_answers|to_nice_yaml }} """, - root - / "copier.yml": """\ + root / "copier.yml": """\ _answers_file: ".copier-answers-{{ module_name }}.yml" module_name: @@ -33,7 +32,7 @@ def template_path(tmp_path_factory: pytest.TempPathFactory) -> str: @pytest.mark.parametrize("answers_file", [None, ".changed-by-user.yml"]) def test_answersfile_templating( - template_path: str, tmp_path: Path, answers_file: Optional[str] + template_path: str, tmp_path: Path, answers_file: str | None ) -> None: """ Test copier behaves properly when _answers_file contains a template diff --git a/tests/test_cli.py b/tests/test_cli.py index 86163e46a..fd56a58b1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -38,9 +38,7 @@ def _template_path_with_dot_config(config_folder: str) -> str: root = tmp_path_factory.mktemp("template") build_file_tree( { - root - / config_folder - / "{{ _copier_conf.answers_file }}.jinja": """\ + root / config_folder / "{{ _copier_conf.answers_file }}.jinja": """\ # Changes here will be overwritten by Copier {{ _copier_answers|to_nice_yaml }} """, diff --git a/tests/test_config.py b/tests/test_config.py index 64848020c..1d282b91f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from pathlib import Path from textwrap import dedent -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable import pytest from plumbum import local @@ -120,7 +122,7 @@ def test_invalid_yaml(capsys: pytest.CaptureFixture[str]) -> None: def test_invalid_config_data( capsys: pytest.CaptureFixture[str], conf_path: str, - check_err: Optional[Callable[[str], bool]], + check_err: Callable[[str], bool] | None, ) -> None: template = Template(conf_path) with pytest.raises(InvalidConfigFileError): @@ -231,7 +233,7 @@ def test_missing_template(tmp_path: Path) -> None: copier.run_copy("./i_do_not_exist", tmp_path) -def is_subdict(small: Dict[Any, Any], big: Dict[Any, Any]) -> bool: +def is_subdict(small: dict[Any, Any], big: dict[Any, Any]) -> bool: return {**big, **small} == big @@ -263,7 +265,7 @@ def test_worker_good_data(tmp_path: Path) -> None: ], ) def test_worker_config_precedence( - tmp_path: Path, test_input: AnyByStrDict, expected_exclusions: Tuple[str, ...] + tmp_path: Path, test_input: AnyByStrDict, expected_exclusions: tuple[str, ...] ) -> None: conf = copier.Worker(dst_path=tmp_path, vcs_ref="HEAD", **test_input) assert expected_exclusions == conf.all_exclusions diff --git a/tests/test_copy.py b/tests/test_copy.py index 0a78ee63d..8fe3300e7 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import platform import stat import sys @@ -5,7 +7,7 @@ from decimal import Decimal from enum import Enum from pathlib import Path -from typing import Any, ContextManager, List +from typing import Any, ContextManager import pytest import yaml @@ -800,7 +802,7 @@ def test_required_question_without_data( ], ) def test_required_choice_question_without_data( - tmp_path_factory: pytest.TempPathFactory, type_name: str, choices: List[Any] + tmp_path_factory: pytest.TempPathFactory, type_name: str, choices: list[Any] ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( diff --git a/tests/test_prompt.py b/tests/test_prompt.py index bcc6ba28f..e3c7ffcb3 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from pathlib import Path -from typing import Any, Dict, List, Mapping, Protocol, Tuple, Union +from typing import Any, Dict, List, Mapping, Protocol, Union import pexpect import pytest @@ -28,7 +30,7 @@ from typing_extensions import TypeAlias -MARIO_TREE: Mapping[StrOrPath, Union[str, bytes]] = { +MARIO_TREE: Mapping[StrOrPath, str | bytes] = { "copier.yml": ( f"""\ _templates_suffix: {SUFFIX_TMPL} @@ -53,7 +55,7 @@ "[[ _copier_conf.answers_file ]].tmpl": "[[_copier_answers|to_nice_yaml]]", } -MARIO_TREE_WITH_NEW_FIELD: Mapping[StrOrPath, Union[str, bytes]] = { +MARIO_TREE_WITH_NEW_FIELD: Mapping[StrOrPath, str | bytes] = { "copier.yml": ( f"""\ _templates_suffix: {SUFFIX_TMPL} @@ -95,7 +97,7 @@ def test_copy_default_advertised( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, name: str, - args: Tuple[str, ...], + args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) @@ -163,7 +165,7 @@ def test_update_skip_answered( spawn: Spawn, name: str, update_action: str, - args: Tuple[str, ...], + args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) @@ -226,7 +228,7 @@ def test_update_with_new_field_in_new_version_skip_answered( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, name: str, - args: Tuple[str, ...], + args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) @@ -328,8 +330,8 @@ def test_update_with_new_field_in_new_version_skip_answered( def test_when( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, - question_1: Union[str, int, float, bool], - question_2_when: Union[bool, str], + question_1: str | int | float | bool, + question_2_when: bool | str, asks: bool, ) -> None: """Test that the 2nd question is skipped or not, properly.""" @@ -483,11 +485,7 @@ def test_multiline( def test_update_choice( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, - choices: Union[ - List[int], - List[List[Union[str, int]]], # actually `List[Tuple[str, int]]` - Dict[str, int], - ], + choices: list[int] | list[list[str | int]] | dict[str, int], ) -> None: """Choices are properly remembered and selected in TUI when updating.""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) @@ -685,7 +683,7 @@ def test_required_text_question( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, type_name: str, - expected_answer: Union[str, None, ValueError], + expected_answer: str | None | ValueError, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( @@ -762,7 +760,7 @@ def test_required_choice_question( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, type_name: str, - choices: List[Any], + choices: list[Any], expected_answer: Any, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) @@ -798,7 +796,7 @@ def test_required_choice_question( QuestionChoices: TypeAlias = Union[List[Any], Dict[str, Any]] ParsedValues: TypeAlias = List[Any] -_CHOICES: Dict[str, Tuple[QuestionType, QuestionChoices, ParsedValues]] = { +_CHOICES: dict[str, tuple[QuestionType, QuestionChoices, ParsedValues]] = { "str": ("str", ["one", "two", "three"], ["one", "two", "three"]), "int": ("int", [1, 2, 3], [1, 2, 3]), "int-label-list": ("int", [["one", 1], ["two", 2], ["three", 3]], [1, 2, 3]), @@ -811,13 +809,13 @@ def test_required_choice_question( class QuestionTreeFixture(Protocol): - def __call__(self, **kwargs) -> Tuple[Path, Path]: + def __call__(self, **kwargs) -> tuple[Path, Path]: ... @pytest.fixture def question_tree(tmp_path_factory: pytest.TempPathFactory) -> QuestionTreeFixture: - def builder(**question) -> Tuple[Path, Path]: + def builder(**question) -> tuple[Path, Path]: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 25d6de782..afbc0e0ed 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from pathlib import Path -from typing import Literal, Optional +from typing import Literal import pytest @@ -79,7 +81,7 @@ def test_pretend_mode(tmp_path_factory: pytest.TempPathFactory) -> None: def test_os_specific_tasks( tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch, - os: Optional[Literal["linux", "macos", "windows"]], + os: Literal["linux", "macos", "windows"] | None, filename: str, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) diff --git a/tests/test_templated_prompt.py b/tests/test_templated_prompt.py index 335d0c52f..b6fb2c3ed 100644 --- a/tests/test_templated_prompt.py +++ b/tests/test_templated_prompt.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import json from datetime import datetime -from typing import Optional, Sequence, Type, Union +from typing import Sequence import pexpect import pytest @@ -149,8 +151,8 @@ def test_templated_prompt( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, questions_data: AnyByStrDict, - expected_value: Union[str, int], - expected_outputs: Sequence[Union[str, Prompt]], + expected_value: str | int, + expected_outputs: Sequence[str | Prompt], ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) questions_combined = {**main_question, **questions_data} @@ -254,7 +256,7 @@ def test_templated_prompt_builtins(tmp_path_factory: pytest.TempPathFactory) -> def test_templated_prompt_invalid( tmp_path_factory: pytest.TempPathFactory, questions: AnyByStrDict, - raises: Optional[Type[BaseException]], + raises: type[BaseException] | None, returns: str, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) diff --git a/tests/test_unsafe.py b/tests/test_unsafe.py index e774d4a89..e501edb17 100644 --- a/tests/test_unsafe.py +++ b/tests/test_unsafe.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from contextlib import nullcontext as does_not_raise -from typing import ContextManager, Union +from typing import ContextManager import pytest import yaml @@ -325,7 +327,7 @@ def test_update( def test_update_cli( tmp_path_factory: pytest.TempPathFactory, capsys: pytest.CaptureFixture[str], - unsafe: Union[bool, str], + unsafe: bool | str, ) -> None: src, dst = map(tmp_path_factory.mktemp, ["src", "dst"]) unsafe_args = [unsafe] if unsafe else [] diff --git a/tests/test_updatediff.py b/tests/test_updatediff.py index 6f4f3ee22..263ae9fb2 100644 --- a/tests/test_updatediff.py +++ b/tests/test_updatediff.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import platform from pathlib import Path from shutil import rmtree from textwrap import dedent -from typing import Literal, Optional +from typing import Literal import pexpect import pytest @@ -559,7 +561,7 @@ def test_skip_update(tmp_path_factory: pytest.TempPathFactory) -> None: "answers_file", [None, ".copier-answers.yml", ".custom.copier-answers.yaml"] ) def test_overwrite_answers_file_always( - tmp_path_factory: pytest.TempPathFactory, answers_file: Optional[str] + tmp_path_factory: pytest.TempPathFactory, answers_file: str | None ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) with local.cwd(src):