diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4ef741c36..7cee0cd64 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -88,9 +88,12 @@ jobs: - name: Check types with mypy run: | - mypy -p git - # With new versions of mypy new issues might arise. This is a problem if there is nobody able to fix them, - # so we have to ignore errors until that changes. + mypy --python-version=${{ matrix.python-version }} -p git + env: + MYPY_FORCE_COLOR: "1" + TERM: "xterm-256color" # For color: https://github.com/python/mypy/issues/13817 + # With new versions of mypy new issues might arise. This is a problem if there is + # nobody able to fix them, so we have to ignore errors until that changes. continue-on-error: true - name: Test with pytest diff --git a/git/__init__.py b/git/__init__.py index ed8a88d4b..ca5bed7a3 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,38 +5,6 @@ # @PydevCodeAnalysisIgnore -__version__ = "git" - -from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING - -from gitdb.util import to_hex_sha -from git.exc import * # noqa: F403 # @NoMove @IgnorePep8 -from git.types import PathLike - -try: - from git.compat import safe_decode # @NoMove @IgnorePep8 - from git.config import GitConfigParser # @NoMove @IgnorePep8 - from git.objects import * # noqa: F403 # @NoMove @IgnorePep8 - from git.refs import * # noqa: F403 # @NoMove @IgnorePep8 - from git.diff import * # noqa: F403 # @NoMove @IgnorePep8 - from git.db import * # noqa: F403 # @NoMove @IgnorePep8 - from git.cmd import Git # @NoMove @IgnorePep8 - from git.repo import Repo # @NoMove @IgnorePep8 - from git.remote import * # noqa: F403 # @NoMove @IgnorePep8 - from git.index import * # noqa: F403 # @NoMove @IgnorePep8 - from git.util import ( # @NoMove @IgnorePep8 - LockFile, - BlockingLockFile, - Stats, - Actor, - remove_password_if_present, - rmtree, - ) -except GitError as _exc: # noqa: F405 - raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc - -# __all__ must be statically defined by py.typed support -# __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))] __all__ = [ # noqa: F405 "Actor", "AmbiguousObjectName", @@ -52,6 +20,7 @@ "CommandError", "Commit", "Diff", + "DiffConstants", "DiffIndex", "Diffable", "FetchInfo", @@ -65,18 +34,19 @@ "HEAD", "Head", "HookExecutionError", + "INDEX", "IndexEntry", "IndexFile", "IndexObject", "InvalidDBRoot", "InvalidGitRepositoryError", - "List", + "List", # Deprecated - import this from `typing` instead. "LockFile", "NULL_TREE", "NoSuchPathError", "ODBError", "Object", - "Optional", + "Optional", # Deprecated - import this from `typing` instead. "ParseError", "PathLike", "PushInfo", @@ -90,31 +60,62 @@ "RepositoryDirtyError", "RootModule", "RootUpdateProgress", - "Sequence", + "Sequence", # Deprecated - import from `typing`, or `collections.abc` in 3.9+. "StageType", "Stats", "Submodule", "SymbolicReference", - "TYPE_CHECKING", + "TYPE_CHECKING", # Deprecated - import this from `typing` instead. "Tag", "TagObject", "TagReference", "Tree", "TreeModifier", - "Tuple", - "Union", + "Tuple", # Deprecated - import this from `typing` instead. + "Union", # Deprecated - import this from `typing` instead. "UnmergedEntriesError", "UnsafeOptionError", "UnsafeProtocolError", "UnsupportedOperation", "UpdateProgress", "WorkTreeRepositoryUnsupported", + "refresh", "remove_password_if_present", "rmtree", "safe_decode", "to_hex_sha", ] +__version__ = "git" + +from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING + +from gitdb.util import to_hex_sha +from git.exc import * # noqa: F403 # @NoMove @IgnorePep8 +from git.types import PathLike + +try: + from git.compat import safe_decode # @NoMove @IgnorePep8 + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import * # noqa: F403 # @NoMove @IgnorePep8 + from git.refs import * # noqa: F403 # @NoMove @IgnorePep8 + from git.diff import * # noqa: F403 # @NoMove @IgnorePep8 + from git.db import * # noqa: F403 # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # noqa: F403 # @NoMove @IgnorePep8 + from git.index import * # noqa: F403 # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 + LockFile, + BlockingLockFile, + Stats, + Actor, + remove_password_if_present, + rmtree, + ) +except GitError as _exc: # noqa: F405 + raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc + # { Initialize git executable path GIT_OK = None @@ -146,7 +147,7 @@ def refresh(path: Optional[PathLike] = None) -> None: if not Git.refresh(path=path): return if not FetchInfo.refresh(): # noqa: F405 - return # type: ignore [unreachable] + return # type: ignore[unreachable] GIT_OK = True diff --git a/git/cmd.py b/git/cmd.py index 915f46a05..ab2688a25 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -14,6 +14,7 @@ import signal from subprocess import Popen, PIPE, DEVNULL import subprocess +import sys import threading from textwrap import dedent @@ -171,7 +172,7 @@ def pump_stream( p_stdout = process.proc.stdout if process.proc else None p_stderr = process.proc.stderr if process.proc else None else: - process = cast(Popen, process) # type: ignore [redundant-cast] + process = cast(Popen, process) # type: ignore[redundant-cast] cmdline = getattr(process, "args", "") p_stdout = process.stdout p_stderr = process.stderr @@ -214,72 +215,77 @@ def pump_stream( error_str = error_str.encode() # We ignore typing on the next line because mypy does not like the way # we inferred that stderr takes str or bytes. - stderr_handler(error_str) # type: ignore + stderr_handler(error_str) # type: ignore[arg-type] if finalizer: finalizer(process) -def _safer_popen_windows( - command: Union[str, Sequence[Any]], - *, - shell: bool = False, - env: Optional[Mapping[str, str]] = None, - **kwargs: Any, -) -> Popen: - """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search. - - This avoids an untrusted search path condition where a file like ``git.exe`` in a - malicious repository would be run when GitPython operates on the repository. The - process using GitPython may have an untrusted repository's working tree as its - current working directory. Some operations may temporarily change to that directory - before running a subprocess. In addition, while by default GitPython does not run - external commands with a shell, it can be made to do so, in which case the CWD of - the subprocess, which GitPython usually sets to a repository working tree, can - itself be searched automatically by the shell. This wrapper covers all those cases. +safer_popen: Callable[..., Popen] - :note: - This currently works by setting the :envvar:`NoDefaultCurrentDirectoryInExePath` - environment variable during subprocess creation. It also takes care of passing - Windows-specific process creation flags, but that is unrelated to path search. +if sys.platform == "win32": - :note: - The current implementation contains a race condition on :attr:`os.environ`. - GitPython isn't thread-safe, but a program using it on one thread should ideally - be able to mutate :attr:`os.environ` on another, without unpredictable results. - See comments in https://github.com/gitpython-developers/GitPython/pull/1650. - """ - # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See: - # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal - # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP - creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP - - # When using a shell, the shell is the direct subprocess, so the variable must be - # set in its environment, to affect its search behavior. (The "1" can be any value.) - if shell: - safer_env = {} if env is None else dict(env) - safer_env["NoDefaultCurrentDirectoryInExePath"] = "1" - else: - safer_env = env - - # When not using a shell, the current process does the search in a CreateProcessW - # API call, so the variable must be set in our environment. With a shell, this is - # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is - # patched. If that is unpatched, then in the rare case the ComSpec environment - # variable is unset, the search for the shell itself is unsafe. Setting - # NoDefaultCurrentDirectoryInExePath in all cases, as is done here, is simpler and - # protects against that. (As above, the "1" can be any value.) - with patch_env("NoDefaultCurrentDirectoryInExePath", "1"): - return Popen( - command, - shell=shell, - env=safer_env, - creationflags=creationflags, - **kwargs, - ) + def _safer_popen_windows( + command: Union[str, Sequence[Any]], + *, + shell: bool = False, + env: Optional[Mapping[str, str]] = None, + **kwargs: Any, + ) -> Popen: + """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the + search. + + This avoids an untrusted search path condition where a file like ``git.exe`` in + a malicious repository would be run when GitPython operates on the repository. + The process using GitPython may have an untrusted repository's working tree as + its current working directory. Some operations may temporarily change to that + directory before running a subprocess. In addition, while by default GitPython + does not run external commands with a shell, it can be made to do so, in which + case the CWD of the subprocess, which GitPython usually sets to a repository + working tree, can itself be searched automatically by the shell. This wrapper + covers all those cases. + :note: + This currently works by setting the + :envvar:`NoDefaultCurrentDirectoryInExePath` environment variable during + subprocess creation. It also takes care of passing Windows-specific process + creation flags, but that is unrelated to path search. + + :note: + The current implementation contains a race condition on :attr:`os.environ`. + GitPython isn't thread-safe, but a program using it on one thread should + ideally be able to mutate :attr:`os.environ` on another, without + unpredictable results. See comments in: + https://github.com/gitpython-developers/GitPython/pull/1650 + """ + # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. + # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal + # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP + creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP + + # When using a shell, the shell is the direct subprocess, so the variable must + # be set in its environment, to affect its search behavior. + if shell: + # The original may be immutable, or the caller may reuse it. Mutate a copy. + env = {} if env is None else dict(env) + env["NoDefaultCurrentDirectoryInExePath"] = "1" # The "1" can be an value. + + # When not using a shell, the current process does the search in a + # CreateProcessW API call, so the variable must be set in our environment. With + # a shell, that's unnecessary if https://github.com/python/cpython/issues/101283 + # is patched. In Python versions where it is unpatched, and in the rare case the + # ComSpec environment variable is unset, the search for the shell itself is + # unsafe. Setting NoDefaultCurrentDirectoryInExePath in all cases, as done here, + # is simpler and protects against that. (As above, the "1" can be any value.) + with patch_env("NoDefaultCurrentDirectoryInExePath", "1"): + return Popen( + command, + shell=shell, + env=env, + creationflags=creationflags, + **kwargs, + ) -if os.name == "nt": safer_popen = _safer_popen_windows else: safer_popen = Popen @@ -1119,13 +1125,13 @@ def execute( if inline_env is not None: env.update(inline_env) - if os.name == "nt": - cmd_not_found_exception = OSError + if sys.platform == "win32": if kill_after_timeout is not None: raise GitCommandError( redacted_command, '"kill_after_timeout" feature is not supported on Windows.', ) + cmd_not_found_exception = OSError else: cmd_not_found_exception = FileNotFoundError # END handle @@ -1164,37 +1170,57 @@ def execute( if as_process: return self.AutoInterrupt(proc, command) - def kill_process(pid: int) -> None: - """Callback to kill a process.""" - if os.name == "nt": - raise AssertionError("Bug: This callback would be ineffective and unsafe on Windows, stopping.") - p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE) - child_pids = [] - if p.stdout is not None: - for line in p.stdout: - if len(line.split()) > 0: - local_pid = (line.split())[0] - if local_pid.isdigit(): - child_pids.append(int(local_pid)) - try: - os.kill(pid, signal.SIGKILL) - for child_pid in child_pids: - try: - os.kill(child_pid, signal.SIGKILL) - except OSError: - pass - kill_check.set() # Tell the main routine that the process was killed. - except OSError: - # It is possible that the process gets completed in the duration after - # timeout happens and before we try to kill the process. - pass - return - - # END kill_process - - if kill_after_timeout is not None: + if sys.platform != "win32" and kill_after_timeout is not None: + # Help mypy figure out this is not None even when used inside communicate(). + timeout = kill_after_timeout + + def kill_process(pid: int) -> None: + """Callback to kill a process. + + This callback implementation would be ineffective and unsafe on Windows. + """ + p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE) + child_pids = [] + if p.stdout is not None: + for line in p.stdout: + if len(line.split()) > 0: + local_pid = (line.split())[0] + if local_pid.isdigit(): + child_pids.append(int(local_pid)) + try: + os.kill(pid, signal.SIGKILL) + for child_pid in child_pids: + try: + os.kill(child_pid, signal.SIGKILL) + except OSError: + pass + # Tell the main routine that the process was killed. + kill_check.set() + except OSError: + # It is possible that the process gets completed in the duration + # after timeout happens and before we try to kill the process. + pass + return + + def communicate() -> Tuple[AnyStr, AnyStr]: + watchdog.start() + out, err = proc.communicate() + watchdog.cancel() + if kill_check.is_set(): + err = 'Timeout: the command "%s" did not complete in %d ' "secs." % ( + " ".join(redacted_command), + timeout, + ) + if not universal_newlines: + err = err.encode(defenc) + return out, err + + # END helpers + kill_check = threading.Event() - watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,)) + watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,)) + else: + communicate = proc.communicate # Wait for the process to return. status = 0 @@ -1203,22 +1229,11 @@ def kill_process(pid: int) -> None: newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: - if kill_after_timeout is not None: - watchdog.start() - stdout_value, stderr_value = proc.communicate() - if kill_after_timeout is not None: - watchdog.cancel() - if kill_check.is_set(): - stderr_value = 'Timeout: the command "%s" did not complete in %d ' "secs." % ( - " ".join(redacted_command), - kill_after_timeout, - ) - if not universal_newlines: - stderr_value = stderr_value.encode(defenc) + stdout_value, stderr_value = communicate() # Strip trailing "\n". - if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore + if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type] stdout_value = stdout_value[:-1] - if stderr_value.endswith(newline): # type: ignore + if stderr_value.endswith(newline): # type: ignore[arg-type] stderr_value = stderr_value[:-1] status = proc.returncode @@ -1228,7 +1243,7 @@ def kill_process(pid: int) -> None: stdout_value = proc.stdout.read() stderr_value = proc.stderr.read() # Strip trailing "\n". - if stderr_value.endswith(newline): # type: ignore + if stderr_value.endswith(newline): # type: ignore[arg-type] stderr_value = stderr_value[:-1] status = proc.wait() # END stdout handling diff --git a/git/compat.py b/git/compat.py index e64c645c7..6f5376c9d 100644 --- a/git/compat.py +++ b/git/compat.py @@ -3,7 +3,12 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -"""Utilities to help provide compatibility with Python 3.""" +"""Utilities to help provide compatibility with Python 3. + +This module exists for historical reasons. Code outside GitPython may make use of public +members of this module, but is unlikely to benefit from doing so. GitPython continues to +use some of these utilities, in some cases for compatibility across different platforms. +""" import locale import os diff --git a/git/config.py b/git/config.py index 2164f67dc..4441c2187 100644 --- a/git/config.py +++ b/git/config.py @@ -246,7 +246,7 @@ def items_all(self) -> List[Tuple[str, List[_T]]]: def get_config_path(config_level: Lit_config_levels) -> str: # We do not support an absolute path of the gitconfig on Windows. # Use the global config instead. - if os.name == "nt" and config_level == "system": + if sys.platform == "win32" and config_level == "system": config_level = "global" if config_level == "system": @@ -344,9 +344,9 @@ def __init__( configuration files. """ cp.RawConfigParser.__init__(self, dict_type=_OMD) - self._dict: Callable[..., _OMD] # type: ignore # mypy/typeshed bug? + self._dict: Callable[..., _OMD] self._defaults: _OMD - self._sections: _OMD # type: ignore # mypy/typeshed bug? + self._sections: _OMD # Used in Python 3. Needs to stay in sync with sections for underlying # implementation to work. diff --git a/git/diff.py b/git/diff.py index 966b73154..06935f87e 100644 --- a/git/diff.py +++ b/git/diff.py @@ -3,6 +3,7 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ +import enum import re from git.cmd import handle_process_output @@ -22,13 +23,12 @@ Match, Optional, Tuple, - Type, TypeVar, Union, TYPE_CHECKING, cast, ) -from git.types import PathLike, Literal +from git.types import Literal, PathLike if TYPE_CHECKING: from .objects.tree import Tree @@ -48,10 +48,55 @@ # ------------------------------------------------------------------------ -__all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE") +__all__ = ("DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff") -NULL_TREE = object() -"""Special object to compare against the empty tree in diffs.""" + +@enum.unique +class DiffConstants(enum.Enum): + """Special objects for :meth:`Diffable.diff`. + + See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various + values including these. + + :note: + These constants are also available as attributes of the :mod:`git.diff` module, + the :class:`Diffable` class and its subclasses and instances, and the top-level + :mod:`git` module. + """ + + NULL_TREE = enum.auto() + """Stand-in indicating you want to compare against the empty tree in diffs. + + Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and + :const:`Diffable.NULL_TREE`. + """ + + INDEX = enum.auto() + """Stand-in indicating you want to diff against the index. + + Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and + :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been + kept for backward compatibility and made an alias of this, so it may still be used. + """ + + +NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE +"""Stand-in indicating you want to compare against the empty tree in diffs. + +See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter. + +This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as +:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`. +""" + +INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX +"""Stand-in indicating you want to diff against the index. + +See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter. + +This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as +:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. +""" _octal_byte_re = re.compile(rb"\\([0-9]{3})") @@ -84,19 +129,56 @@ class Diffable: compatible type. :note: - Subclasses require a repo member as it is the case for - :class:`~git.objects.base.Object` instances, for practical reasons we do not + Subclasses require a :attr:`repo` member, as it is the case for + :class:`~git.objects.base.Object` instances. For practical reasons we do not derive from :class:`~git.objects.base.Object`. """ __slots__ = () - class Index: - """Stand-in indicating you want to diff against the index.""" + repo: "Repo" + """Repository to operate on. Must be provided by subclass or sibling class.""" + + NULL_TREE = NULL_TREE + """Stand-in indicating you want to compare against the empty tree in diffs. + + See the :meth:`diff` method, which accepts this as a value of its ``other`` + parameter. + + This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as + :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`. + """ + + INDEX = INDEX + """Stand-in indicating you want to diff against the index. + + See the :meth:`diff` method, which accepts this as a value of its ``other`` + parameter. + + This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as + :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`, + which is kept for backward compatibility (it is now defined an alias of this). + """ + + Index = INDEX + """Stand-in indicating you want to diff against the index + (same as :const:`~Diffable.INDEX`). + + This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See + :const:`~Diffable.INDEX` and :meth:`diff` for details. + + :note: + Although always meant for use as an opaque constant, this was formerly defined + as a class. Its usage is unchanged, but static type annotations that attempt + to permit only this object must be changed to avoid new mypy errors. This was + previously not possible to do, though ``Type[Diffable.Index]`` approximated it. + It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``. + """ def _process_diff_args( - self, args: List[Union[str, "Diffable", Type["Diffable.Index"], object]] - ) -> List[Union[str, "Diffable", Type["Diffable.Index"], object]]: + self, + args: List[Union[PathLike, "Diffable"]], + ) -> List[Union[PathLike, "Diffable"]]: """ :return: Possibly altered version of the given args list. @@ -107,7 +189,7 @@ def _process_diff_args( def diff( self, - other: Union[Type["Index"], "Tree", "Commit", None, str, object] = Index, + other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX, paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None, create_patch: bool = False, **kwargs: Any, @@ -119,12 +201,16 @@ def diff( This the item to compare us with. * If ``None``, we will be compared to the working tree. - * If :class:`~git.index.base.Treeish`, it will be compared against the - respective tree. - * If :class:`Diffable.Index`, it will be compared against the index. - * If :attr:`git.NULL_TREE`, it will compare against the empty tree. - * It defaults to :class:`Diffable.Index` so that the method will not by - default fail on bare repositories. + + * If a :class:`~git.types.Tree_ish` or string, it will be compared against + the respective tree. + + * If :const:`INDEX`, it will be compared against the index. + + * If :const:`NULL_TREE`, it will compare against the empty tree. + + This parameter defaults to :const:`INDEX` (rather than ``None``) so that the + method will not by default fail on bare repositories. :param paths: This a list of paths or a single path to limit the diff to. It will only @@ -140,14 +226,14 @@ def diff( sides of the diff. :return: - :class:`DiffIndex` + A :class:`DiffIndex` representing the computed diff. :note: - On a bare repository, `other` needs to be provided as - :class:`~Diffable.Index`, or as :class:`~git.objects.tree.Tree` or + On a bare repository, `other` needs to be provided as :const:`INDEX`, or as + an instance of :class:`~git.objects.tree.Tree` or :class:`~git.objects.commit.Commit`, or a git command error will occur. """ - args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = [] + args: List[Union[PathLike, Diffable]] = [] args.append("--abbrev=40") # We need full shas. args.append("--full-index") # Get full index paths, not only filenames. @@ -169,11 +255,8 @@ def diff( if paths is not None and not isinstance(paths, (tuple, list)): paths = [paths] - if hasattr(self, "Has_Repo"): - self.repo: "Repo" = self.repo - diff_cmd = self.repo.git.diff - if other is self.Index: + if other is INDEX: args.insert(0, "--cached") elif other is NULL_TREE: args.insert(0, "-r") # Recursive diff-tree. @@ -186,7 +269,7 @@ def diff( args.insert(0, self) - # paths is a list here, or None. + # paths is a list or tuple here, or None. if paths: args.append("--") args.extend(paths) @@ -206,7 +289,7 @@ def diff( class DiffIndex(List[T_Diff]): - R"""An Index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff + R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff properties. The class improves the diff handling convenience. diff --git a/git/index/base.py b/git/index/base.py index 985b1bccf..59b019f0f 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -11,22 +11,16 @@ import glob from io import BytesIO import os +import os.path as osp from stat import S_ISLNK import subprocess +import sys import tempfile -from git.compat import ( - force_bytes, - defenc, -) -from git.exc import GitCommandError, CheckoutError, GitError, InvalidGitRepositoryError -from git.objects import ( - Blob, - Submodule, - Tree, - Object, - Commit, -) +from git.compat import defenc, force_bytes +import git.diff as git_diff +from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError +from git.objects import Blob, Commit, Object, Submodule, Tree from git.objects.util import Serializable from git.util import ( LazyMixin, @@ -40,24 +34,17 @@ from gitdb.base import IStream from gitdb.db import MemoryDB -import git.diff as git_diff -import os.path as osp - from .fun import ( + S_IFGITLINK, + aggressive_tree_merge, entry_key, - write_cache, read_cache, - aggressive_tree_merge, - write_tree_from_cache, - stat_mode_to_index_mode, - S_IFGITLINK, run_commit_hook, + stat_mode_to_index_mode, + write_cache, + write_tree_from_cache, ) -from .typ import ( - BaseIndexEntry, - IndexEntry, - StageType, -) +from .typ import BaseIndexEntry, IndexEntry, StageType from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir # typing ----------------------------------------------------------------------------- @@ -76,16 +63,16 @@ Sequence, TYPE_CHECKING, Tuple, - Type, Union, ) -from git.types import Commit_ish, PathLike +from git.types import Literal, PathLike if TYPE_CHECKING: from subprocess import Popen - from git.repo import Repo + from git.refs.reference import Reference + from git.repo import Repo from git.util import Actor @@ -108,7 +95,7 @@ def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, A context manager object that creates the file and provides its name on entry, and deletes it on exit. """ - if os.name == "nt": + if sys.platform == "win32": fd, name = tempfile.mkstemp(dir=directory) os.close(fd) try: @@ -644,9 +631,9 @@ def write_tree(self) -> Tree: return root_tree def _process_diff_args( - self, # type: ignore[override] - args: List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]], - ) -> List[Union[str, "git_diff.Diffable", Type["git_diff.Diffable.Index"]]]: + self, + args: List[Union[PathLike, "git_diff.Diffable"]], + ) -> List[Union[PathLike, "git_diff.Diffable"]]: try: args.pop(args.index(self)) except IndexError: @@ -1127,7 +1114,7 @@ def move( def commit( self, message: str, - parent_commits: Union[Commit_ish, None] = None, + parent_commits: Union[List[Commit], None] = None, head: bool = True, author: Union[None, "Actor"] = None, committer: Union[None, "Actor"] = None, @@ -1476,10 +1463,17 @@ def reset( return self - # @ default_index, breaks typing for some reason, copied into function + # FIXME: This is documented to accept the same parameters as Diffable.diff, but this + # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.) def diff( - self, # type: ignore[override] - other: Union[Type["git_diff.Diffable.Index"], "Tree", "Commit", str, None] = git_diff.Diffable.Index, + self, + other: Union[ # type: ignore[override] + Literal[git_diff.DiffConstants.INDEX], + "Tree", + "Commit", + str, + None, + ] = git_diff.INDEX, paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None, create_patch: bool = False, **kwargs: Any, @@ -1494,12 +1488,11 @@ def diff( Will only work with indices that represent the default git index as they have not been initialized with a stream. """ - # Only run if we are the default repository index. if self._file_path != self._index_path(): raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff()) # Index against index is always empty. - if other is self.Index: + if other is self.INDEX: return git_diff.DiffIndex() # Index against anything but None is a reverse diff with the respective item. @@ -1513,12 +1506,12 @@ def diff( # Invert the existing R flag. cur_val = kwargs.get("R", False) kwargs["R"] = not cur_val - return other.diff(self.Index, paths, create_patch, **kwargs) + return other.diff(self.INDEX, paths, create_patch, **kwargs) # END diff against other item handling # If other is not None here, something is wrong. if other is not None: - raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other) + raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other) # Diff against working copy - can be handled by superclass natively. return super().diff(other, paths, create_patch, **kwargs) diff --git a/git/index/fun.py b/git/index/fun.py index beca67d3f..001e8f6f2 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -18,6 +18,7 @@ S_IXUSR, ) import subprocess +import sys from git.cmd import handle_process_output, safer_popen from git.compat import defenc, force_bytes, force_text, safe_decode @@ -99,7 +100,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: env["GIT_EDITOR"] = ":" cmd = [hp] try: - if os.name == "nt" and not _has_file_extension(hp): + if sys.platform == "win32" and not _has_file_extension(hp): # Windows only uses extensions to determine how to open files # (doesn't understand shebangs). Try using bash to run the hook. relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix() diff --git a/git/index/typ.py b/git/index/typ.py index a7d2ad47a..c247fab99 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -12,7 +12,7 @@ # typing ---------------------------------------------------------------------- -from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast, List +from typing import NamedTuple, Sequence, TYPE_CHECKING, Tuple, Union, cast from git.types import PathLike @@ -60,8 +60,8 @@ def __call__(self, stage_blob: Tuple[StageType, Blob]) -> bool: path: Path = pathlike if isinstance(pathlike, Path) else Path(pathlike) # TODO: Change to use `PosixPath.is_relative_to` once Python 3.8 is no # longer supported. - filter_parts: List[str] = path.parts - blob_parts: List[str] = blob_path.parts + filter_parts = path.parts + blob_parts = blob_path.parts if len(filter_parts) > len(blob_parts): continue if all(i == j for i, j in zip(filter_parts, blob_parts)): diff --git a/git/objects/base.py b/git/objects/base.py index 2b8dd0ff6..f568a4bc5 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -3,12 +3,12 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -from git.exc import WorkTreeRepositoryUnsupported -from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex - import gitdb.typ as dbtyp import os.path as osp +from git.exc import WorkTreeRepositoryUnsupported +from git.util import LazyMixin, join_path_native, stream_copy, bin_to_hex + from .util import get_object_type_by_name @@ -16,30 +16,58 @@ from typing import Any, TYPE_CHECKING, Union -from git.types import PathLike, Commit_ish, Lit_commit_ish +from git.types import AnyGitObject, GitObjectTypeString, PathLike if TYPE_CHECKING: - from git.repo import Repo from gitdb.base import OStream + + from git.refs.reference import Reference + from git.repo import Repo + from .tree import Tree from .blob import Blob from .submodule.base import Submodule - from git.refs.reference import Reference IndexObjUnion = Union["Tree", "Blob", "Submodule"] # -------------------------------------------------------------------------- - -_assertion_msg_format = "Created object %r whose python type %r disagrees with the actual git object type %r" - __all__ = ("Object", "IndexObject") class Object(LazyMixin): - """An Object which may be :class:`~git.objects.blob.Blob`, - :class:`~git.objects.tree.Tree`, :class:`~git.objects.commit.Commit` or - `~git.objects.tag.TagObject`.""" + """Base class for classes representing git object types. + + The following four leaf classes represent specific kinds of git objects: + + * :class:`Blob ` + * :class:`Tree ` + * :class:`Commit ` + * :class:`TagObject ` + + See gitglossary(7) on: + + * "object": https://git-scm.com/docs/gitglossary#def_object + * "object type": https://git-scm.com/docs/gitglossary#def_object_type + * "blob": https://git-scm.com/docs/gitglossary#def_blob_object + * "tree object": https://git-scm.com/docs/gitglossary#def_tree_object + * "commit object": https://git-scm.com/docs/gitglossary#def_commit_object + * "tag object": https://git-scm.com/docs/gitglossary#def_tag_object + + :note: + See the :class:`~git.types.AnyGitObject` union type of the four leaf subclasses + that represent actual git object types. + + :note: + :class:`~git.objects.submodule.base.Submodule` is defined under the hierarchy + rooted at this :class:`Object` class, even though submodules are not really a + type of git object. (This also applies to its + :class:`~git.objects.submodule.root.RootModule` subclass.) + + :note: + This :class:`Object` class should not be confused with :class:`object` (the root + of the class hierarchy in Python). + """ NULL_HEX_SHA = "0" * 40 NULL_BIN_SHA = b"\0" * 20 @@ -53,7 +81,21 @@ class Object(LazyMixin): __slots__ = ("repo", "binsha", "size") - type: Union[Lit_commit_ish, None] = None + type: Union[GitObjectTypeString, None] = None + """String identifying (a concrete :class:`Object` subtype for) a git object type. + + The subtypes that this may name correspond to the kinds of git objects that exist, + i.e., the objects that may be present in a git repository. + + :note: + Most subclasses represent specific types of git objects and override this class + attribute accordingly. This attribute is ``None`` in the :class:`Object` base + class, as well as the :class:`IndexObject` intermediate subclass, but never + ``None`` in concrete leaf subclasses representing specific git object types. + + :note: + See also :class:`~git.types.GitObjectTypeString`. + """ def __init__(self, repo: "Repo", binsha: bytes) -> None: """Initialize an object by identifying it by its binary sha. @@ -75,7 +117,7 @@ def __init__(self, repo: "Repo", binsha: bytes) -> None: ) @classmethod - def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish: + def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject: """ :return: New :class:`Object` instance of a type appropriate to the object type behind @@ -92,7 +134,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish: return repo.rev_parse(str(id)) @classmethod - def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish: + def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject: """ :return: New object instance of a type appropriate to represent the given binary sha1 @@ -113,8 +155,7 @@ def _set_cache_(self, attr: str) -> None: """Retrieve object information.""" if attr == "size": oinfo = self.repo.odb.info(self.binsha) - self.size = oinfo.size # type: int - # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type) + self.size = oinfo.size # type: int else: super()._set_cache_(attr) @@ -174,9 +215,13 @@ def stream_data(self, ostream: "OStream") -> "Object": class IndexObject(Object): - """Base for all objects that can be part of the index file, namely - :class:`~git.objects.tree.Tree`, :class:`~git.objects.blob.Blob` and - :class:`~git.objects.submodule.base.Submodule` objects.""" + """Base for all objects that can be part of the index file. + + The classes representing git object types that can be part of the index file are + :class:`~git.objects.tree.Tree and :class:`~git.objects.blob.Blob`. In addition, + :class:`~git.objects.submodule.base.Submodule`, which is not really a git object + type but can be part of an index file, is also a subclass. + """ __slots__ = ("path", "mode") diff --git a/git/objects/blob.py b/git/objects/blob.py index 4035c3e7c..b49930edf 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -4,19 +4,23 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ from mimetypes import guess_type -from . import base +import sys +from . import base -try: +if sys.version_info >= (3, 8): from typing import Literal -except ImportError: +else: from typing_extensions import Literal __all__ = ("Blob",) class Blob(base.IndexObject): - """A Blob encapsulates a git blob object.""" + """A Blob encapsulates a git blob object. + + See gitglossary(7) on "blob": https://git-scm.com/docs/gitglossary#def_blob_object + """ DEFAULT_MIME_TYPE = "text/plain" type: Literal["blob"] = "blob" diff --git a/git/objects/commit.py b/git/objects/commit.py index dcb3be695..473eae8cc 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -3,57 +3,57 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ +from collections import defaultdict import datetime +from io import BytesIO +import logging +import os import re from subprocess import Popen, PIPE +import sys +from time import altzone, daylight, localtime, time, timezone + from gitdb import IStream -from git.util import hex_to_bin, Actor, Stats, finalize_process -from git.diff import Diffable from git.cmd import Git +from git.diff import Diffable +from git.util import hex_to_bin, Actor, Stats, finalize_process from .tree import Tree -from . import base from .util import ( Serializable, TraversableIterableObj, - parse_date, altz_to_utctz_str, - parse_actor_and_date, from_timestamp, + parse_actor_and_date, + parse_date, ) - -from time import time, daylight, altzone, timezone, localtime -import os -from io import BytesIO -import logging -from collections import defaultdict - +from . import base # typing ------------------------------------------------------------------ from typing import ( Any, + Dict, IO, Iterator, List, Sequence, Tuple, - Union, TYPE_CHECKING, + Union, cast, - Dict, ) -from git.types import PathLike - -try: +if sys.version_info >= (3, 8): from typing import Literal -except ImportError: +else: from typing_extensions import Literal +from git.types import PathLike + if TYPE_CHECKING: - from git.repo import Repo from git.refs import SymbolicReference + from git.repo import Repo # ------------------------------------------------------------------------ @@ -65,8 +65,12 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): """Wraps a git commit object. - This class will act lazily on some of its attributes and will query the value on - demand only if it involves calling the git binary. + See gitglossary(7) on "commit object": + https://git-scm.com/docs/gitglossary#def_commit_object + + :note: + This class will act lazily on some of its attributes and will query the value on + demand only if it involves calling the git binary. """ # ENVIRONMENT VARIABLES @@ -80,8 +84,8 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): # INVARIANTS default_encoding = "UTF-8" - # object configuration type: Literal["commit"] = "commit" + __slots__ = ( "tree", "author", @@ -95,8 +99,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): "encoding", "gpgsig", ) + _id_attribute_ = "hexsha" + parents: Sequence["Commit"] + def __init__( self, repo: "Repo", @@ -113,15 +120,12 @@ def __init__( encoding: Union[str, None] = None, gpgsig: Union[str, None] = None, ) -> None: - R"""Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as + """Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as default will be implicitly set on first query. :param binsha: 20 byte sha1. - :param parents: tuple(Commit, ...) - A tuple of commit ids or actual :class:`Commit`\s. - :param tree: A :class:`~git.objects.tree.Tree` object. @@ -293,7 +297,7 @@ def name_rev(self) -> str: def iter_items( cls, repo: "Repo", - rev: Union[str, "Commit", "SymbolicReference"], # type: ignore + rev: Union[str, "Commit", "SymbolicReference"], paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any, ) -> Iterator["Commit"]: @@ -429,7 +433,11 @@ def trailers_list(self) -> List[Tuple[str, str]]: List containing key-value tuples of whitespace stripped trailer information. """ cmd = ["git", "interpret-trailers", "--parse"] - proc: Git.AutoInterrupt = self.repo.git.execute(cmd, as_process=True, istream=PIPE) # type: ignore + proc: Git.AutoInterrupt = self.repo.git.execute( # type: ignore[call-overload] + cmd, + as_process=True, + istream=PIPE, + ) trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8") trailer = trailer.strip() @@ -508,7 +516,7 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, if proc_or_stream.stdout is not None: stream = proc_or_stream.stdout elif hasattr(proc_or_stream, "readline"): - proc_or_stream = cast(IO, proc_or_stream) # type: ignore [redundant-cast] + proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast] stream = proc_or_stream readline = stream.readline diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index e5933b116..4e5a2a964 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -7,6 +7,7 @@ import os import os.path as osp import stat +import sys import uuid import git @@ -38,23 +39,32 @@ sm_section, ) - # typing ---------------------------------------------------------------------- -from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast -from typing import Any, Iterator, Union - -from git.types import Commit_ish, PathLike, TBD +from typing import ( + Any, + Callable, + Dict, + Iterator, + Mapping, + Sequence, + TYPE_CHECKING, + Union, + cast, +) -try: +if sys.version_info >= (3, 8): from typing import Literal -except ImportError: +else: from typing_extensions import Literal +from git.types import Commit_ish, PathLike, TBD + if TYPE_CHECKING: from git.index import IndexFile - from git.repo import Repo + from git.objects.commit import Commit from git.refs import Head + from git.repo import Repo # ----------------------------------------------------------------------------- @@ -102,8 +112,8 @@ class Submodule(IndexObject, TraversableIterableObj): k_default_mode = stat.S_IFDIR | stat.S_IFLNK """Submodule flags. Submodules are directories with link-status.""" - type: Literal["submodule"] = "submodule" # type: ignore - """This is a bogus type for base class compatibility.""" + type: Literal["submodule"] = "submodule" # type: ignore[assignment] + """This is a bogus type string for base class compatibility.""" __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__") @@ -116,7 +126,7 @@ def __init__( mode: Union[int, None] = None, path: Union[PathLike, None] = None, name: Union[str, None] = None, - parent_commit: Union[Commit_ish, None] = None, + parent_commit: Union["Commit", None] = None, url: Union[str, None] = None, branch_path: Union[PathLike, None] = None, ) -> None: @@ -148,7 +158,6 @@ def __init__( if url is not None: self._url = url if branch_path is not None: - # assert isinstance(branch_path, str) self._branch_path = branch_path if name is not None: self._name = name @@ -217,7 +226,7 @@ def __repr__(self) -> str: @classmethod def _config_parser( - cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool + cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool ) -> SubmoduleConfigParser: """ :return: @@ -268,7 +277,7 @@ def _clear_cache(self) -> None: # END for each name to delete @classmethod - def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO: + def _sio_modules(cls, parent_commit: "Commit") -> BytesIO: """ :return: Configuration file as :class:`~io.BytesIO` - we only access it through the @@ -281,7 +290,7 @@ def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO: def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: """:return: Config parser constrained to our submodule in read or write mode""" try: - pc: Union["Commit_ish", None] = self.parent_commit + pc = self.parent_commit except ValueError: pc = None # END handle empty parent repository @@ -406,7 +415,7 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab """ git_file = osp.join(working_tree_dir, ".git") rela_path = osp.relpath(module_abspath, start=working_tree_dir) - if os.name == "nt" and osp.isfile(git_file): + if sys.platform == "win32" and osp.isfile(git_file): os.remove(git_file) with open(git_file, "wb") as fp: fp.write(("gitdir: %s" % rela_path).encode(defenc)) @@ -1246,7 +1255,7 @@ def remove( return self - def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) -> "Submodule": + def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule": """Set this instance to use the given commit whose tree is supposed to contain the ``.gitmodules`` blob. @@ -1499,7 +1508,7 @@ def url(self) -> str: return self._url @property - def parent_commit(self) -> "Commit_ish": + def parent_commit(self) -> "Commit": """ :return: :class:`~git.objects.commit.Commit` instance with the tree containing the @@ -1562,7 +1571,7 @@ def iter_items( cls, repo: "Repo", parent_commit: Union[Commit_ish, str] = "HEAD", - *Args: Any, + *args: Any, **kwargs: Any, ) -> Iterator["Submodule"]: """ diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py index 3268d73a4..ae56e5ef4 100644 --- a/git/objects/submodule/root.py +++ b/git/objects/submodule/root.py @@ -1,12 +1,12 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ +import logging + +import git +from git.exc import InvalidGitRepositoryError from .base import Submodule, UpdateProgress from .util import find_first_remote_branch -from git.exc import InvalidGitRepositoryError -import git - -import logging # typing ------------------------------------------------------------------- @@ -75,9 +75,9 @@ def _clear_cache(self) -> None: # { Interface - def update( + def update( # type: ignore[override] self, - previous_commit: Union[Commit_ish, None] = None, # type: ignore[override] + previous_commit: Union[Commit_ish, str, None] = None, recursive: bool = True, force_remove: bool = False, init: bool = True, diff --git a/git/objects/tag.py b/git/objects/tag.py index d8815e436..e7ecfa62b 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -5,10 +5,12 @@ """Provides an :class:`~git.objects.base.Object`-based type for annotated tags. -This defines the :class:`TagReference` class, which represents annotated tags. +This defines the :class:`TagObject` class, which represents annotated tags. For lightweight tags, see the :mod:`git.refs.tag` module. """ +import sys + from . import base from .util import get_object_type_by_name, parse_actor_and_date from ..util import hex_to_bin @@ -16,9 +18,9 @@ from typing import List, TYPE_CHECKING, Union -try: +if sys.version_info >= (3, 8): from typing import Literal -except ImportError: +else: from typing_extensions import Literal if TYPE_CHECKING: @@ -33,7 +35,11 @@ class TagObject(base.Object): """Annotated (i.e. non-lightweight) tag carrying additional information about an - object we are pointing to.""" + object we are pointing to. + + See gitglossary(7) on "tag object": + https://git-scm.com/docs/gitglossary#def_tag_object + """ type: Literal["tag"] = "tag" diff --git a/git/objects/tree.py b/git/objects/tree.py index 3964b016c..308dd47a0 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -3,17 +3,16 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -from git.util import IterableList, join_path +import sys + import git.diff as git_diff -from git.util import to_bin_sha +from git.util import IterableList, join_path, to_bin_sha -from . import util -from .base import IndexObject, IndexObjUnion +from .base import IndexObjUnion, IndexObject from .blob import Blob -from .submodule.base import Submodule - from .fun import tree_entries_from_data, tree_to_stream - +from .submodule.base import Submodule +from . import util # typing ------------------------------------------------- @@ -25,22 +24,22 @@ Iterator, List, Tuple, + TYPE_CHECKING, Type, Union, cast, - TYPE_CHECKING, ) -from git.types import PathLike - -try: +if sys.version_info >= (3, 8): from typing import Literal -except ImportError: +else: from typing_extensions import Literal +from git.types import PathLike + if TYPE_CHECKING: - from git.repo import Repo from io import BytesIO + from git.repo import Repo TreeCacheTup = Tuple[bytes, int, str] @@ -167,9 +166,12 @@ def __delitem__(self, name: str) -> None: class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable): R"""Tree objects represent an ordered list of :class:`~git.objects.blob.Blob`\s and - other :class:`~git.objects.tree.Tree`\s. + other :class:`Tree`\s. - Tree as a list: + See gitglossary(7) on "tree object": + https://git-scm.com/docs/gitglossary#def_tree_object + + Subscripting is supported, as with a list or dict: * Access a specific blob using the ``tree["filename"]`` notation. * You may likewise access by index, like ``blob = tree[0]``. @@ -235,8 +237,8 @@ def join(self, file: str) -> IndexObjUnion: """Find the named object in this tree's contents. :return: - :class:`~git.objects.blob.Blob`, :class:`~git.objects.tree.Tree`, - or :class:`~git.objects.submodule.base.Submodule` + :class:`~git.objects.blob.Blob`, :class:`Tree`, or + :class:`~git.objects.submodule.base.Submodule` :raise KeyError: If the given file or tree does not exist in this tree. @@ -302,7 +304,7 @@ def cache(self) -> TreeModifier: return TreeModifier(self._cache) def traverse( - self, # type: ignore[override] + self, predicate: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: True, prune: Callable[[Union[IndexObjUnion, TraversedTreeTup], int], bool] = lambda i, d: False, depth: int = -1, @@ -331,9 +333,9 @@ def traverse( return cast( Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]], super()._traverse( - predicate, - prune, - depth, # type: ignore + predicate, # type: ignore[arg-type] + prune, # type: ignore[arg-type] + depth, branch_first, visit_once, ignore_self, @@ -393,7 +395,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool: return False def __reversed__(self) -> Iterator[IndexObjUnion]: - return reversed(self._iter_convert_to_object(self._cache)) # type: ignore + return reversed(self._iter_convert_to_object(self._cache)) # type: ignore[call-overload] def _serialize(self, stream: "BytesIO") -> "Tree": """Serialize this tree into the stream. Assumes sorted tree data. diff --git a/git/objects/util.py b/git/objects/util.py index 26a34f94c..7cca05134 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -619,7 +619,7 @@ class TraversableIterableObj(IterableObj, Traversable): def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TIobj]: return super()._list_traverse(*args, **kwargs) - @overload # type: ignore + @overload def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ... @overload @@ -688,5 +688,13 @@ def traverse( return cast( Union[Iterator[T_TIobj], Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]], - super()._traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge), # type: ignore + super()._traverse( + predicate, # type: ignore[arg-type] + prune, # type: ignore[arg-type] + depth, + branch_first, + visit_once, + ignore_self, + as_edge, + ), ) diff --git a/git/refs/head.py b/git/refs/head.py index f6020f461..aae5767d4 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -15,14 +15,14 @@ # typing --------------------------------------------------- -from typing import Any, Sequence, Union, TYPE_CHECKING +from typing import Any, Sequence, TYPE_CHECKING, Union -from git.types import PathLike, Commit_ish +from git.types import Commit_ish, PathLike if TYPE_CHECKING: - from git.repo import Repo from git.objects import Commit from git.refs import RemoteReference + from git.repo import Repo # ------------------------------------------------------------------- @@ -44,11 +44,13 @@ class HEAD(SymbolicReference): __slots__ = () + # TODO: This can be removed once SymbolicReference.commit has static type hints. + commit: "Commit" + def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None: if path != self._HEAD_NAME: raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) super().__init__(repo, path) - self.commit: "Commit" def orig_head(self) -> SymbolicReference: """ @@ -97,7 +99,7 @@ def reset( if index: mode = "--mixed" - # Tt appears some git versions declare mixed and paths deprecated. + # It appears some git versions declare mixed and paths deprecated. # See http://github.com/Byron/GitPython/issues#issue/2. if paths: mode = None diff --git a/git/refs/reference.py b/git/refs/reference.py index a7b545fed..cf418aa5d 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -1,24 +1,20 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -from git.util import ( - LazyMixin, - IterableObj, -) +from git.util import IterableObj, LazyMixin from .symbolic import SymbolicReference, T_References - # typing ------------------------------------------------------------------ -from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING -from git.types import Commit_ish, PathLike, _T +from typing import Any, Callable, Iterator, TYPE_CHECKING, Type, Union + +from git.types import AnyGitObject, PathLike, _T if TYPE_CHECKING: from git.repo import Repo # ------------------------------------------------------------------------------ - __all__ = ["Reference"] # { Utilities @@ -81,7 +77,7 @@ def __str__(self) -> str: # @ReservedAssignment def set_object( self, - object: Union[Commit_ish, "SymbolicReference", str], + object: Union[AnyGitObject, "SymbolicReference", str], logmsg: Union[str, None] = None, ) -> "Reference": """Special version which checks if the head-log needs an update as well. @@ -150,7 +146,7 @@ def iter_items( # { Remote Interface - @property # type: ignore # mypy cannot deal with properties with an extra decorator (2021-04-21). + @property @require_remote_ref_path def remote_name(self) -> str: """ @@ -162,7 +158,7 @@ def remote_name(self) -> str: # /refs/remotes// return tokens[2] - @property # type: ignore # mypy cannot deal with properties with an extra decorator (2021-04-21). + @property @require_remote_ref_path def remote_head(self) -> str: """ diff --git a/git/refs/remote.py b/git/refs/remote.py index bb2a4e438..5cbd1b81b 100644 --- a/git/refs/remote.py +++ b/git/refs/remote.py @@ -52,7 +52,7 @@ def iter_items( # subclasses and recommends Any or "type: ignore". # (See: https://github.com/python/typing/issues/241) @classmethod - def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: # type: ignore + def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: # type: ignore[override] """Delete the given remote references. :note: diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 465acf872..754e90089 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -6,17 +6,16 @@ from git.compat import defenc from git.objects import Object from git.objects.commit import Commit +from git.refs.log import RefLog from git.util import ( + LockedFD, + assure_directory_exists, + hex_to_bin, join_path, join_path_native, to_native_path_linux, - assure_directory_exists, - hex_to_bin, - LockedFD, ) -from gitdb.exc import BadObject, BadName - -from .log import RefLog +from gitdb.exc import BadName, BadObject # typing ------------------------------------------------------------------ @@ -24,21 +23,21 @@ Any, Iterator, List, + TYPE_CHECKING, Tuple, Type, TypeVar, Union, - TYPE_CHECKING, cast, ) -from git.types import Commit_ish, PathLike +from git.types import AnyGitObject, PathLike if TYPE_CHECKING: - from git.repo import Repo - from git.refs import Head, TagReference, RemoteReference, Reference - from .log import RefLogEntry from git.config import GitConfigParser from git.objects.commit import Actor + from git.refs import Head, TagReference, RemoteReference, Reference + from git.refs.log import RefLogEntry + from git.repo import Repo T_References = TypeVar("T_References", bound="SymbolicReference") @@ -278,7 +277,7 @@ def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[T """ return cls._get_ref_info_helper(repo, ref_path) - def _get_object(self) -> Commit_ish: + def _get_object(self) -> AnyGitObject: """ :return: The object our ref currently refers to. Refs can be cached, they will always @@ -345,7 +344,7 @@ def set_commit( def set_object( self, - object: Union[Commit_ish, "SymbolicReference", str], + object: Union[AnyGitObject, "SymbolicReference", str], logmsg: Union[str, None] = None, ) -> "SymbolicReference": """Set the object we point to, possibly dereference our symbolic reference @@ -353,9 +352,12 @@ def set_object( :param object: A refspec, a :class:`SymbolicReference` or an - :class:`~git.objects.base.Object` instance. :class:`SymbolicReference` - instances will be dereferenced beforehand to obtain the object they point - to. + :class:`~git.objects.base.Object` instance. + + * :class:`SymbolicReference` instances will be dereferenced beforehand to + obtain the git object they point to. + * :class:`~git.objects.base.Object` instances must represent git objects + (:class:`~git.types.AnyGitObject`). :param logmsg: If not ``None``, the message will be used in the reflog entry to be written. @@ -385,8 +387,17 @@ def set_object( # set the commit on our reference return self._get_reference().set_object(object, logmsg) - commit = property(_get_commit, set_commit, doc="Query or set commits directly") # type: ignore - object = property(_get_object, set_object, doc="Return the object our ref currently refers to") # type: ignore + commit = property( + _get_commit, + set_commit, # type: ignore[arg-type] + doc="Query or set commits directly", + ) + + object = property( + _get_object, + set_object, # type: ignore[arg-type] + doc="Return the object our ref currently refers to", + ) def _get_reference(self) -> "SymbolicReference": """ @@ -404,22 +415,22 @@ def _get_reference(self) -> "SymbolicReference": def set_reference( self, - ref: Union[Commit_ish, "SymbolicReference", str], + ref: Union[AnyGitObject, "SymbolicReference", str], logmsg: Union[str, None] = None, ) -> "SymbolicReference": """Set ourselves to the given `ref`. - It will stay a symbol if the ref is a :class:`~git.refs.reference.Reference`. + It will stay a symbol if the `ref` is a :class:`~git.refs.reference.Reference`. - Otherwise an Object, given as :class:`~git.objects.base.Object` instance or - refspec, is assumed and if valid, will be set which effectively detaches the - reference if it was a purely symbolic one. + Otherwise a git object, specified as a :class:`~git.objects.base.Object` + instance or refspec, is assumed. If it is valid, this reference will be set to + it, which effectively detaches the reference if it was a purely symbolic one. :param ref: A :class:`SymbolicReference` instance, an :class:`~git.objects.base.Object` - instance, or a refspec string. Only if the ref is a - :class:`SymbolicReference` instance, we will point to it. Everything else is - dereferenced to obtain the actual object. + instance (specifically an :class:`~git.types.AnyGitObject`), or a refspec + string. Only if the ref is a :class:`SymbolicReference` instance, we will + point to it. Everything else is dereferenced to obtain the actual object. :param logmsg: If set to a string, the message will be used in the reflog. @@ -486,7 +497,11 @@ def set_reference( # Aliased reference reference: Union["Head", "TagReference", "RemoteReference", "Reference"] - reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") # type: ignore + reference = property( # type: ignore[assignment] + _get_reference, + set_reference, # type: ignore[arg-type] + doc="Returns the Reference we point to", + ) ref = reference def is_valid(self) -> bool: diff --git a/git/refs/tag.py b/git/refs/tag.py index 6a6dad09a..a1d0b470f 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -14,14 +14,15 @@ # typing ------------------------------------------------------------------ -from typing import Any, Type, Union, TYPE_CHECKING -from git.types import Commit_ish, PathLike +from typing import Any, TYPE_CHECKING, Type, Union + +from git.types import AnyGitObject, PathLike if TYPE_CHECKING: - from git.repo import Repo from git.objects import Commit from git.objects import TagObject from git.refs import SymbolicReference + from git.repo import Repo # ------------------------------------------------------------------------------ @@ -82,7 +83,7 @@ def tag(self) -> Union["TagObject", None]: # Make object read-only. It should be reasonably hard to adjust an existing tag. @property - def object(self) -> Commit_ish: # type: ignore[override] + def object(self) -> AnyGitObject: # type: ignore[override] return Reference._get_object(self) @classmethod diff --git a/git/remote.py b/git/remote.py index b63cfc208..1723216a4 100644 --- a/git/remote.py +++ b/git/remote.py @@ -41,14 +41,12 @@ overload, ) -from git.types import PathLike, Literal, Commit_ish +from git.types import AnyGitObject, Literal, PathLike if TYPE_CHECKING: - from git.repo.base import Repo + from git.objects.commit import Commit from git.objects.submodule.base import UpdateProgress - - # from git.objects.commit import Commit - # from git.objects import Blob, Tree, TagObject + from git.repo.base import Repo flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"] @@ -193,7 +191,7 @@ def __init__( self.summary = summary @property - def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]: + def old_commit(self) -> Union["Commit", None]: return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None @property @@ -359,7 +357,7 @@ def __init__( ref: SymbolicReference, flags: int, note: str = "", - old_commit: Union[Commit_ish, None] = None, + old_commit: Union[AnyGitObject, None] = None, remote_ref_path: Optional[PathLike] = None, ) -> None: """Initialize a new instance.""" @@ -378,7 +376,7 @@ def name(self) -> str: return self.ref.name @property - def commit(self) -> Commit_ish: + def commit(self) -> "Commit": """:return: Commit of our remote ref""" return self.ref.commit @@ -435,7 +433,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": # Parse operation string for more info. # This makes no sense for symbolic refs, but we parse it anyway. - old_commit: Union[Commit_ish, None] = None + old_commit: Union[AnyGitObject, None] = None is_tag_operation = False if "rejected" in operation: flags |= cls.REJECTED @@ -556,6 +554,9 @@ class Remote(LazyMixin, IterableObj): "--exec", ] + url: str # Obtained dynamically from _config_reader. See __getattr__ below. + """The URL configured for the remote.""" + def __init__(self, repo: "Repo", name: str) -> None: """Initialize a remote instance. @@ -567,7 +568,6 @@ def __init__(self, repo: "Repo", name: str) -> None: """ self.repo = repo self.name = name - self.url: str def __getattr__(self, attr: str) -> Any: """Allows to call this instance like ``remote.special(*args, **kwargs)`` to diff --git a/git/repo/base.py b/git/repo/base.py index a54591746..fe01a9279 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -12,6 +12,7 @@ from pathlib import Path import re import shlex +import sys import warnings import gitdb @@ -33,29 +34,29 @@ from git.remote import Remote, add_progress, to_progress_instance from git.util import ( Actor, - finalize_process, cygpath, - hex_to_bin, expand_path, + finalize_process, + hex_to_bin, remove_password_if_present, ) from .fun import ( - rev_parse, - is_git_dir, find_submodule_git_dir, - touch, find_worktree_git_dir, + is_git_dir, + rev_parse, + touch, ) # typing ------------------------------------------------------ from git.types import ( - TBD, - PathLike, - Lit_config_levels, - Commit_ish, CallableProgress, + Commit_ish, + Lit_config_levels, + PathLike, + TBD, Tree_ish, assert_never, ) @@ -67,25 +68,25 @@ Iterator, List, Mapping, + NamedTuple, Optional, Sequence, + TYPE_CHECKING, TextIO, Tuple, Type, Union, - NamedTuple, cast, - TYPE_CHECKING, ) from git.types import ConfigLevels_Tup, TypedDict if TYPE_CHECKING: - from git.util import IterableList - from git.refs.symbolic import SymbolicReference from git.objects import Tree from git.objects.submodule.base import UpdateProgress + from git.refs.symbolic import SymbolicReference from git.remote import RemoteProgress + from git.util import IterableList # ----------------------------------------------------------- @@ -95,7 +96,7 @@ class BlameEntry(NamedTuple): - commit: Dict[str, "Commit"] + commit: Dict[str, Commit] linenos: range orig_path: Optional[str] orig_linenos: range @@ -218,7 +219,7 @@ def __init__( # Given how the tests are written, this seems more likely to catch Cygwin # git used from Windows than Windows git used from Cygwin. Therefore # changing to Cygwin-style paths is the relevant operation. - epath = cygpath(epath) + epath = cygpath(str(epath)) epath = epath or path or os.getcwd() if not isinstance(epath, str): @@ -336,10 +337,10 @@ def close(self) -> None: # they are collected by the garbage collector, thus preventing deletion. # TODO: Find these references and ensure they are closed and deleted # synchronously rather than forcing a gc collection. - if os.name == "nt": + if sys.platform == "win32": gc.collect() gitdb.util.mman.collect() - if os.name == "nt": + if sys.platform == "win32": gc.collect() def __eq__(self, rhs: object) -> bool: @@ -618,7 +619,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa git_dir = self.git_dir # We do not support an absolute path of the gitconfig on Windows. # Use the global config instead. - if os.name == "nt" and config_level == "system": + if sys.platform == "win32" and config_level == "system": config_level = "global" if config_level == "system": @@ -635,7 +636,7 @@ def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[Pa else: return osp.normpath(osp.join(repo_dir, "config")) else: - assert_never( # type:ignore[unreachable] + assert_never( # type: ignore[unreachable] config_level, ValueError(f"Invalid configuration level: {config_level!r}"), ) @@ -771,7 +772,7 @@ def iter_commits( return Commit.iter_items(self, rev, paths, **kwargs) - def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]: + def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: R"""Find the closest common ancestor for the given revision (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s, :class:`~git.refs.reference.Reference`\s, etc.). @@ -796,9 +797,9 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]: raise ValueError("Please specify at least two revs, got only %i" % len(rev)) # END handle input - res: List[Union[Commit_ish, None]] = [] + res: List[Commit] = [] try: - lines = self.git.merge_base(*rev, **kwargs).splitlines() # List[str] + lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines() except GitCommandError as err: if err.status == 128: raise @@ -814,7 +815,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]: return res - def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool: + def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool: """Check if a commit is an ancestor of another. :param ancestor_rev: diff --git a/git/repo/fun.py b/git/repo/fun.py index e3c69c68c..0ac481206 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -6,34 +6,30 @@ from __future__ import annotations import os -import stat +import os.path as osp from pathlib import Path +import stat from string import digits +from git.cmd import Git from git.exc import WorkTreeRepositoryUnsupported from git.objects import Object from git.refs import SymbolicReference from git.util import hex_to_bin, bin_to_hex, cygpath -from gitdb.exc import ( - BadObject, - BadName, -) - -import os.path as osp -from git.cmd import Git +from gitdb.exc import BadName, BadObject # Typing ---------------------------------------------------------------------- -from typing import Union, Optional, cast, TYPE_CHECKING -from git.types import Commit_ish +from typing import Optional, TYPE_CHECKING, Union, cast, overload + +from git.types import AnyGitObject, Literal, PathLike if TYPE_CHECKING: - from git.types import PathLike - from .base import Repo from git.db import GitCmdObjectDB + from git.objects import Commit, TagObject from git.refs.reference import Reference - from git.objects import Commit, TagObject, Blob, Tree from git.refs.tag import Tag + from .base import Repo # ---------------------------------------------------------------------------- @@ -56,7 +52,7 @@ def touch(filename: str) -> str: return filename -def is_git_dir(d: "PathLike") -> bool: +def is_git_dir(d: PathLike) -> bool: """This is taken from the git setup.c:is_git_directory function. :raise git.exc.WorkTreeRepositoryUnsupported: @@ -79,7 +75,7 @@ def is_git_dir(d: "PathLike") -> bool: return False -def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]: +def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]: """Search for a gitdir for this worktree.""" try: statbuf = os.stat(dotgit) @@ -98,7 +94,7 @@ def find_worktree_git_dir(dotgit: "PathLike") -> Optional[str]: return None -def find_submodule_git_dir(d: "PathLike") -> Optional["PathLike"]: +def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: """Search for a submodule repo.""" if is_git_dir(d): return d @@ -141,9 +137,15 @@ def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]: # END exception handling -def name_to_object( - repo: "Repo", name: str, return_ref: bool = False -) -> Union[SymbolicReference, "Commit", "TagObject", "Blob", "Tree"]: +@overload +def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ... + + +@overload +def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ... + + +def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]: """ :return: Object specified by the given name - hexshas (short and long) as well as @@ -151,8 +153,8 @@ def name_to_object( :param return_ref: If ``True``, and name specifies a reference, we will return the reference - instead of the object. Otherwise it will raise `~gitdb.exc.BadObject` or - `~gitdb.exc.BadName`. + instead of the object. Otherwise it will raise :class:`~gitdb.exc.BadObject` or + :class:`~gitdb.exc.BadName`. """ hexsha: Union[None, str, bytes] = None @@ -201,7 +203,7 @@ def name_to_object( return Object.new_from_sha(repo, hex_to_bin(hexsha)) -def deref_tag(tag: "Tag") -> "TagObject": +def deref_tag(tag: "Tag") -> AnyGitObject: """Recursively dereference a tag and return the resulting object.""" while True: try: @@ -212,7 +214,7 @@ def deref_tag(tag: "Tag") -> "TagObject": return tag -def to_commit(obj: Object) -> Union["Commit", "TagObject"]: +def to_commit(obj: Object) -> "Commit": """Convert the given object to a commit if possible and return it.""" if obj.type == "tag": obj = deref_tag(obj) @@ -223,12 +225,18 @@ def to_commit(obj: Object) -> Union["Commit", "TagObject"]: return obj -def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: - """ +def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: + """Parse a revision string. Like ``git rev-parse``. + :return: - `~git.objects.base.Object` at the given revision, either - `~git.objects.commit.Commit`, `~git.refs.tag.Tag`, `~git.objects.tree.Tree` or - `~git.objects.blob.Blob`. + `~git.objects.base.Object` at the given revision. + + This may be any type of git object: + + * :class:`Commit ` + * :class:`TagObject ` + * :class:`Tree ` + * :class:`Blob ` :param rev: ``git rev-parse``-compatible revision specification as string. Please see @@ -249,7 +257,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: raise NotImplementedError("commit by message search (regex)") # END handle search - obj: Union[Commit_ish, "Reference", None] = None + obj: Optional[AnyGitObject] = None ref = None output_type = "commit" start = 0 @@ -271,12 +279,10 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: if token == "@": ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True)) else: - obj = cast(Commit_ish, name_to_object(repo, rev[:start])) + obj = name_to_object(repo, rev[:start]) # END handle token # END handle refname else: - assert obj is not None - if ref is not None: obj = cast("Commit", ref.commit) # END handle ref @@ -296,7 +302,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: pass # Default. elif output_type == "tree": try: - obj = cast(Commit_ish, obj) + obj = cast(AnyGitObject, obj) obj = to_commit(obj).tree except (AttributeError, ValueError): pass # Error raised later. @@ -369,7 +375,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: parsed_to = start # Handle hierarchy walk. try: - obj = cast(Commit_ish, obj) + obj = cast(AnyGitObject, obj) if token == "~": obj = to_commit(obj) for _ in range(num): @@ -398,7 +404,7 @@ def rev_parse(repo: "Repo", rev: str) -> Union["Commit", "Tag", "Tree", "Blob"]: # Still no obj? It's probably a simple name. if obj is None: - obj = cast(Commit_ish, name_to_object(repo, rev)) + obj = name_to_object(repo, rev) parsed_to = lr # END handle simple name diff --git a/git/types.py b/git/types.py index efb393471..336f49082 100644 --- a/git/types.py +++ b/git/types.py @@ -4,98 +4,226 @@ import os import sys from typing import ( # noqa: F401 + Any, + Callable, Dict, NoReturn, + Optional, Sequence as Sequence, Tuple, - Union, - Any, - Optional, - Callable, TYPE_CHECKING, TypeVar, + Union, ) if sys.version_info >= (3, 8): from typing import ( # noqa: F401 Literal, - TypedDict, Protocol, SupportsIndex as SupportsIndex, + TypedDict, runtime_checkable, ) else: from typing_extensions import ( # noqa: F401 Literal, + Protocol, SupportsIndex as SupportsIndex, TypedDict, - Protocol, runtime_checkable, ) -# if sys.version_info >= (3, 10): -# from typing import TypeGuard # noqa: F401 -# else: -# from typing_extensions import TypeGuard # noqa: F401 - -PathLike = Union[str, "os.PathLike[str]"] - if TYPE_CHECKING: - from git.repo import Repo from git.objects import Commit, Tree, TagObject, Blob + from git.repo import Repo - # from git.refs import SymbolicReference +PathLike = Union[str, "os.PathLike[str]"] +"""A :class:`str` (Unicode) based file or directory path.""" TBD = Any +"""Alias of :class:`~typing.Any`, when a type hint is meant to become more specific.""" + _T = TypeVar("_T") +"""Type variable used internally in GitPython.""" + +AnyGitObject = Union["Commit", "Tree", "TagObject", "Blob"] +"""Union of the :class:`~git.objects.base.Object`-based types that represent actual git +object types. + +As noted in :class:`~git.objects.base.Object`, which has further details, these are: + +* :class:`Blob ` +* :class:`Tree ` +* :class:`Commit ` +* :class:`TagObject ` + +Those GitPython classes represent the four git object types, per gitglossary(7): + +* "blob": https://git-scm.com/docs/gitglossary#def_blob_object +* "tree object": https://git-scm.com/docs/gitglossary#def_tree_object +* "commit object": https://git-scm.com/docs/gitglossary#def_commit_object +* "tag object": https://git-scm.com/docs/gitglossary#def_tag_object + +For more general information on git objects and their types as git understands them: + +* "object": https://git-scm.com/docs/gitglossary#def_object +* "object type": https://git-scm.com/docs/gitglossary#def_object_type + +:note: + See also the :class:`Tree_ish` and :class:`Commit_ish` unions. +""" Tree_ish = Union["Commit", "Tree"] -Commit_ish = Union["Commit", "TagObject", "Blob", "Tree"] -Lit_commit_ish = Literal["commit", "tag", "blob", "tree"] +"""Union of :class:`~git.objects.base.Object`-based types that are inherently tree-ish. + +See gitglossary(7) on "tree-ish": https://git-scm.com/docs/gitglossary#def_tree-ish + +:note: + This union comprises **only** the :class:`~git.objects.commit.Commit` and + :class:`~git.objects.tree.Tree` classes, **all** of whose instances are tree-ish. + This has been done because of the way GitPython uses it as a static type annotation. + + :class:`~git.objects.tag.TagObject`, some but not all of whose instances are + tree-ish (those representing git tag objects that ultimately resolve to a tree or + commit), is not covered as part of this union type. + +:note: + See also the :class:`AnyGitObject` union of all four classes corresponding to git + object types. +""" + +Commit_ish = Union["Commit", "TagObject"] +"""Union of :class:`~git.objects.base.Object`-based types that are sometimes commit-ish. + +See gitglossary(7) on "commit-ish": https://git-scm.com/docs/gitglossary#def_commit-ish + +:note: + :class:`~git.objects.commit.Commit` is the only class whose instances are all + commit-ish. This union type includes :class:`~git.objects.commit.Commit`, but also + :class:`~git.objects.tag.TagObject`, only **some** of whose instances are + commit-ish. Whether a particular :class:`~git.objects.tag.TagObject` peels + (recursively dereferences) to a commit can in general only be known at runtime. + +:note: + This is an inversion of the situation with :class:`Tree_ish`. This union is broader + than all commit-ish objects, while :class:`Tree_ish` is narrower than all tree-ish + objects. + +:note: + See also the :class:`AnyGitObject` union of all four classes corresponding to git + object types. +""" + +GitObjectTypeString = Literal["commit", "tag", "blob", "tree"] +"""Literal strings identifying git object types and the +:class:`~git.objects.base.Object`-based types that represent them. + +See the :attr:`Object.type ` attribute. These are its +values in :class:`~git.objects.base.Object` subclasses that represent git objects. These +literals therefore correspond to the types in the :class:`AnyGitObject` union. + +These are the same strings git itself uses to identify its four object types. See +gitglossary(7) on "object type": https://git-scm.com/docs/gitglossary#def_object_type +""" + +Lit_commit_ish = Literal["commit", "tag"] +"""Deprecated. Type of literal strings identifying sometimes-commitish git object types. + +Prior to a bugfix, this type had been defined more broadly. Any usage is in practice +ambiguous and likely to be incorrect. Instead of this type: + +* For the type of the string literals associated with :class:`Commit_ish`, use + ``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to + this type as currently defined. + +* For the type of all four string literals associated with :class:`AnyGitObject`, use + :class:`GitObjectTypeString`. That is equivalent to the old definition of this type + prior to the bugfix. +""" # Config_levels --------------------------------------------------------- Lit_config_levels = Literal["system", "global", "user", "repository"] +"""Type of literal strings naming git configuration levels. + +These strings relate to which file a git configuration variable is in. +""" + +ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]] +"""Static type of a tuple of the four strings representing configuration levels.""" # Progress parameter type alias ----------------------------------------- CallableProgress = Optional[Callable[[int, Union[str, float], Union[str, float, None], str], None]] +"""General type of a function or other callable used as a progress reporter for cloning. -# def is_config_level(inp: str) -> TypeGuard[Lit_config_levels]: -# # return inp in get_args(Lit_config_level) # only py >= 3.8 -# return inp in ("system", "user", "global", "repository") +This is the type of a function or other callable that reports the progress of a clone, +when passed as a ``progress`` argument to :meth:`Repo.clone ` +or :meth:`Repo.clone_from `. +:note: + Those :meth:`~git.repo.base.Repo.clone` and :meth:`~git.repo.base.Repo.clone_from` + methods also accept :meth:`~git.util.RemoteProgress` instances, including instances + of its :meth:`~git.util.CallableRemoteProgress` subclass. -ConfigLevels_Tup = Tuple[Literal["system"], Literal["user"], Literal["global"], Literal["repository"]] +:note: + Unlike objects that match this type, :meth:`~git.util.RemoteProgress` instances are + not directly callable, not even when they are instances of + :meth:`~git.util.CallableRemoteProgress`, which wraps a callable and forwards + information to it but is not itself callable. + +:note: + This type also allows ``None``, for cloning without reporting progress. +""" # ----------------------------------------------------------------------------------- def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None: - """For use in exhaustive checking of literal or Enum in if/else chain. + """For use in exhaustive checking of a literal or enum in if/else chains. - Should only be reached if all members not handled OR attempt to pass non-members through chain. + A call to this function should only be reached if not all members are handled, or if + an attempt is made to pass non-members through the chain. - If all members handled, type is Empty. Otherwise, will cause mypy error. + :param inp: + If all members are handled, the argument for `inp` will have the + :class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will + mismatch and cause a mypy error. - If non-members given, should cause mypy error at variable creation. + :param raise_error: + If ``True``, will also raise :class:`ValueError` with a general "unhandled + literal" message, or the exception object passed as `exc`. - If raise_error is True, will also raise AssertionError or the Exception passed to exc. + :param exc: + It not ``None``, this should be an already-constructed exception object, to be + raised if `raise_error` is ``True``. """ if raise_error: if exc is None: - raise ValueError(f"An unhandled Literal ({inp}) in an if/else chain was found") + raise ValueError(f"An unhandled literal ({inp!r}) in an if/else chain was found") else: raise exc class Files_TD(TypedDict): + """Dictionary with stat counts for the diff of a particular file. + + For the :class:`~git.util.Stats.files` attribute of :class:`~git.util.Stats` + objects. + """ + insertions: int deletions: int lines: int class Total_TD(TypedDict): + """Dictionary with total stats from any number of files. + + For the :class:`~git.util.Stats.total` attribute of :class:`~git.util.Stats` + objects. + """ + insertions: int deletions: int lines: int @@ -103,15 +231,21 @@ class Total_TD(TypedDict): class HSH_TD(TypedDict): + """Dictionary carrying the same information as a :class:`~git.util.Stats` object.""" + total: Total_TD files: Dict[PathLike, Files_TD] @runtime_checkable class Has_Repo(Protocol): + """Protocol for having a :attr:`repo` attribute, the repository to operate on.""" + repo: "Repo" @runtime_checkable class Has_id_attribute(Protocol): + """Protocol for having :attr:`_id_attribute_` used in iteration and traversal.""" + _id_attribute_: str diff --git a/git/util.py b/git/util.py index 27751f687..2a9dd10a9 100644 --- a/git/util.py +++ b/git/util.py @@ -107,19 +107,12 @@ _logger = logging.getLogger(__name__) -def _read_win_env_flag(name: str, default: bool) -> bool: - """Read a boolean flag from an environment variable on Windows. +def _read_env_flag(name: str, default: bool) -> bool: + """Read a boolean flag from an environment variable. :return: - On Windows, the flag, or the `default` value if absent or ambiguous. - On all other operating systems, ``False``. - - :note: - This only accesses the environment on Windows. + The flag, or the `default` value if absent or ambiguous. """ - if os.name != "nt": - return False - try: value = os.environ[name] except KeyError: @@ -140,6 +133,19 @@ def _read_win_env_flag(name: str, default: bool) -> bool: return default +def _read_win_env_flag(name: str, default: bool) -> bool: + """Read a boolean flag from an environment variable on Windows. + + :return: + On Windows, the flag, or the `default` value if absent or ambiguous. + On all other operating systems, ``False``. + + :note: + This only accesses the environment on Windows. + """ + return sys.platform == "win32" and _read_env_flag(name, default) + + #: We need an easy way to see if Appveyor TCs start failing, #: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy, #: till then, we wish to hide them. @@ -223,7 +229,7 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None: raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex raise - if os.name != "nt": + if sys.platform != "win32": shutil.rmtree(path) elif sys.version_info >= (3, 12): shutil.rmtree(path, onexc=handler) @@ -235,7 +241,7 @@ def rmfile(path: PathLike) -> None: """Ensure file deleted also on *Windows* where read-only files need special treatment.""" if osp.isfile(path): - if os.name == "nt": + if sys.platform == "win32": os.chmod(path, 0o777) os.remove(path) @@ -276,7 +282,7 @@ def join_path(a: PathLike, *p: PathLike) -> PathLike: return path -if os.name == "nt": +if sys.platform == "win32": def to_native_path_windows(path: PathLike) -> PathLike: path = str(path) @@ -328,7 +334,7 @@ def _get_exe_extensions() -> Sequence[str]: PATHEXT = os.environ.get("PATHEXT", None) if PATHEXT: return tuple(p.upper() for p in PATHEXT.split(os.pathsep)) - elif os.name == "nt": + elif sys.platform == "win32": return (".BAT", "COM", ".EXE") else: return () @@ -354,7 +360,9 @@ def is_exec(fpath: str) -> bool: return ( osp.isfile(fpath) and os.access(fpath, os.X_OK) - and (os.name != "nt" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts)) + and ( + sys.platform != "win32" or not winprog_exts or any(fpath.upper().endswith(ext) for ext in winprog_exts) + ) ) progs = [] @@ -440,23 +448,7 @@ def decygpath(path: PathLike) -> str: _is_cygwin_cache: Dict[str, Optional[bool]] = {} -@overload -def is_cygwin_git(git_executable: None) -> Literal[False]: ... - - -@overload -def is_cygwin_git(git_executable: PathLike) -> bool: ... - - -def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool: - if os.name == "nt": - # This is Windows-native Python, since Cygwin has os.name == "posix". - return False - - if git_executable is None: - return False - - git_executable = str(git_executable) +def _is_cygwin_git(git_executable: str) -> bool: is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool] if is_cygwin is None: is_cygwin = False @@ -479,6 +471,23 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool: return is_cygwin +@overload +def is_cygwin_git(git_executable: None) -> Literal[False]: ... + + +@overload +def is_cygwin_git(git_executable: PathLike) -> bool: ... + + +def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool: + if sys.platform == "win32": # TODO: See if we can use `sys.platform != "cygwin"`. + return False + elif git_executable is None: + return False + else: + return _is_cygwin_git(str(git_executable)) + + def get_user_id() -> str: """:return: String identifying the currently active system user as ``name@node``""" return "%s@%s" % (getpass.getuser(), platform.node()) @@ -505,10 +514,10 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ if isinstance(p, pathlib.Path): return p.resolve() try: - p = osp.expanduser(p) # type: ignore + p = osp.expanduser(p) # type: ignore[arg-type] if expand_vars: - p = osp.expandvars(p) # type: ignore - return osp.normpath(osp.abspath(p)) # type: ignore + p = osp.expandvars(p) + return osp.normpath(osp.abspath(p)) except Exception: return None @@ -732,7 +741,14 @@ def update( class CallableRemoteProgress(RemoteProgress): - """An implementation forwarding updates to any callable.""" + """A :class:`RemoteProgress` implementation forwarding updates to any callable. + + :note: + Like direct instances of :class:`RemoteProgress`, instances of this + :class:`CallableRemoteProgress` class are not themselves directly callable. + Rather, instances of this class wrap a callable and forward to it. This should + therefore not be confused with :class:`git.types.CallableProgress`. + """ __slots__ = ("_callable",) @@ -1176,7 +1192,7 @@ def __getattr__(self, attr: str) -> T_IterableObj: # END for each item return list.__getattribute__(self, attr) - def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore + def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore[override] assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str" if isinstance(index, int): diff --git a/pyproject.toml b/pyproject.toml index 57ae01058..1dc1e6aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,13 +20,12 @@ testpaths = "test" # Space separated list of paths from root e.g test tests doc # filterwarnings ignore::WarningType # ignores those warnings [tool.mypy] -python_version = "3.7" +python_version = "3.8" disallow_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true -# warn_unused_ignores = true +warn_unused_ignores = true warn_unreachable = true -show_error_codes = true implicit_reexport = true # strict = true # TODO: Remove when 'gitdb' is fully annotated. diff --git a/test/lib/helper.py b/test/lib/helper.py index 27586c2b0..45a778b7d 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -178,7 +178,7 @@ def git_daemon_launched(base_path, ip, port): gd = None try: - if os.name == "nt": + if sys.platform == "win32": # On MINGW-git, daemon exists in Git\mingw64\libexec\git-core\, # but if invoked as 'git daemon', it detaches from parent `git` cmd, # and then CANNOT DIE! @@ -202,7 +202,7 @@ def git_daemon_launched(base_path, ip, port): as_process=True, ) # Yes, I know... fortunately, this is always going to work if sleep time is just large enough. - time.sleep(1.0 if os.name == "nt" else 0.5) + time.sleep(1.0 if sys.platform == "win32" else 0.5) except Exception as ex: msg = textwrap.dedent( """ @@ -406,7 +406,7 @@ class VirtualEnvironment: __slots__ = ("_env_dir",) def __init__(self, env_dir, *, with_pip): - if os.name == "nt": + if sys.platform == "win32": self._env_dir = osp.realpath(env_dir) venv.create(self.env_dir, symlinks=False, with_pip=with_pip) else: @@ -441,7 +441,7 @@ def sources(self): return os.path.join(self.env_dir, "src") def _executable(self, basename): - if os.name == "nt": + if sys.platform == "win32": path = osp.join(self.env_dir, "Scripts", basename + ".exe") else: path = osp.join(self.env_dir, "bin", basename) diff --git a/test/test_base.py b/test/test_base.py index ef7486e86..e477b4837 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -130,7 +130,7 @@ def test_add_unicode(self, rw_repo): with open(file_path, "wb") as fp: fp.write(b"something") - if os.name == "nt": + if sys.platform == "win32": # On Windows, there is no way this works, see images on: # https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897 # Therefore, it must be added using the Python implementation. diff --git a/test/test_config.py b/test/test_config.py index 4843d91eb..ac19a7fa8 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -7,6 +7,7 @@ import io import os import os.path as osp +import sys from unittest import mock import pytest @@ -238,7 +239,7 @@ def check_test_value(cr, value): check_test_value(cr, tv) @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")', raises=AssertionError, ) diff --git a/test/test_diff.py b/test/test_diff.py index ed82b1bbd..96fbc60e3 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -4,15 +4,15 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ import gc -import os import os.path as osp import shutil +import sys import tempfile import ddt import pytest -from git import NULL_TREE, Diff, DiffIndex, GitCommandError, Repo, Submodule +from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule from git.cmd import Git from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory @@ -309,7 +309,7 @@ def test_diff_with_spaces(self): self.assertEqual(diff_index[0].b_path, "file with spaces", repr(diff_index[0].b_path)) @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason='"Access is denied" when tearDown calls shutil.rmtree', raises=PermissionError, ) @@ -352,7 +352,7 @@ def test_diff_submodule(self): self.assertIsInstance(diff.b_blob.size, int) def test_diff_interface(self): - # Test a few variations of the main diff routine. + """Test a few variations of the main diff routine.""" assertion_map = {} for i, commit in enumerate(self.rorepo.iter_commits("0.1.6", max_count=2)): diff_item = commit @@ -360,7 +360,7 @@ def test_diff_interface(self): diff_item = commit.tree # END use tree every second item - for other in (None, NULL_TREE, commit.Index, commit.parents[0]): + for other in (None, NULL_TREE, commit.INDEX, commit.parents[0]): for paths in (None, "CHANGES", ("CHANGES", "lib")): for create_patch in range(2): diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch) @@ -406,10 +406,22 @@ def test_diff_interface(self): diff_index = c.diff(cp, ["does/not/exist"]) self.assertEqual(len(diff_index), 0) + def test_diff_interface_stability(self): + """Test that the Diffable.Index redefinition should not break compatibility.""" + self.assertIs( + Diffable.Index, + Diffable.INDEX, + "The old and new class attribute names must be aliases.", + ) + self.assertIs( + type(Diffable.INDEX).__eq__, + object.__eq__, + "Equality comparison must be reference-based.", + ) + @with_rw_directory def test_rename_override(self, rw_dir): - """Test disabling of diff rename detection""" - + """Test disabling of diff rename detection.""" # Create and commit file_a.txt. repo = Repo.init(rw_dir) file_a = osp.join(rw_dir, "file_a.txt") diff --git a/test/test_git.py b/test/test_git.py index e1a8bda5e..dae0f6a39 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -74,7 +74,7 @@ def _fake_git(*version_info): fake_output = f"git version {fake_version} (fake)" with tempfile.TemporaryDirectory() as tdir: - if os.name == "nt": + if sys.platform == "win32": fake_git = Path(tdir, "fake-git.cmd") script = f"@echo {fake_output}\n" fake_git.write_text(script, encoding="utf-8") @@ -215,7 +215,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case): repo = Repo.init(rw_dir) - if os.name == "nt": + if sys.platform == "win32": # Copy an actual binary executable that is not git. (On Windows, running # "hostname" only displays the hostname, it never tries to change it.) other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe") @@ -228,7 +228,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case): os.chmod(impostor_path, 0o755) if use_shell_impostor: - shell_name = "cmd.exe" if os.name == "nt" else "sh" + shell_name = "cmd.exe" if sys.platform == "win32" else "sh" shutil.copy(impostor_path, Path(rw_dir, shell_name)) with contextlib.ExitStack() as stack: @@ -245,7 +245,7 @@ def test_it_executes_git_not_from_cwd(self, rw_dir, case): self.assertRegex(output, r"^git version\b") @skipUnless( - os.name == "nt", + sys.platform == "win32", "The regression only affected Windows, and this test logic is OS-specific.", ) def test_it_avoids_upcasing_unrelated_environment_variable_names(self): @@ -667,7 +667,7 @@ def test_successful_default_refresh_invalidates_cached_version_info(self): stack.enter_context(mock.patch.dict(os.environ, {"PATH": new_path_var})) stack.enter_context(_patch_out_env("GIT_PYTHON_GIT_EXECUTABLE")) - if os.name == "nt": + if sys.platform == "win32": # On Windows, use a shell so "git" finds "git.cmd". (In the infrequent # case that this effect is desired in production code, it should not be # done with this technique. USE_SHELL=True is less secure and reliable, diff --git a/test/test_index.py b/test/test_index.py index fa64b82a2..622e7ca9a 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -14,6 +14,7 @@ import shutil from stat import S_ISLNK, ST_MODE import subprocess +import sys import tempfile import ddt @@ -124,7 +125,7 @@ def check(cls): in System32; Popen finds it even if a shell would run another one, as on CI. (Without WSL, System32 may still have bash.exe; users sometimes put it there.) """ - if os.name != "nt": + if sys.platform != "win32": return cls.Inapplicable() try: @@ -561,7 +562,7 @@ def _count_existing(self, repo, files): # END num existing helper @pytest.mark.xfail( - os.name == "nt" and Git().config("core.symlinks") == "true", + sys.platform == "win32" and Git().config("core.symlinks") == "true", reason="Assumes symlinks are not created on Windows and opens a symlink to a nonexistent target.", raises=FileNotFoundError, ) @@ -754,7 +755,7 @@ def mixed_iterator(): self.assertNotEqual(entries[0].hexsha, null_hex_sha) # Add symlink. - if os.name != "nt": + if sys.platform != "win32": for target in ("/etc/nonexisting", "/etc/passwd", "/etc"): basename = "my_real_symlink" @@ -812,7 +813,7 @@ def mixed_iterator(): index.checkout(fake_symlink_path) # On Windows, we currently assume we will never get symlinks. - if os.name == "nt": + if sys.platform == "win32": # Symlinks should contain the link as text (which is what a # symlink actually is). with open(fake_symlink_path, "rt") as fd: @@ -1043,7 +1044,7 @@ def test_run_commit_hook(self, rw_repo): def test_hook_uses_shell_not_from_cwd(self, rw_dir, case): (chdir_to_repo,) = case - shell_name = "bash.exe" if os.name == "nt" else "sh" + shell_name = "bash.exe" if sys.platform == "win32" else "sh" maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext() repo = Repo.init(rw_dir) diff --git a/test/test_remote.py b/test/test_remote.py index 35af8172d..f84452deb 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -4,10 +4,10 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ import gc -import os import os.path as osp from pathlib import Path import random +import sys import tempfile from unittest import skipIf @@ -769,7 +769,7 @@ def test_create_remote_unsafe_url(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value", raises=AssertionError, ) @@ -832,7 +832,7 @@ def test_fetch_unsafe_options(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_fetch_unsafe_options must be adjusted in the " @@ -900,7 +900,7 @@ def test_pull_unsafe_options(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_pull_unsafe_options must be adjusted in the " @@ -974,7 +974,7 @@ def test_push_unsafe_options(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_push_unsafe_options must be adjusted in the " diff --git a/test/test_repo.py b/test/test_repo.py index 30a44b6c1..238f94712 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -309,7 +309,7 @@ def test_clone_unsafe_options(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_clone_unsafe_options must be adjusted in the " @@ -388,7 +388,7 @@ def test_clone_from_unsafe_options(self, rw_repo): assert not tmp_file.exists() @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=( "File not created. A separate Windows command may be needed. This and the " "currently passing test test_clone_from_unsafe_options must be adjusted in the " @@ -1389,7 +1389,7 @@ def test_do_not_strip_newline_in_stdout(self, rw_dir): self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), "hello\n") @pytest.mark.xfail( - os.name == "nt", + sys.platform == "win32", reason=R"fatal: could not create leading directories of '--upload-pack=touch C:\Users\ek\AppData\Local\Temp\tmpnantqizc\pwn': Invalid argument", # noqa: E501 raises=GitCommandError, ) diff --git a/test/test_submodule.py b/test/test_submodule.py index 68164729b..ee7795dbb 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -987,7 +987,7 @@ def test_rename(self, rwdir): # This is needed to work around a PermissionError on Windows, resembling others, # except new in Python 3.12. (*Maybe* this could be due to changes in CPython's # garbage collector detailed in https://github.com/python/cpython/issues/97922.) - if os.name == "nt" and sys.version_info >= (3, 12): + if sys.platform == "win32" and sys.version_info >= (3, 12): gc.collect() new_path = "renamed/myname" @@ -1071,7 +1071,7 @@ def test_branch_renames(self, rw_dir): assert sm_mod.commit() == sm_pfb.commit, "Now head should have been reset" assert sm_mod.head.ref.name == sm_pfb.name - @skipUnless(os.name == "nt", "Specifically for Windows.") + @skipUnless(sys.platform == "win32", "Specifically for Windows.") def test_to_relative_path_with_super_at_root_drive(self): class Repo: working_tree_dir = "D:\\" diff --git a/test/test_util.py b/test/test_util.py index 824b3ab3d..369896581 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -149,7 +149,7 @@ def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors): mocker.patch.object(pathlib.Path, "chmod") @pytest.mark.skipif( - os.name != "nt", + sys.platform != "win32", reason="PermissionError is only ever wrapped on Windows", ) def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir): @@ -168,7 +168,7 @@ def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir): "hide_windows_known_errors", [ pytest.param(False), - pytest.param(True, marks=pytest.mark.skipif(os.name == "nt", reason="We would wrap on Windows")), + pytest.param(True, marks=pytest.mark.skipif(sys.platform == "win32", reason="We would wrap on Windows")), ], ) def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors): @@ -214,7 +214,7 @@ def _run_parse(name, value): return ast.literal_eval(output) @pytest.mark.skipif( - os.name != "nt", + sys.platform != "win32", reason="These environment variables are only used on Windows.", ) @pytest.mark.parametrize( @@ -410,7 +410,7 @@ def test_blocking_lock_file(self): elapsed = time.time() - start extra_time = 0.02 - if os.name == "nt" or sys.platform == "cygwin": + if sys.platform in {"win32", "cygwin"}: extra_time *= 6 # Without this, we get indeterministic failures on Windows. elif sys.platform == "darwin": extra_time *= 18 # The situation on macOS is similar, but with more delay.