Skip to content

Commit

Permalink
fix(mypy): 💚 add missing typing
Browse files Browse the repository at this point in the history
Includes workarounds for tmbo/questionary#191 and pydantic/pydantic#3175.
  • Loading branch information
yajo committed Jan 4, 2022
1 parent 0ef7020 commit a814e50
Show file tree
Hide file tree
Showing 12 changed files with 96 additions and 51 deletions.
3 changes: 2 additions & 1 deletion copier/cli.py
Expand Up @@ -47,6 +47,7 @@
import sys
from functools import wraps
from io import StringIO
from pathlib import Path
from textwrap import dedent
from unittest.mock import patch

Expand Down Expand Up @@ -205,7 +206,7 @@ def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Wor
"""
return Worker(
data=self.data,
dst_path=dst_path,
dst_path=Path(dst_path),
answers_file=self.answers_file,
exclude=self.exclude,
defaults=self.force or self.defaults,
Expand Down
3 changes: 2 additions & 1 deletion copier/errors.py
Expand Up @@ -9,7 +9,8 @@
from .types import PathSeq

if TYPE_CHECKING: # always false
from .user_data import AnswersMap, Question, Template
from .template import Template
from .user_data import AnswersMap, Question


# Errors
Expand Down
29 changes: 18 additions & 11 deletions copier/main.py
Expand Up @@ -9,7 +9,7 @@
from itertools import chain
from pathlib import Path
from shutil import rmtree
from typing import Callable, List, Mapping, Optional, Sequence
from typing import Callable, Iterable, List, Mapping, Optional, Sequence
from unicodedata import normalize

import pathspec
Expand Down Expand Up @@ -37,9 +37,10 @@
)
from .user_data import DEFAULT_DATA, AnswersMap, Question

try:
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075
if sys.version_info >= (3, 8):
from functools import cached_property
except ImportError:
else:
from backports.cached_property import cached_property


Expand Down Expand Up @@ -130,7 +131,7 @@ class Worker:
"""

src_path: Optional[str] = None
dst_path: Path = field(default=".")
dst_path: Path = field(default=Path("."))
answers_file: Optional[RelativePath] = None
vcs_ref: OptStr = None
data: AnyByStrDict = field(default_factory=dict)
Expand All @@ -155,7 +156,7 @@ def _answers_to_remember(self) -> Mapping:
answers[key] = value
# Other data goes next
answers.update(
(k, v)
(str(k), v)
for (k, v) in self.answers.combined.items()
if not k.startswith("_")
and k not in self.template.secret_questions
Expand Down Expand Up @@ -208,7 +209,7 @@ def _render_context(self) -> Mapping:
_folder_name=self.subproject.local_abspath.name,
)

def _path_matcher(self, patterns: StrSeq) -> Callable[[Path], bool]:
def _path_matcher(self, patterns: Iterable[str]) -> Callable[[Path], bool]:
"""Produce a function that matches against specified patterns."""
# TODO Is normalization really needed?
normalized_patterns = (normalize("NFD", pattern) for pattern in patterns)
Expand Down Expand Up @@ -345,7 +346,7 @@ def answers(self) -> AnswersMap:
question.get_default()
if self.defaults
else unsafe_prompt(
question.get_questionary_structure(), answers=result.combined
[question.get_questionary_structure()], answers=result.combined
)[question.var_name]
)
except KeyboardInterrupt as err:
Expand Down Expand Up @@ -538,7 +539,7 @@ def subproject(self) -> Subproject:
"""Get related subproject."""
return Subproject(
local_abspath=self.dst_path.absolute(),
answers_relpath=self.answers_file or ".copier-answers.yml",
answers_relpath=self.answers_file or Path(".copier-answers.yml"),
)

@cached_property
Expand All @@ -548,7 +549,7 @@ def template(self) -> Template:
if not url:
if self.subproject.template is None:
raise TypeError("Template not found")
url = self.subproject.template.url
url = str(self.subproject.template.url)
return Template(url=url, ref=self.vcs_ref, use_prereleases=self.use_prereleases)

@cached_property
Expand Down Expand Up @@ -631,6 +632,12 @@ def run_update(self) -> None:
raise UserMessageError(
"Updating is only supported in git-tracked templates."
)
if not self.subproject.template.version:
raise UserMessageError(
"Cannot update: version from last update not detected."
)
if not self.template.version:
raise UserMessageError("Cannot update: version from template not detected.")
if self.subproject.template.version > self.template.version:
raise UserMessageError(
f"Your are downgrading from {self.subproject.template.version} to {self.template.version}. "
Expand Down Expand Up @@ -716,7 +723,7 @@ def run_copy(
"""
if data is not None:
kwargs["data"] = data
worker = Worker(src_path=src_path, dst_path=dst_path, **kwargs)
worker = Worker(src_path=src_path, dst_path=Path(dst_path), **kwargs)
worker.run_copy()
return worker

Expand All @@ -734,7 +741,7 @@ def run_update(
"""
if data is not None:
kwargs["data"] = data
worker = Worker(dst_path=dst_path, **kwargs)
worker = Worker(dst_path=Path(dst_path), **kwargs)
worker.run_update()
return worker

Expand Down
6 changes: 4 additions & 2 deletions copier/subproject.py
Expand Up @@ -3,6 +3,7 @@
A *subproject* is a project that gets rendered and/or updated with Copier.
"""

import sys
from pathlib import Path
from typing import Optional

Expand All @@ -15,9 +16,10 @@
from .types import AbsolutePath, AnyByStrDict, VCSTypes
from .vcs import is_in_git_repo

try:
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075
if sys.version_info >= (3, 8):
from functools import cached_property
except ImportError:
else:
from backports.cached_property import cached_property


Expand Down
14 changes: 6 additions & 8 deletions copier/template.py
@@ -1,5 +1,6 @@
"""Tools related to template management."""
import re
import sys
from collections import ChainMap
from contextlib import suppress
from pathlib import Path
Expand Down Expand Up @@ -27,16 +28,13 @@
from .types import AnyByStrDict, OptStr, StrSeq, VCSTypes
from .vcs import checkout_latest_tag, clone, get_repo

try:
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075
if sys.version_info >= (3, 8):
from functools import cached_property
except ImportError:
else:
from backports.cached_property import cached_property

try:
from typing import Literal
except ImportError:
from typing_extensions import Literal

from .types import Literal

# Default list of files in the template to exclude from the rendered project
DEFAULT_EXCLUDE: Tuple[str, ...] = (
Expand Down Expand Up @@ -102,7 +100,7 @@ def load_template_config(conf_path: Path, quiet: bool = False) -> AnyByStrDict:
depth=2,
types=(list,),
)
return ChainMap(*reversed(list(flattened_result)))
return dict(ChainMap(*reversed(list(flattened_result))))
except yaml.parser.ParserError as e:
raise InvalidConfigFileError(conf_path, quiet) from e

Expand Down
9 changes: 6 additions & 3 deletions copier/tools.py
Expand Up @@ -9,13 +9,14 @@
import warnings
from contextlib import suppress
from pathlib import Path
from typing import Any, Callable, Optional, TextIO, Union
from types import TracebackType
from typing import Any, Callable, Optional, TextIO, Tuple, Union

import colorama
from packaging.version import Version
from pydantic import StrictBool

from .types import ExcInfo, IntSeq
from .types import IntSeq

try:
from importlib.metadata import version
Expand Down Expand Up @@ -128,7 +129,9 @@ def force_str_end(original_str: str, end: str = "\n") -> str:
return original_str


def handle_remove_readonly(func: Callable, path: str, exc: ExcInfo) -> None:
def handle_remove_readonly(
func: Callable, path: str, exc: Tuple[BaseException, OSError, TracebackType]
) -> None:
"""Handle errors when trying to remove read-only files through `shutil.rmtree`.
This handler makes sure the given file is writable, then re-execute the given removal function.
Expand Down
36 changes: 20 additions & 16 deletions copier/types.py
@@ -1,7 +1,7 @@
"""Complex types, annotations, validators."""

import sys
from pathlib import Path
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -18,9 +18,10 @@

from pydantic.validators import path_validator

try:
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075
if sys.version_info >= (3, 8):
from typing import Literal
except ImportError:
else:
from typing_extensions import Literal

if TYPE_CHECKING:
Expand Down Expand Up @@ -54,7 +55,6 @@
Filters = Dict[str, Callable]
LoaderPaths = Union[str, Iterable[str]]
VCSTypes = Literal["git"]
ExcInfo = Tuple[BaseException, Exception, TracebackType]


class AllowArbitraryTypes:
Expand All @@ -79,15 +79,19 @@ def path_is_relative(value: Path) -> Path:


# Validated types
class AbsolutePath(Path):
@classmethod
def __get_validators__(cls) -> "CallableGenerator":
yield path_validator
yield path_is_absolute


class RelativePath(Path):
@classmethod
def __get_validators__(cls) -> "CallableGenerator":
yield path_validator
yield path_is_relative
if TYPE_CHECKING:
AbsolutePath = Path
RelativePath = Path
else:

class AbsolutePath(Path):
@classmethod
def __get_validators__(cls) -> "CallableGenerator":
yield path_validator
yield path_is_absolute

class RelativePath(Path):
@classmethod
def __get_validators__(cls) -> "CallableGenerator":
yield path_validator
yield path_is_relative
8 changes: 5 additions & 3 deletions copier/user_data.py
@@ -1,6 +1,7 @@
"""Functions used to load user data."""
import datetime
import json
import sys
import warnings
from collections import ChainMap
from dataclasses import field
Expand Down Expand Up @@ -30,9 +31,10 @@
from .tools import cast_str_to_bool, force_str_end
from .types import AllowArbitraryTypes, AnyByStrDict, OptStr, OptStrOrPath, StrOrPath

try:
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075
if sys.version_info >= (3, 8):
from functools import cached_property
except ImportError:
else:
from backports.cached_property import cached_property

if TYPE_CHECKING:
Expand Down Expand Up @@ -249,7 +251,7 @@ def get_default_rendered(self) -> Union[bool, str, Choice, None]:
return json.dumps(default, indent=2 if self.get_multiline() else None)
if self.get_type_name() == "yaml":
return yaml.safe_dump(
default, default_flow_style=not self.get_multiline(), width=float("inf")
default, default_flow_style=not self.get_multiline(), width=2147483647
).strip()
# All other data has to be str
return str(default)
Expand Down
3 changes: 0 additions & 3 deletions mypy.ini

This file was deleted.

26 changes: 25 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion pyproject.toml
Expand Up @@ -40,7 +40,7 @@ mkdocstrings = { version = ">=0.16.2,<0.18.0", optional = true }
packaging = "^21.0" # packaging is needed when installing from PyPI
pathspec = "^0.9.0"
plumbum = "^1.6.9"
pydantic = "^1.7.2"
pydantic = "^1.9.0"
Pygments = "^2.7.1"
PyYAML = "^5.3.1"
pyyaml-include = "^1.2"
Expand Down Expand Up @@ -68,6 +68,8 @@ pre-commit = "^2.16.0"
pytest = "^6.1.1"
pytest-cov = "^3.0.0"
pytest-xdist = "^2.5.0"
types-PyYAML = "^6.0.1"
types-backports = "^0.1.3"

[tool.poe.tasks.clean]
script = "devtasks:clean"
Expand Down Expand Up @@ -116,6 +118,11 @@ line_length = 88
multi_line_output = 3 # black interop
use_parentheses = true

[tool.mypy]
ignore_missing_imports = true
plugins = ["pydantic.mypy"]
warn_no_return = false

[build-system]
requires = ["poetry_core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry.core.masonry.api"

0 comments on commit a814e50

Please sign in to comment.