Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4e626bd

Browse files
authoredMar 31, 2024
Merge pull request #1886 from EliahKagan/deprecation-warnings
Issue and test deprecation warnings
2 parents c7675d2 + f6060df commit 4e626bd

21 files changed

+1257
-63
lines changed
 

‎.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888

8989
- name: Check types with mypy
9090
run: |
91-
mypy --python-version=${{ matrix.python-version }} -p git
91+
mypy --python-version=${{ matrix.python-version }}
9292
env:
9393
MYPY_FORCE_COLOR: "1"
9494
TERM: "xterm-256color" # For color: https://github.com/python/mypy/issues/13817

‎README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ This includes the linting and autoformatting done by Ruff, as well as some other
167167
To typecheck, run:
168168

169169
```sh
170-
mypy -p git
170+
mypy
171171
```
172172

173173
#### CI (and tox)

‎git/__init__.py

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@
8888

8989
__version__ = "git"
9090

91-
from typing import List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
91+
from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92+
93+
if TYPE_CHECKING:
94+
from types import ModuleType
95+
96+
import warnings
9297

9398
from gitdb.util import to_hex_sha
9499

@@ -144,11 +149,6 @@
144149
SymbolicReference,
145150
Tag,
146151
TagReference,
147-
head, # noqa: F401 # Nonpublic. May disappear! Use git.refs.head.
148-
log, # noqa: F401 # Nonpublic. May disappear! Use git.refs.log.
149-
reference, # noqa: F401 # Nonpublic. May disappear! Use git.refs.reference.
150-
symbolic, # noqa: F401 # Nonpublic. May disappear! Use git.refs.symbolic.
151-
tag, # noqa: F401 # Nonpublic. May disappear! Use git.refs.tag.
152152
)
153153
from git.diff import ( # @NoMove
154154
INDEX,
@@ -169,21 +169,9 @@
169169
IndexEntry,
170170
IndexFile,
171171
StageType,
172-
base, # noqa: F401 # Nonpublic. May disappear! Use git.index.base.
173-
fun, # noqa: F401 # Nonpublic. May disappear! Use git.index.fun.
174-
typ, # noqa: F401 # Nonpublic. May disappear! Use git.index.typ.
175-
#
176-
# NOTE: The expression `git.util` evaluates to git.index.util, and the import
177-
# `from git import util` imports git.index.util, NOT git.util. It may not be
178-
# feasible to change this until the next major version, to avoid breaking code
179-
# inadvertently relying on it. If git.index.util really is what you want, use or
180-
# import from that name, to avoid confusion. To use the "real" git.util module,
181-
# write `from git.util import ...`, or access it as `sys.modules["git.util"]`.
182-
# (This differs from other historical indirect-submodule imports that are
183-
# unambiguously nonpublic and are subject to immediate removal. Here, the public
184-
# git.util module, even though different, makes it less discoverable that the
185-
# expression `git.util` refers to a non-public attribute of the git module.)
186-
util, # noqa: F401
172+
# NOTE: This tells type checkers what util resolves to. We delete it, and it is
173+
# really resolved by __getattr__, which warns. See below on what to use instead.
174+
util,
187175
)
188176
from git.util import ( # @NoMove
189177
Actor,
@@ -196,7 +184,79 @@
196184
except GitError as _exc:
197185
raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
198186

187+
188+
def _warned_import(message: str, fullname: str) -> "ModuleType":
189+
import importlib
190+
191+
warnings.warn(message, DeprecationWarning, stacklevel=3)
192+
return importlib.import_module(fullname)
193+
194+
195+
def _getattr(name: str) -> Any:
196+
# TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
197+
198+
if name == "util":
199+
return _warned_import(
200+
"The expression `git.util` and the import `from git import util` actually "
201+
"reference git.index.util, and not the git.util module accessed in "
202+
'`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
203+
"confusing behavior is currently preserved for compatibility, but may be "
204+
"changed in the future and should not be relied on.",
205+
fullname="git.index.util",
206+
)
207+
208+
for names, prefix in (
209+
({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
210+
({"base", "fun", "typ"}, "git.index"),
211+
):
212+
if name not in names:
213+
continue
214+
215+
fullname = f"{prefix}.{name}"
216+
217+
return _warned_import(
218+
f"{__name__}.{name} is a private alias of {fullname} and subject to "
219+
f"immediate removal. Use {fullname} instead.",
220+
fullname=fullname,
221+
)
222+
223+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
224+
225+
226+
if not TYPE_CHECKING:
227+
# NOTE: The expression `git.util` gives git.index.util and `from git import util`
228+
# imports git.index.util, NOT git.util. It may not be feasible to change this until
229+
# the next major version, to avoid breaking code inadvertently relying on it.
230+
#
231+
# - If git.index.util *is* what you want, use (or import from) that, to avoid
232+
# confusion.
233+
#
234+
# - To use the "real" git.util module, write `from git.util import ...`, or if
235+
# necessary access it as `sys.modules["git.util"]`.
236+
#
237+
# Note also that `import git.util` technically imports the "real" git.util... but
238+
# the *expression* `git.util` after doing so is still git.index.util!
239+
#
240+
# (This situation differs from that of other indirect-submodule imports that are
241+
# unambiguously non-public and subject to immediate removal. Here, the public
242+
# git.util module, though different, makes less discoverable that the expression
243+
# `git.util` refers to a non-public attribute of the git module.)
244+
#
245+
# This had originally come about by a wildcard import. Now that all intended imports
246+
# are explicit, the intuitive but potentially incompatible binding occurs due to the
247+
# usual rules for Python submodule bindings. So for now we replace that binding with
248+
# git.index.util, delete that, and let __getattr__ handle it and issue a warning.
249+
#
250+
# For the same runtime behavior, it would be enough to forgo importing util, and
251+
# delete util as created naturally; __getattr__ would behave the same. But type
252+
# checkers would not know what util refers to when accessed as an attribute of git.
253+
del util
254+
255+
# This is "hidden" to preserve static checking for undefined/misspelled attributes.
256+
__getattr__ = _getattr
257+
199258
# { Initialize git executable path
259+
200260
GIT_OK = None
201261

202262

@@ -232,12 +292,9 @@ def refresh(path: Optional[PathLike] = None) -> None:
232292
GIT_OK = True
233293

234294

235-
# } END initialize git executable path
236-
237-
238-
#################
239295
try:
240296
refresh()
241297
except Exception as _exc:
242298
raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc
243-
#################
299+
300+
# } END initialize git executable path

‎git/cmd.py

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
__all__ = ["Git"]
8+
__all__ = ["GitMeta", "Git"]
99

1010
import contextlib
1111
import io
@@ -19,6 +19,7 @@
1919
import sys
2020
from textwrap import dedent
2121
import threading
22+
import warnings
2223

2324
from git.compat import defenc, force_bytes, safe_decode
2425
from git.exc import (
@@ -307,8 +308,79 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
307308

308309
## -- End Utilities -- @}
309310

311+
_USE_SHELL_DEFAULT_MESSAGE = (
312+
"Git.USE_SHELL is deprecated, because only its default value of False is safe. "
313+
"It will be removed in a future release."
314+
)
315+
316+
_USE_SHELL_DANGER_MESSAGE = (
317+
"Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
318+
"shell syntax cannot usually be accounted for. This can result in a command "
319+
"injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
320+
"and will be removed in a future release."
321+
)
322+
323+
324+
def _warn_use_shell(extra_danger: bool) -> None:
325+
warnings.warn(
326+
_USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE,
327+
DeprecationWarning,
328+
stacklevel=3,
329+
)
330+
331+
332+
class _GitMeta(type):
333+
"""Metaclass for :class:`Git`.
334+
335+
This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
336+
"""
337+
338+
def __getattribute(cls, name: str) -> Any:
339+
if name == "USE_SHELL":
340+
_warn_use_shell(False)
341+
return super().__getattribute__(name)
342+
343+
def __setattr(cls, name: str, value: Any) -> Any:
344+
if name == "USE_SHELL":
345+
_warn_use_shell(value)
346+
super().__setattr__(name, value)
347+
348+
if not TYPE_CHECKING:
349+
# To preserve static checking for undefined/misspelled attributes while letting
350+
# the methods' bodies be type-checked, these are defined as non-special methods,
351+
# then bound to special names out of view of static type checkers. (The original
352+
# names invoke name mangling (leading "__") to avoid confusion in other scopes.)
353+
__getattribute__ = __getattribute
354+
__setattr__ = __setattr
355+
356+
357+
GitMeta = _GitMeta
358+
"""Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
359+
360+
Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
361+
custom metaclass is not documented and may change at any time. This statically checkable
362+
metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
363+
used. Code that benefits from it is likely to be remain brittle even if it is used.
310364
311-
class Git:
365+
In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
366+
callable attributes representing git subcommands, it rarely makes sense to inherit from
367+
:class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
368+
to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
369+
as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
370+
might have to be introduced to solve a metaclass conflict, is not recommended.
371+
372+
:note:
373+
The correct static type of the :class:`Git` class itself, and any subclasses, is
374+
``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
375+
376+
:class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
377+
intended or otherwise possible to use. This alias is truly only for very rare and
378+
inherently precarious situations where it is necessary to deal with the metaclass
379+
explicitly.
380+
"""
381+
382+
383+
class Git(metaclass=_GitMeta):
312384
"""The Git class manages communication with the Git binary.
313385
314386
It provides a convenient interface to calling the Git binary, such as in::
@@ -358,24 +430,53 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
358430
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
359431
"""Enables debugging of GitPython's git commands."""
360432

361-
USE_SHELL = False
433+
USE_SHELL: bool = False
362434
"""Deprecated. If set to ``True``, a shell will be used when executing git commands.
363435
436+
Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
437+
functions should be updated to use the default value of ``False`` instead. ``True``
438+
is unsafe unless the effect of syntax treated specially by the shell is fully
439+
considered and accounted for, which is not possible under most circumstances. As
440+
detailed below, it is also no longer needed, even where it had been in the past.
441+
442+
It is in many if not most cases a command injection vulnerability for an application
443+
to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
444+
fragment of text to make its way into any part of any argument to any git command
445+
(including paths, branch names, etc.) can cause the shell to read and write
446+
arbitrary files and execute arbitrary commands. Innocent input may also accidentally
447+
contain special shell syntax, leading to inadvertent malfunctions.
448+
449+
In addition, how a value of ``True`` interacts with some aspects of GitPython's
450+
operation is not precisely specified and may change without warning, even before
451+
GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
452+
453+
* Whether or how GitPython automatically customizes the shell environment.
454+
455+
* Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
456+
separate arguments even when ``shell=True``), this can be used with any GitPython
457+
functionality other than direct calls to the :meth:`execute` method.
458+
459+
* Whether any GitPython feature that runs git commands ever attempts to partially
460+
sanitize data a shell may treat specially. Currently this is not done.
461+
364462
Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
365463
in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
366464
GitPython solves that problem more robustly and safely by using the
367465
``CREATE_NO_WINDOW`` process creation flag on Windows.
368466
369-
Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
370-
functions should be updated to use the default value of ``False`` instead. ``True``
371-
is unsafe unless the effect of shell expansions is fully considered and accounted
372-
for, which is not possible under most circumstances.
467+
Because Windows path search differs subtly based on whether a shell is used, in rare
468+
cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
469+
such as a batch file, from being found. To fix this, set the command name or full
470+
path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
471+
full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
373472
374-
See:
473+
Further reading:
375474
376-
- :meth:`Git.execute` (on the ``shell`` parameter).
377-
- https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
378-
- https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
475+
* :meth:`Git.execute` (on the ``shell`` parameter).
476+
* https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
477+
* https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
478+
* https://github.com/python/cpython/issues/91558#issuecomment-1100942950
479+
* https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
379480
"""
380481

381482
_git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
@@ -868,6 +969,11 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
868969
self.cat_file_header: Union[None, TBD] = None
869970
self.cat_file_all: Union[None, TBD] = None
870971

972+
def __getattribute__(self, name: str) -> Any:
973+
if name == "USE_SHELL":
974+
_warn_use_shell(False)
975+
return super().__getattribute__(name)
976+
871977
def __getattr__(self, name: str) -> Any:
872978
"""A convenience method as it allows to call the command as if it was an object.
873979
@@ -1138,7 +1244,12 @@ def execute(
11381244

11391245
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
11401246
if shell is None:
1141-
shell = self.USE_SHELL
1247+
# Get the value of USE_SHELL with no deprecation warning. Do this without
1248+
# warnings.catch_warnings, to avoid a race condition with application code
1249+
# configuring warnings. The value could be looked up in type(self).__dict__
1250+
# or Git.__dict__, but those can break under some circumstances. This works
1251+
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1252+
shell = super().__getattribute__("USE_SHELL")
11421253
_logger.debug(
11431254
"Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
11441255
redacted_command,

‎git/compat.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import locale
1414
import os
1515
import sys
16+
import warnings
1617

1718
from gitdb.utils.encoding import force_bytes, force_text # noqa: F401
1819

@@ -23,7 +24,9 @@
2324
AnyStr,
2425
Dict, # noqa: F401
2526
IO, # noqa: F401
27+
List,
2628
Optional,
29+
TYPE_CHECKING,
2730
Tuple, # noqa: F401
2831
Type, # noqa: F401
2932
Union,
@@ -33,7 +36,37 @@
3336
# ---------------------------------------------------------------------------
3437

3538

36-
is_win = os.name == "nt"
39+
_deprecated_platform_aliases = {
40+
"is_win": os.name == "nt",
41+
"is_posix": os.name == "posix",
42+
"is_darwin": sys.platform == "darwin",
43+
}
44+
45+
46+
def _getattr(name: str) -> Any:
47+
try:
48+
value = _deprecated_platform_aliases[name]
49+
except KeyError:
50+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
51+
52+
warnings.warn(
53+
f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
54+
"Write the desired os.name or sys.platform check explicitly instead.",
55+
DeprecationWarning,
56+
stacklevel=2,
57+
)
58+
return value
59+
60+
61+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
62+
__getattr__ = _getattr
63+
64+
65+
def __dir__() -> List[str]:
66+
return [*globals(), *_deprecated_platform_aliases]
67+
68+
69+
is_win: bool
3770
"""Deprecated alias for ``os.name == "nt"`` to check for native Windows.
3871
3972
This is deprecated because it is clearer to write out :attr:`os.name` or
@@ -45,7 +78,7 @@
4578
Cygwin, use ``sys.platform == "cygwin"``.
4679
"""
4780

48-
is_posix = os.name == "posix"
81+
is_posix: bool
4982
"""Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
5083
5184
This is deprecated because it clearer to write out :attr:`os.name` or
@@ -58,7 +91,7 @@
5891
(Darwin).
5992
"""
6093

61-
is_darwin = sys.platform == "darwin"
94+
is_darwin: bool
6295
"""Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
6396
6497
This is deprecated because it clearer to write out :attr:`os.name` or

‎git/diff.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import enum
99
import re
10+
import warnings
1011

1112
from git.cmd import handle_process_output
1213
from git.compat import defenc
@@ -554,6 +555,11 @@ def renamed(self) -> bool:
554555
This property is deprecated.
555556
Please use the :attr:`renamed_file` property instead.
556557
"""
558+
warnings.warn(
559+
"Diff.renamed is deprecated, use Diff.renamed_file instead",
560+
DeprecationWarning,
561+
stacklevel=2,
562+
)
557563
return self.renamed_file
558564

559565
@property

‎git/objects/commit.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from subprocess import Popen, PIPE
1515
import sys
1616
from time import altzone, daylight, localtime, time, timezone
17+
import warnings
1718

1819
from gitdb import IStream
1920

@@ -399,6 +400,11 @@ def trailers(self) -> Dict[str, str]:
399400
Dictionary containing whitespace stripped trailer information.
400401
Only contains the latest instance of each trailer key.
401402
"""
403+
warnings.warn(
404+
"Commit.trailers is deprecated, use Commit.trailers_list or Commit.trailers_dict instead",
405+
DeprecationWarning,
406+
stacklevel=2,
407+
)
402408
return {k: v[0] for k, v in self.trailers_dict.items()}
403409

404410
@property

‎git/types.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
Any,
88
Callable,
99
Dict,
10+
List,
1011
NoReturn,
1112
Optional,
1213
Sequence as Sequence,
1314
Tuple,
1415
TYPE_CHECKING,
16+
Type,
1517
TypeVar,
1618
Union,
1719
)
20+
import warnings
1821

1922
if sys.version_info >= (3, 8):
2023
from typing import (
@@ -127,21 +130,50 @@
127130
https://git-scm.com/docs/gitglossary#def_object_type
128131
"""
129132

130-
Lit_commit_ish = Literal["commit", "tag"]
131-
"""Deprecated. Type of literal strings identifying sometimes-commitish git object types.
133+
Lit_commit_ish: Type[Literal["commit", "tag"]]
134+
"""Deprecated. Type of literal strings identifying typically-commitish git object types.
132135
133136
Prior to a bugfix, this type had been defined more broadly. Any usage is in practice
134-
ambiguous and likely to be incorrect. Instead of this type:
137+
ambiguous and likely to be incorrect. This type has therefore been made a static type
138+
error to appear in annotations. It is preserved, with a deprecated status, to avoid
139+
introducing runtime errors in code that refers to it, but it should not be used.
140+
141+
Instead of this type:
135142
136143
* For the type of the string literals associated with :class:`Commit_ish`, use
137144
``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
138-
this type as currently defined.
145+
this type as currently defined (but usable in statically checked type annotations).
139146
140147
* For the type of all four string literals associated with :class:`AnyGitObject`, use
141148
:class:`GitObjectTypeString`. That is equivalent to the old definition of this type
142-
prior to the bugfix.
149+
prior to the bugfix (and is also usable in statically checked type annotations).
143150
"""
144151

152+
153+
def _getattr(name: str) -> Any:
154+
if name != "Lit_commit_ish":
155+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
156+
157+
warnings.warn(
158+
"Lit_commit_ish is deprecated. It is currently defined as "
159+
'`Literal["commit", "tag"]`, which should be used in its place if desired. It '
160+
'had previously been defined as `Literal["commit", "tag", "blob", "tree"]`, '
161+
"covering all four git object type strings including those that are never "
162+
"commit-ish. For that, use the GitObjectTypeString type instead.",
163+
DeprecationWarning,
164+
stacklevel=2,
165+
)
166+
return Literal["commit", "tag"]
167+
168+
169+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
170+
__getattr__ = _getattr
171+
172+
173+
def __dir__() -> List[str]:
174+
return [*globals(), "Lit_commit_ish"]
175+
176+
145177
# Config_levels ---------------------------------------------------------
146178

147179
Lit_config_levels = Literal["system", "global", "user", "repository"]
@@ -188,12 +220,12 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception,
188220
189221
:param inp:
190222
If all members are handled, the argument for `inp` will have the
191-
:class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will
192-
mismatch and cause a mypy error.
223+
:class:`~typing.Never`/:class:`~typing.NoReturn` type.
224+
Otherwise, the type will mismatch and cause a mypy error.
193225
194226
:param raise_error:
195-
If ``True``, will also raise :exc:`ValueError` with a general "unhandled
196-
literal" message, or the exception object passed as `exc`.
227+
If ``True``, will also raise :exc:`ValueError` with a general
228+
"unhandled literal" message, or the exception object passed as `exc`.
197229
198230
:param exc:
199231
It not ``None``, this should be an already-constructed exception object, to be

‎git/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1285,7 +1285,7 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I
12851285

12861286

12871287
class IterableClassWatcher(type):
1288-
"""Metaclass that issues :class:`DeprecationWarning` when :class:`git.util.Iterable`
1288+
"""Metaclass that issues :exc:`DeprecationWarning` when :class:`git.util.Iterable`
12891289
is subclassed."""
12901290

12911291
def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:

‎pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ testpaths = "test" # Space separated list of paths from root e.g test tests doc
2121

2222
[tool.mypy]
2323
python_version = "3.8"
24+
files = ["git/", "test/deprecation/"]
2425
disallow_untyped_defs = true
2526
no_implicit_optional = true
2627
warn_redundant_casts = true
27-
warn_unused_ignores = true
28+
warn_unused_ignores = true # Useful in general, but especially in test/deprecation.
2829
warn_unreachable = true
2930
implicit_reexport = true
3031
# strict = true

‎test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ pytest-cov
88
pytest-instafail
99
pytest-mock
1010
pytest-sugar
11+
typing-extensions ; python_version < "3.11"

‎test/deprecation/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Tests of deprecation warnings and possible related attribute bugs.
5+
6+
Most deprecation warnings are "basic" in the sense that there is no special complexity
7+
to consider, in introducing them. However, to issue deprecation warnings on mere
8+
attribute access can involve adding new dynamic behavior. This can lead to subtle bugs
9+
or less useful dynamic metadata. It can also weaken static typing, as happens if a type
10+
checker sees a method like ``__getattr__`` in a module or class whose attributes it did
11+
not already judge to be dynamic. This test.deprecation submodule covers all three cases:
12+
the basic cases, subtle dynamic behavior, and subtle static type checking issues.
13+
14+
Static type checking is "tested" by a combination of code that should not be treated as
15+
a type error but would be in the presence of particular bugs, and code that *should* be
16+
treated as a type error and is accordingly marked ``# type: ignore[REASON]`` (for
17+
specific ``REASON``. The latter will only produce mypy errors when the expectation is
18+
not met if it is configured with ``warn_unused_ignores = true``.
19+
"""

‎test/deprecation/lib.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Support library for deprecation tests."""
5+
6+
__all__ = ["assert_no_deprecation_warning", "suppress_deprecation_warning"]
7+
8+
import contextlib
9+
import warnings
10+
11+
from typing import Generator
12+
13+
14+
@contextlib.contextmanager
15+
def assert_no_deprecation_warning() -> Generator[None, None, None]:
16+
"""Context manager to assert that code does not issue any deprecation warnings."""
17+
with warnings.catch_warnings():
18+
warnings.simplefilter("error", DeprecationWarning)
19+
warnings.simplefilter("error", PendingDeprecationWarning)
20+
yield
21+
22+
23+
@contextlib.contextmanager
24+
def suppress_deprecation_warning() -> Generator[None, None, None]:
25+
with warnings.catch_warnings():
26+
warnings.simplefilter("ignore", DeprecationWarning)
27+
yield

‎test/deprecation/test_basic.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Tests of assorted deprecation warnings when there are no extra subtleties to check.
5+
6+
This tests deprecation warnings where all that needs be verified is that a deprecated
7+
property, function, or class issues a DeprecationWarning when used and, if applicable,
8+
that recommended alternatives do not issue the warning.
9+
10+
This is in contrast to other modules within test.deprecation, which test warnings where
11+
there is a risk of breaking other runtime behavior, or of breaking static type checking
12+
or making it less useful, by introducing the warning or in plausible future changes to
13+
how the warning is implemented. That happens when it is necessary to customize attribute
14+
access on a module or class, in a way it was not customized before, to issue a warning.
15+
It is inapplicable to the deprecations whose warnings are tested in this module.
16+
"""
17+
18+
import pytest
19+
20+
from git.diff import NULL_TREE
21+
from git.objects.util import Traversable
22+
from git.repo import Repo
23+
from git.util import Iterable as _Iterable, IterableObj
24+
25+
from .lib import assert_no_deprecation_warning
26+
27+
# typing -----------------------------------------------------------------
28+
29+
from typing import Generator, TYPE_CHECKING
30+
31+
if TYPE_CHECKING:
32+
from pathlib import Path
33+
34+
from git.diff import Diff
35+
from git.objects.commit import Commit
36+
37+
# ------------------------------------------------------------------------
38+
39+
40+
@pytest.fixture
41+
def commit(tmp_path: "Path") -> Generator["Commit", None, None]:
42+
"""Fixture to supply a one-commit repo's commit, enough for deprecation tests."""
43+
(tmp_path / "a.txt").write_text("hello\n", encoding="utf-8")
44+
repo = Repo.init(tmp_path)
45+
repo.index.add(["a.txt"])
46+
yield repo.index.commit("Initial commit")
47+
repo.close()
48+
49+
50+
@pytest.fixture
51+
def diff(commit: "Commit") -> Generator["Diff", None, None]:
52+
"""Fixture to supply a single-file diff."""
53+
(diff,) = commit.diff(NULL_TREE) # Exactly one file in the diff.
54+
yield diff
55+
56+
57+
def test_diff_renamed_warns(diff: "Diff") -> None:
58+
"""The deprecated Diff.renamed property issues a deprecation warning."""
59+
with pytest.deprecated_call():
60+
diff.renamed
61+
62+
63+
def test_diff_renamed_file_does_not_warn(diff: "Diff") -> None:
64+
"""The preferred Diff.renamed_file property issues no deprecation warning."""
65+
with assert_no_deprecation_warning():
66+
diff.renamed_file
67+
68+
69+
def test_commit_trailers_warns(commit: "Commit") -> None:
70+
"""The deprecated Commit.trailers property issues a deprecation warning."""
71+
with pytest.deprecated_call():
72+
commit.trailers
73+
74+
75+
def test_commit_trailers_list_does_not_warn(commit: "Commit") -> None:
76+
"""The nondeprecated Commit.trailers_list property issues no deprecation warning."""
77+
with assert_no_deprecation_warning():
78+
commit.trailers_list
79+
80+
81+
def test_commit_trailers_dict_does_not_warn(commit: "Commit") -> None:
82+
"""The nondeprecated Commit.trailers_dict property issues no deprecation warning."""
83+
with assert_no_deprecation_warning():
84+
commit.trailers_dict
85+
86+
87+
def test_traverse_list_traverse_in_base_class_warns(commit: "Commit") -> None:
88+
"""Traversable.list_traverse's base implementation issues a deprecation warning."""
89+
with pytest.deprecated_call():
90+
Traversable.list_traverse(commit)
91+
92+
93+
def test_traversable_list_traverse_override_does_not_warn(commit: "Commit") -> None:
94+
"""Calling list_traverse on concrete subclasses is not deprecated, does not warn."""
95+
with assert_no_deprecation_warning():
96+
commit.list_traverse()
97+
98+
99+
def test_traverse_traverse_in_base_class_warns(commit: "Commit") -> None:
100+
"""Traversable.traverse's base implementation issues a deprecation warning."""
101+
with pytest.deprecated_call():
102+
Traversable.traverse(commit)
103+
104+
105+
def test_traverse_traverse_override_does_not_warn(commit: "Commit") -> None:
106+
"""Calling traverse on concrete subclasses is not deprecated, does not warn."""
107+
with assert_no_deprecation_warning():
108+
commit.traverse()
109+
110+
111+
def test_iterable_inheriting_warns() -> None:
112+
"""Subclassing the deprecated git.util.Iterable issues a deprecation warning."""
113+
with pytest.deprecated_call():
114+
115+
class Derived(_Iterable):
116+
pass
117+
118+
119+
def test_iterable_obj_inheriting_does_not_warn() -> None:
120+
"""Subclassing git.util.IterableObj is not deprecated, does not warn."""
121+
with assert_no_deprecation_warning():
122+
123+
class Derived(IterableObj):
124+
pass

‎test/deprecation/test_cmd_git.py

Lines changed: 391 additions & 0 deletions
Large diffs are not rendered by default.

‎test/deprecation/test_compat.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Tests for dynamic and static characteristics of git.compat module attributes.
5+
6+
These tests verify that the is_<platform> attributes are available, and are even listed
7+
in the output of dir(), but issue warnings, and that bogus (misspelled or unrecognized)
8+
attribute access is still an error both at runtime and with mypy. This is similar to
9+
some of the tests in test_toplevel, but the situation being tested here is simpler
10+
because it does not involve unintuitive module aliasing or import behavior. So this only
11+
tests attribute access, not "from" imports (whose behavior can be intuitively inferred).
12+
"""
13+
14+
import os
15+
import sys
16+
17+
if sys.version_info >= (3, 11):
18+
from typing import assert_type
19+
else:
20+
from typing_extensions import assert_type
21+
22+
import pytest
23+
24+
import git.compat
25+
26+
_MESSAGE_LEADER = "{} and other is_<platform> aliases are deprecated."
27+
"""Form taken by the beginning of the warnings issued for is_<platform> access."""
28+
29+
30+
def test_cannot_access_undefined() -> None:
31+
"""Accessing a bogus attribute in git.compat remains a dynamic and static error."""
32+
with pytest.raises(AttributeError):
33+
git.compat.foo # type: ignore[attr-defined]
34+
35+
36+
def test_is_platform() -> None:
37+
"""The is_<platform> attributes work, warn, and mypy accepts code accessing them."""
38+
fully_qualified_names = [
39+
"git.compat.is_win",
40+
"git.compat.is_posix",
41+
"git.compat.is_darwin",
42+
]
43+
44+
with pytest.deprecated_call() as ctx:
45+
is_win = git.compat.is_win
46+
is_posix = git.compat.is_posix
47+
is_darwin = git.compat.is_darwin
48+
49+
assert_type(is_win, bool)
50+
assert_type(is_posix, bool)
51+
assert_type(is_darwin, bool)
52+
53+
messages = [str(entry.message) for entry in ctx]
54+
assert len(messages) == 3
55+
56+
for fullname, message in zip(fully_qualified_names, messages):
57+
assert message.startswith(_MESSAGE_LEADER.format(fullname))
58+
59+
# These assertions exactly reproduce the expressions in the code under test, so they
60+
# are not good for testing that the values are correct. Instead, their purpose is to
61+
# ensure that any dynamic machinery put in place in git.compat to cause warnings to
62+
# be issued does not get in the way of the intended values being accessed.
63+
assert is_win == (os.name == "nt")
64+
assert is_posix == (os.name == "posix")
65+
assert is_darwin == (sys.platform == "darwin")
66+
67+
68+
def test_dir() -> None:
69+
"""dir() on git.compat includes all public attributes, even if deprecated.
70+
71+
As dir() usually does, it also has nonpublic attributes, which should also not be
72+
removed by a custom __dir__ function, but those are less important to test.
73+
"""
74+
expected_subset = {
75+
"is_win",
76+
"is_posix",
77+
"is_darwin",
78+
"defenc",
79+
"safe_decode",
80+
"safe_encode",
81+
"win_encode",
82+
}
83+
actual = set(dir(git.compat))
84+
assert expected_subset <= actual

‎test/deprecation/test_toplevel.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Tests for dynamic and static characteristics of top-level git module attributes.
5+
6+
Provided mypy has ``warn_unused_ignores = true`` set, running mypy on these test cases
7+
checks static typing of the code under test. This is the reason for the many separate
8+
single-line attr-defined suppressions, so those should not be replaced with a smaller
9+
number of more broadly scoped suppressions, even where it is feasible to do so.
10+
11+
Running pytest checks dynamic behavior as usual.
12+
"""
13+
14+
import itertools
15+
import sys
16+
from typing import Type
17+
18+
if sys.version_info >= (3, 11):
19+
from typing import assert_type
20+
else:
21+
from typing_extensions import assert_type
22+
23+
import pytest
24+
25+
import git
26+
import git.index.base
27+
import git.index.fun
28+
import git.index.typ
29+
import git.refs.head
30+
import git.refs.log
31+
import git.refs.reference
32+
import git.refs.symbolic
33+
import git.refs.tag
34+
35+
36+
def test_cannot_access_undefined() -> None:
37+
"""Accessing a bogus attribute in git remains a dynamic and static error."""
38+
with pytest.raises(AttributeError):
39+
git.foo # type: ignore[attr-defined]
40+
41+
42+
def test_cannot_import_undefined() -> None:
43+
"""Importing a bogus attribute from git remains a dynamic and static error."""
44+
with pytest.raises(ImportError):
45+
from git import foo # type: ignore[attr-defined] # noqa: F401
46+
47+
48+
def test_util_alias_access() -> None:
49+
"""Accessing util in git works, warns, and mypy verifies it and its attributes."""
50+
# The attribute access should succeed.
51+
with pytest.deprecated_call() as ctx:
52+
util = git.util
53+
54+
# There should be exactly one warning and it should have our util-specific message.
55+
(message,) = [str(entry.message) for entry in ctx]
56+
assert "git.util" in message
57+
assert "git.index.util" in message
58+
assert "should not be relied on" in message
59+
60+
# We check access through the util alias to the TemporaryFileSwap member, since it
61+
# is slightly simpler to validate and reason about than the other public members,
62+
# which are functions (specifically, higher-order functions for use as decorators).
63+
from git.index.util import TemporaryFileSwap
64+
65+
assert_type(util.TemporaryFileSwap, Type[TemporaryFileSwap])
66+
67+
# This comes after the static assertion, just in case it would affect the inference.
68+
assert util.TemporaryFileSwap is TemporaryFileSwap
69+
70+
71+
def test_util_alias_import() -> None:
72+
"""Importing util from git works, warns, and mypy verifies it and its attributes."""
73+
# The import should succeed.
74+
with pytest.deprecated_call() as ctx:
75+
from git import util
76+
77+
# There may be multiple warnings. In CPython there will be currently always be
78+
# exactly two, possibly due to the equivalent of calling hasattr to do a pre-check
79+
# prior to retrieving the attribute for actual use. However, all warnings should
80+
# have the same message, and it should be our util-specific message.
81+
(message,) = {str(entry.message) for entry in ctx}
82+
assert "git.util" in message, "Has alias."
83+
assert "git.index.util" in message, "Has target."
84+
assert "should not be relied on" in message, "Distinct from other messages."
85+
86+
# As above, we check access through the util alias to the TemporaryFileSwap member.
87+
from git.index.util import TemporaryFileSwap
88+
89+
assert_type(util.TemporaryFileSwap, Type[TemporaryFileSwap])
90+
91+
# This comes after the static assertion, just in case it would affect the inference.
92+
assert util.TemporaryFileSwap is TemporaryFileSwap
93+
94+
95+
_PRIVATE_MODULE_ALIAS_TARGETS = (
96+
git.refs.head,
97+
git.refs.log,
98+
git.refs.reference,
99+
git.refs.symbolic,
100+
git.refs.tag,
101+
git.index.base,
102+
git.index.fun,
103+
git.index.typ,
104+
)
105+
"""Targets of private aliases in the git module to some modules, not including util."""
106+
107+
108+
_PRIVATE_MODULE_ALIAS_TARGET_NAMES = (
109+
"git.refs.head",
110+
"git.refs.log",
111+
"git.refs.reference",
112+
"git.refs.symbolic",
113+
"git.refs.tag",
114+
"git.index.base",
115+
"git.index.fun",
116+
"git.index.typ",
117+
)
118+
"""Expected ``__name__`` attributes of targets of private aliases in the git module."""
119+
120+
121+
def test_alias_target_module_names_are_by_location() -> None:
122+
"""The aliases are weird, but their targets are normal, even in ``__name__``."""
123+
actual = [module.__name__ for module in _PRIVATE_MODULE_ALIAS_TARGETS]
124+
expected = list(_PRIVATE_MODULE_ALIAS_TARGET_NAMES)
125+
assert actual == expected
126+
127+
128+
def test_private_module_alias_access() -> None:
129+
"""Non-util private alias access works but warns and is a deliberate mypy error."""
130+
with pytest.deprecated_call() as ctx:
131+
assert (
132+
git.head, # type: ignore[attr-defined]
133+
git.log, # type: ignore[attr-defined]
134+
git.reference, # type: ignore[attr-defined]
135+
git.symbolic, # type: ignore[attr-defined]
136+
git.tag, # type: ignore[attr-defined]
137+
git.base, # type: ignore[attr-defined]
138+
git.fun, # type: ignore[attr-defined]
139+
git.typ, # type: ignore[attr-defined]
140+
) == _PRIVATE_MODULE_ALIAS_TARGETS
141+
142+
# Each should have warned exactly once, and note what to use instead.
143+
messages = [str(w.message) for w in ctx]
144+
145+
assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
146+
147+
for fullname, message in zip(_PRIVATE_MODULE_ALIAS_TARGET_NAMES, messages):
148+
assert message.endswith(f"Use {fullname} instead.")
149+
150+
151+
def test_private_module_alias_import() -> None:
152+
"""Non-util private alias import works but warns and is a deliberate mypy error."""
153+
with pytest.deprecated_call() as ctx:
154+
from git import head # type: ignore[attr-defined]
155+
from git import log # type: ignore[attr-defined]
156+
from git import reference # type: ignore[attr-defined]
157+
from git import symbolic # type: ignore[attr-defined]
158+
from git import tag # type: ignore[attr-defined]
159+
from git import base # type: ignore[attr-defined]
160+
from git import fun # type: ignore[attr-defined]
161+
from git import typ # type: ignore[attr-defined]
162+
163+
assert (
164+
head,
165+
log,
166+
reference,
167+
symbolic,
168+
tag,
169+
base,
170+
fun,
171+
typ,
172+
) == _PRIVATE_MODULE_ALIAS_TARGETS
173+
174+
# Each import may warn multiple times. In CPython there will be currently always be
175+
# exactly two warnings per import, possibly due to the equivalent of calling hasattr
176+
# to do a pre-check prior to retrieving the attribute for actual use. However, for
177+
# each import, all messages should be the same and should note what to use instead.
178+
messages_with_duplicates = [str(w.message) for w in ctx]
179+
messages = [message for message, _ in itertools.groupby(messages_with_duplicates)]
180+
181+
assert len(messages) == len(_PRIVATE_MODULE_ALIAS_TARGETS)
182+
183+
for fullname, message in zip(_PRIVATE_MODULE_ALIAS_TARGET_NAMES, messages):
184+
assert message.endswith(f"Use {fullname} instead.")
185+
186+
187+
def test_dir_contains_public_attributes() -> None:
188+
"""All public attributes of the git module are present when dir() is called on it.
189+
190+
This is naturally the case, but some ways of adding dynamic attribute access
191+
behavior can change it, especially if __dir__ is defined but care is not taken to
192+
preserve the contents that should already be present.
193+
194+
Note that dir() should usually automatically list non-public attributes if they are
195+
actually "physically" present as well, so the approach taken here to test it should
196+
not be reproduced if __dir__ is added (instead, a call to globals() could be used,
197+
as its keys list the automatic values).
198+
"""
199+
expected_subset = set(git.__all__)
200+
actual = set(dir(git))
201+
assert expected_subset <= actual
202+
203+
204+
def test_dir_does_not_contain_util() -> None:
205+
"""The util attribute is absent from the dir() of git.
206+
207+
Because this behavior is less confusing than including it, where its meaning would
208+
be assumed by users examining the dir() for what is available.
209+
"""
210+
assert "util" not in dir(git)
211+
212+
213+
def test_dir_does_not_contain_private_module_aliases() -> None:
214+
"""Names from inside index and refs only pretend to be there and are not in dir().
215+
216+
The reason for omitting these is not that they are private, since private members
217+
are usually included in dir() when actually present. Instead, these are only sort
218+
of even there, no longer being imported and only being resolved dynamically for the
219+
time being. In addition, it would be confusing to list these because doing so would
220+
obscure the module structure of GitPython.
221+
"""
222+
expected_absent = {
223+
"head",
224+
"log",
225+
"reference",
226+
"symbolic",
227+
"tag",
228+
"base",
229+
"fun",
230+
"typ",
231+
}
232+
actual = set(dir(git))
233+
assert not (expected_absent & actual), "They should be completely disjoint."

‎test/deprecation/test_types.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# This module is part of GitPython and is released under the
2+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3+
4+
"""Tests for dynamic and static characteristics of git.types module attributes."""
5+
6+
import sys
7+
8+
if sys.version_info >= (3, 8):
9+
from typing import Literal
10+
else:
11+
from typing_extensions import Literal
12+
13+
import pytest
14+
15+
import git.types
16+
17+
18+
def test_cannot_access_undefined() -> None:
19+
"""Accessing a bogus attribute in git.types remains a dynamic and static error."""
20+
with pytest.raises(AttributeError):
21+
git.types.foo # type: ignore[attr-defined]
22+
23+
24+
def test_can_access_lit_commit_ish_but_it_is_not_usable() -> None:
25+
"""Lit_commit_ish_can be accessed, but warns and is an invalid type annotation."""
26+
# It would be fine to test attribute access rather than a "from" import. But a
27+
# "from" import is more likely to appear in actual usage, so it is used here.
28+
with pytest.deprecated_call() as ctx:
29+
from git.types import Lit_commit_ish
30+
31+
# As noted in test_toplevel.test_util_alias_import, there may be multiple warnings,
32+
# but all with the same message.
33+
(message,) = {str(entry.message) for entry in ctx}
34+
assert "Lit_commit_ish is deprecated." in message
35+
assert 'Literal["commit", "tag", "blob", "tree"]' in message, "Has old definition."
36+
assert 'Literal["commit", "tag"]' in message, "Has new definition."
37+
assert "GitObjectTypeString" in message, "Has new type name for old definition."
38+
39+
_: Lit_commit_ish = "commit" # type: ignore[valid-type]
40+
41+
# It should be as documented (even though deliberately unusable in static checks).
42+
assert Lit_commit_ish == Literal["commit", "tag"]
43+
44+
45+
def test_dir() -> None:
46+
"""dir() on git.types includes public names, even ``Lit_commit_ish``.
47+
48+
It also contains private names that we don't test. See test_compat.test_dir.
49+
"""
50+
expected_subset = {
51+
"PathLike",
52+
"TBD",
53+
"AnyGitObject",
54+
"Tree_ish",
55+
"Commit_ish",
56+
"GitObjectTypeString",
57+
"Lit_commit_ish",
58+
"Lit_config_levels",
59+
"ConfigLevels_Tup",
60+
"CallableProgress",
61+
"assert_never",
62+
"Files_TD",
63+
"Total_TD",
64+
"HSH_TD",
65+
"Has_Repo",
66+
"Has_id_attribute",
67+
}
68+
actual = set(dir(git.types))
69+
assert expected_subset <= actual

‎test/performance/lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This module is part of GitPython and is released under the
22
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
33

4-
"""Support library for tests."""
4+
"""Support library for performance tests."""
55

66
import logging
77
import os

‎test/test_git.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -669,13 +669,13 @@ def test_successful_default_refresh_invalidates_cached_version_info(self):
669669
stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE"))
670670

671671
if sys.platform == "win32":
672-
# On Windows, use a shell so "git" finds "git.cmd". (In the infrequent
673-
# case that this effect is desired in production code, it should not be
674-
# done with this technique. USE_SHELL=True is less secure and reliable,
675-
# as unintended shell expansions can occur, and is deprecated. Instead,
676-
# use a custom command, by setting the GIT_PYTHON_GIT_EXECUTABLE
677-
# environment variable to git.cmd or by passing git.cmd's full path to
678-
# git.refresh. Or wrap the script with a .exe shim.)
672+
# On Windows, use a shell so "git" finds "git.cmd". The correct and safe
673+
# ways to do this straightforwardly are to set GIT_PYTHON_GIT_EXECUTABLE
674+
# to git.cmd in the environment, or call git.refresh with the command's
675+
# full path. See the Git.USE_SHELL docstring for deprecation details.
676+
# But this tests a "default" scenario where neither is done. The
677+
# approach used here, setting USE_SHELL to True so PATHEXT is honored,
678+
# should not be used in production code (nor even in most test cases).
679679
stack.enter_context(mock.patch.object(Git, "USE_SHELL", True))
680680

681681
new_git = Git()

‎tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ description = Typecheck with mypy
3030
base_python = py{39,310,311,312,38,37}
3131
set_env =
3232
MYPY_FORCE_COLOR = 1
33-
commands = mypy -p git
33+
commands = mypy
3434
ignore_outcome = true
3535

3636
[testenv:html]

0 commit comments

Comments
 (0)
Please sign in to comment.