Skip to content

tools.py

Some utility functions.

Style

Common color styles.

Source code in copier/tools.py
25
26
27
28
29
30
31
32
class Style:
    """Common color styles."""

    OK = [colorama.Fore.GREEN, colorama.Style.BRIGHT]
    WARNING = [colorama.Fore.YELLOW, colorama.Style.BRIGHT]
    IGNORE = [colorama.Fore.CYAN]
    DANGER = [colorama.Fore.RED, colorama.Style.BRIGHT]
    RESET = [colorama.Fore.RESET, colorama.Style.RESET_ALL]

cast_to_bool(value)

Parse anything to bool.

Parameters:

Name Type Description Default
value Any

Anything to be casted to a bool. Tries to be as smart as possible.

  1. Cast to number. Then: 0 = False; anything else = True.
  2. Find YAML booleans, YAML nulls or none in it and use it appropriately.
  3. Cast to boolean using standard python bool(value).
required
Source code in copier/tools.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def cast_to_bool(value: Any) -> bool:
    """Parse anything to bool.

    Params:
        value:
            Anything to be casted to a bool. Tries to be as smart as possible.

            1.  Cast to number. Then: 0 = False; anything else = True.
            1.  Find [YAML booleans](https://yaml.org/type/bool.html),
                [YAML nulls](https://yaml.org/type/null.html) or `none` in it
                and use it appropriately.
            1.  Cast to boolean using standard python `bool(value)`.
    """
    # Assume it's a number
    with suppress(TypeError, ValueError):
        return bool(float(value))
    # Assume it's a string
    with suppress(AttributeError):
        lower = value.lower()
        if lower in {"y", "yes", "t", "true", "on"}:
            return True
        elif lower in {"n", "no", "f", "false", "off", "~", "null", "none"}:
            return False
    # Assume nothing
    return bool(value)

cast_to_str(value)

Parse anything to str.

Parameters:

Name Type Description Default
value Any

Anything to be casted to a str.

required
Source code in copier/tools.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def cast_to_str(value: Any) -> str:
    """Parse anything to str.

    Params:
        value:
            Anything to be casted to a str.
    """
    if isinstance(value, str):
        return value.value if isinstance(value, Enum) else value
    if isinstance(value, (float, int, Decimal)):
        return str(value)
    if isinstance(value, (bytes, bytearray)):
        return value.decode()
    raise ValueError(f"Could not convert {value} to string")

copier_version()

Get closest match for the installed copier version.

Source code in copier/tools.py
48
49
50
51
52
53
54
55
56
57
58
59
def copier_version() -> Version:
    """Get closest match for the installed copier version."""
    # Importing __version__ at the top of the module creates a circular import
    # ("cannot import name '__version__' from partially initialized module 'copier'"),
    # so instead we do a lazy import here
    from . import __version__

    if __version__ != "0.0.0":
        return Version(__version__)

    # Get the installed package version otherwise, which is sometimes more specific
    return Version(version("copier"))

force_str_end(original_str, end='\n')

Make sure a original_str ends with end.

Parameters:

Name Type Description Default
original_str str

String that you want to ensure ending.

required
end str

String that must exist at the end of original_str

'\n'
Source code in copier/tools.py
138
139
140
141
142
143
144
145
146
147
def force_str_end(original_str: str, end: str = "\n") -> str:
    """Make sure a `original_str` ends with `end`.

    Params:
        original_str: String that you want to ensure ending.
        end: String that must exist at the end of `original_str`
    """
    if not original_str.endswith(end):
        return original_str + end
    return original_str

handle_remove_readonly(func, path, exc)

Handle errors when trying to remove read-only files through shutil.rmtree.

On Windows, shutil.rmtree does not handle read-only files very well. This handler makes sure the given file is writable, then re-execute the given removal function.

Parameters:

Name Type Description Default
func Callable[[str], None]

An OS-dependant function used to remove a file.

required
path str

The path to the file to remove.

required
exc BaseException | tuple[type[BaseException], BaseException, TracebackType]

An exception (Python >= 3.12) or sys.exc_info() object.

required
Source code in copier/tools.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def handle_remove_readonly(
    func: Callable[[str], None],
    path: str,
    # TODO: Change this union to simply `BaseException` when Python 3.11 support is dropped
    exc: BaseException | tuple[type[BaseException], BaseException, TracebackType],
) -> None:
    """Handle errors when trying to remove read-only files through `shutil.rmtree`.

    On Windows, `shutil.rmtree` does not handle read-only files very well. This handler
    makes sure the given file is writable, then re-execute the given removal function.

    Arguments:
        func: An OS-dependant function used to remove a file.
        path: The path to the file to remove.
        exc: An exception (Python >= 3.12) or `sys.exc_info()` object.
    """
    # TODO: Change to `excvalue = exc` when Python 3.11 support is dropped
    excvalue = cast(OSError, exc if isinstance(exc, BaseException) else exc[1])

    if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES:
        os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)  # 0777
        func(path)
    else:
        raise

normalize_git_path(path)

Convert weird characters returned by Git to normal UTF-8 path strings.

A filename like âñ will be reported by Git as "\303\242\303\261" (octal notation). This can be disabled with git config core.quotepath off.

Parameters:

Name Type Description Default
path str

The Git path to normalize.

required

Returns:

Name Type Description
str str

The normalized Git path.

Source code in copier/tools.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def normalize_git_path(path: str) -> str:
    r"""Convert weird characters returned by Git to normal UTF-8 path strings.

    A filename like âñ will be reported by Git as "\\303\\242\\303\\261" (octal notation).
    This can be disabled with `git config core.quotepath off`.

    Args:
        path: The Git path to normalize.

    Returns:
        str: The normalized Git path.
    """
    # Remove surrounding quotes
    if path[0] == path[-1] == '"':
        path = path[1:-1]
    # Repair double-quotes
    path = path.replace('\\"', '"')
    # Convert octal to utf8
    return _re_octal.sub(_re_octal_replace, path)

printf(action, msg='', style=None, indent=10, quiet=False, file_=sys.stdout)

Print string with common format.

Source code in copier/tools.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def printf(
    action: str,
    msg: Any = "",
    style: list[str] | None = None,
    indent: int = 10,
    quiet: bool | StrictBool = False,
    file_: TextIO = sys.stdout,
) -> str | None:
    """Print string with common format."""
    if quiet:
        return None
    _msg = str(msg)
    action = action.rjust(indent, " ")
    if not style:
        return action + _msg

    out = style + [action] + Style.RESET + [INDENT, _msg]
    print(*out, sep="", file=file_)
    return None

printf_exception(e, action, msg='', indent=0, quiet=False)

Print exception with common format.

Source code in copier/tools.py
83
84
85
86
87
88
89
90
91
92
def printf_exception(
    e: Exception, action: str, msg: str = "", indent: int = 0, quiet: bool = False
) -> None:
    """Print exception with common format."""
    if not quiet:
        print("", file=sys.stderr)
        printf(action, msg=msg, style=Style.DANGER, indent=indent, file_=sys.stderr)
        print(HLINE, file=sys.stderr)
        print(e, file=sys.stderr)
        print(HLINE, file=sys.stderr)

A custom version of os.readlink/pathlib.Path.readlink.

pathlib.Path.readlink is what we ideally would want to use, but it is only available on python>=3.9.

Source code in copier/tools.py
176
177
178
179
180
181
182
183
184
def readlink(link: Path) -> Path:
    """A custom version of os.readlink/pathlib.Path.readlink.

    pathlib.Path.readlink is what we ideally would want to use, but it is only available on python>=3.9.
    """
    if sys.version_info >= (3, 9):
        return link.readlink()
    else:
        return Path(os.readlink(link))