summaryrefslogtreecommitdiff
path: root/git/util.py
diff options
context:
space:
mode:
Diffstat (limited to 'git/util.py')
-rw-r--r--git/util.py235
1 files changed, 171 insertions, 64 deletions
diff --git a/git/util.py b/git/util.py
index af499028..4f82219e 100644
--- a/git/util.py
+++ b/git/util.py
@@ -4,6 +4,10 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+from abc import abstractmethod
+from .exc import InvalidGitRepositoryError
+import os.path as osp
+from .compat import is_win
import contextlib
from functools import wraps
import getpass
@@ -18,15 +22,32 @@ from sys import maxsize
import time
from unittest import SkipTest
from urllib.parse import urlsplit, urlunsplit
+import warnings
+
+# from git.objects.util import Traversable
# typing ---------------------------------------------------------
-from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, List,
- NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING)
+from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List,
+ Optional, Pattern, Sequence, Tuple, TypeVar, Union, cast,
+ TYPE_CHECKING, overload, )
+
+import pathlib
+
if TYPE_CHECKING:
from git.remote import Remote
from git.repo.base import Repo
-from .types import PathLike, TBD
+ from git.config import GitConfigParser, SectionConstraint
+ from git import Git
+ # from git.objects.base import IndexObject
+
+
+from .types import (Literal, SupportsIndex, Protocol, runtime_checkable, # because behind py version guards
+ PathLike, HSH_TD, Total_TD, Files_TD, # aliases
+ Has_id_attribute)
+
+T_IterableObj = TypeVar('T_IterableObj', bound=Union['IterableObj', 'Has_id_attribute'], covariant=True)
+# So IterableList[Head] is subtype of IterableList[IterableObj]
# ---------------------------------------------------------------------
@@ -43,18 +64,13 @@ from gitdb.util import ( # NOQA @IgnorePep8
hex_to_bin, # @UnusedImport
)
-from .compat import is_win
-import os.path as osp
-
-from .exc import InvalidGitRepositoryError
-
# NOTE: Some of the unused imports might be used/imported by others.
# Handle once test-cases are back up and running.
# Most of these are unused here, but are for use by git-python modules so these
# don't see gitdb all the time. Flake of course doesn't like it.
__all__ = ["stream_copy", "join_path", "to_native_path_linux",
- "join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList",
+ "join_path_native", "Stats", "IndexFileSHA1Writer", "IterableObj", "IterableList",
"BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists',
'RemoteProgress', 'CallableRemoteProgress', 'rmtree', 'unbare_repo',
'HIDE_WINDOWS_KNOWN_ERRORS']
@@ -70,15 +86,17 @@ log = logging.getLogger(__name__)
HIDE_WINDOWS_KNOWN_ERRORS = is_win and os.environ.get('HIDE_WINDOWS_KNOWN_ERRORS', True)
HIDE_WINDOWS_FREEZE_ERRORS = is_win and os.environ.get('HIDE_WINDOWS_FREEZE_ERRORS', True)
-#{ Utility Methods
+# { Utility Methods
+T = TypeVar('T')
-def unbare_repo(func: Callable) -> Callable:
+
+def unbare_repo(func: Callable[..., T]) -> Callable[..., T]:
"""Methods with this decorator raise InvalidGitRepositoryError if they
encounter a bare repository"""
@wraps(func)
- def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> TBD:
+ def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> T:
if self.repo.bare:
raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__)
# END bare method
@@ -104,7 +122,7 @@ def rmtree(path: PathLike) -> None:
:note: we use shutil rmtree but adjust its behaviour to see whether files that
couldn't be deleted are read-only. Windows will not remove them in that case"""
- def onerror(func: Callable, path: PathLike, exc_info: TBD) -> None:
+ def onerror(func: Callable, path: PathLike, exc_info: str) -> None:
# Is the error an access error ?
os.chmod(path, stat.S_IWUSR)
@@ -165,7 +183,7 @@ if is_win:
path = str(path)
return path.replace('/', '\\')
- def to_native_path_linux(path: PathLike) -> PathLike:
+ def to_native_path_linux(path: PathLike) -> str:
path = str(path)
return path.replace('\\', '/')
@@ -173,8 +191,9 @@ if is_win:
to_native_path = to_native_path_windows
else:
# no need for any work on linux
- def to_native_path_linux(path: PathLike) -> PathLike:
- return path
+ def to_native_path_linux(path: PathLike) -> str:
+ return str(path)
+
to_native_path = to_native_path_linux
@@ -230,9 +249,9 @@ def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
return progs
-def _cygexpath(drive: Optional[str], path: PathLike) -> str:
+def _cygexpath(drive: Optional[str], path: str) -> str:
if osp.isabs(path) and not drive:
- ## Invoked from `cygpath()` directly with `D:Apps\123`?
+ # Invoked from `cygpath()` directly with `D:Apps\123`?
# It's an error, leave it alone just slashes)
p = path # convert to str if AnyPath given
else:
@@ -249,9 +268,9 @@ def _cygexpath(drive: Optional[str], path: PathLike) -> str:
return p_str.replace('\\', '/')
-_cygpath_parsers = (
- ## See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
- ## and: https://www.cygwin.com/cygwin-ug-net/using.html#unc-paths
+_cygpath_parsers: Tuple[Tuple[Pattern[str], Callable, bool], ...] = (
+ # See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+ # and: https://www.cygwin.com/cygwin-ug-net/using.html#unc-paths
(re.compile(r"\\\\\?\\UNC\\([^\\]+)\\([^\\]+)(?:\\(.*))?"),
(lambda server, share, rest_path: '//%s/%s/%s' % (server, share, rest_path.replace('\\', '/'))),
False
@@ -276,12 +295,13 @@ _cygpath_parsers = (
(lambda url: url),
False
),
-) # type: Tuple[Tuple[Pattern[str], Callable, bool], ...]
+)
-def cygpath(path: PathLike) -> PathLike:
+def cygpath(path: str) -> str:
"""Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment."""
- path = str(path) # ensure is str and not AnyPath
+ path = str(path) # ensure is str and not AnyPath.
+ # Fix to use Paths when 3.5 dropped. or to be just str if only for urls?
if not path.startswith(('/cygdrive', '//')):
for regex, parser, recurse in _cygpath_parsers:
match = regex.match(path)
@@ -311,14 +331,26 @@ def decygpath(path: PathLike) -> str:
#: Store boolean flags denoting if a specific Git executable
#: is from a Cygwin installation (since `cache_lru()` unsupported on PY2).
-_is_cygwin_cache = {} # type: Dict[str, Optional[bool]]
+_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 not is_win:
return False
- #from subprocess import check_output
+ if git_executable is None:
+ return False
+
git_executable = str(git_executable)
is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool]
if is_cygwin is None:
@@ -329,7 +361,7 @@ def is_cygwin_git(git_executable: PathLike) -> bool:
res = py_where(git_executable)
git_dir = osp.dirname(res[0]) if res else ""
- ## Just a name given, not a real path.
+ # Just a name given, not a real path.
uname_cmd = osp.join(git_dir, 'uname')
process = subprocess.Popen([uname_cmd], stdout=subprocess.PIPE,
universal_newlines=True)
@@ -348,23 +380,36 @@ def get_user_id() -> str:
return "%s@%s" % (getpass.getuser(), platform.node())
-def finalize_process(proc: TBD, **kwargs: Any) -> None:
+def finalize_process(proc: Union[subprocess.Popen, 'Git.AutoInterrupt'], **kwargs: Any) -> None:
"""Wait for the process (clone, fetch, pull or push) and handle its errors accordingly"""
- ## TODO: No close proc-streams??
+ # TODO: No close proc-streams??
proc.wait(**kwargs)
-def expand_path(p: PathLike, expand_vars: bool = True) -> Optional[PathLike]:
+@overload
+def expand_path(p: None, expand_vars: bool = ...) -> None:
+ ...
+
+
+@overload
+def expand_path(p: PathLike, expand_vars: bool = ...) -> str:
+ # improve these overloads when 3.5 dropped
+ ...
+
+
+def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[PathLike]:
+ if isinstance(p, pathlib.Path):
+ return p.resolve()
try:
- p = osp.expanduser(p)
+ p = osp.expanduser(p) # type: ignore
if expand_vars:
- p = osp.expandvars(p)
- return osp.normpath(osp.abspath(p))
+ p = osp.expandvars(p) # type: ignore
+ return osp.normpath(osp.abspath(p)) # type: ignore
except Exception:
return None
-def remove_password_if_present(cmdline):
+def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
"""
Parse any command line argument and if on of the element is an URL with a
password, replace it by stars (in-place).
@@ -391,9 +436,9 @@ def remove_password_if_present(cmdline):
return new_cmdline
-#} END utilities
+# } END utilities
-#{ Classes
+# { Classes
class RemoteProgress(object):
@@ -401,7 +446,7 @@ class RemoteProgress(object):
Handler providing an interface to parse progress information emitted by git-push
and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.
"""
- _num_op_codes = 9
+ _num_op_codes: int = 9
BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT = \
[1 << x for x in range(_num_op_codes)]
STAGE_MASK = BEGIN | END
@@ -418,10 +463,10 @@ class RemoteProgress(object):
re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")
def __init__(self) -> None:
- self._seen_ops = [] # type: List[TBD]
- self._cur_line = None # type: Optional[str]
- self.error_lines = [] # type: List[str]
- self.other_lines = [] # type: List[str]
+ self._seen_ops: List[int] = []
+ self._cur_line: Optional[str] = None
+ self.error_lines: List[str] = []
+ self.other_lines: List[str] = []
def _parse_progress_line(self, line: AnyStr) -> None:
"""Parse progress information from the given line as retrieved by git-push
@@ -429,7 +474,7 @@ class RemoteProgress(object):
- Lines that do not contain progress info are stored in :attr:`other_lines`.
- Lines that seem to contain an error (i.e. start with error: or fatal:) are stored
- in :attr:`error_lines`."""
+ in :attr:`error_lines`."""
# handle
# Counting objects: 4, done.
# Compressing objects: 50% (1/2)
@@ -441,7 +486,7 @@ class RemoteProgress(object):
line_str = line
self._cur_line = line_str
- if self.error_lines or self._cur_line.startswith(('error:', 'fatal:')):
+ if self._cur_line.startswith(('error:', 'fatal:')):
self.error_lines.append(self._cur_line)
return
@@ -639,7 +684,8 @@ class Actor(object):
# END handle name/email matching
@classmethod
- def _main_actor(cls, env_name: str, env_email: str, config_reader: Optional[TBD] = None) -> 'Actor':
+ def _main_actor(cls, env_name: str, env_email: str,
+ config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor':
actor = Actor('', '')
user_id = None # We use this to avoid multiple calls to getpass.getuser()
@@ -668,7 +714,7 @@ class Actor(object):
return actor
@classmethod
- def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor':
+ def committer(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor':
"""
:return: Actor instance corresponding to the configured committer. It behaves
similar to the git implementation, such that the environment will override
@@ -679,7 +725,7 @@ class Actor(object):
return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader)
@classmethod
- def author(cls, config_reader: Optional[TBD] = None) -> 'Actor':
+ def author(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor':
"""Same as committer(), but defines the main author. It may be specified in the environment,
but defaults to the committer"""
return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
@@ -713,7 +759,7 @@ class Stats(object):
files = number of changed files as int"""
__slots__ = ("total", "files")
- def __init__(self, total: Dict[str, Dict[str, int]], files: Dict[str, Dict[str, int]]):
+ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]):
self.total = total
self.files = files
@@ -722,9 +768,13 @@ class Stats(object):
"""Create a Stat object from output retrieved by git-diff.
:return: git.Stat"""
- hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0},
- 'files': {}
- } # type: Dict[str, Dict[str, TBD]] ## need typeddict or refactor for mypy
+
+ hsh: HSH_TD = {'total': {'insertions': 0,
+ 'deletions': 0,
+ 'lines': 0,
+ 'files': 0},
+ 'files': {}
+ }
for line in text.splitlines():
(raw_insertions, raw_deletions, filename) = line.split("\t")
insertions = raw_insertions != '-' and int(raw_insertions) or 0
@@ -733,9 +783,10 @@ class Stats(object):
hsh['total']['deletions'] += deletions
hsh['total']['lines'] += insertions + deletions
hsh['total']['files'] += 1
- hsh['files'][filename.strip()] = {'insertions': insertions,
- 'deletions': deletions,
- 'lines': insertions + deletions}
+ files_dict: Files_TD = {'insertions': insertions,
+ 'deletions': deletions,
+ 'lines': insertions + deletions}
+ hsh['files'][filename.strip()] = files_dict
return Stats(hsh['total'], hsh['files'])
@@ -891,7 +942,7 @@ class BlockingLockFile(LockFile):
# END endless loop
-class IterableList(list):
+class IterableList(List[T_IterableObj]):
"""
List of iterable objects allowing to query an object by id or by named index::
@@ -901,6 +952,9 @@ class IterableList(list):
heads['master']
heads[0]
+ Iterable parent objects = [Commit, SubModule, Reference, FetchInfo, PushInfo]
+ Iterable via inheritance = [Head, TagReference, RemoteReference]
+ ]
It requires an id_attribute name to be set which will be queried from its
contained items to have a means for comparison.
@@ -909,7 +963,7 @@ class IterableList(list):
can be left out."""
__slots__ = ('_id_attr', '_prefix')
- def __new__(cls, id_attr: str, prefix: str = '') -> 'IterableList':
+ def __new__(cls, id_attr: str, prefix: str = '') -> 'IterableList[IterableObj]':
return super(IterableList, cls).__new__(cls)
def __init__(self, id_attr: str, prefix: str = '') -> None:
@@ -934,7 +988,7 @@ class IterableList(list):
return False
# END handle membership
- def __getattr__(self, attr: str) -> Any:
+ def __getattr__(self, attr: str) -> T_IterableObj:
attr = self._prefix + attr
for item in self:
if getattr(item, self._id_attr) == attr:
@@ -942,7 +996,10 @@ class IterableList(list):
# END for each item
return list.__getattribute__(self, attr)
- def __getitem__(self, index: Union[int, slice, str]) -> Any:
+ def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> T_IterableObj: # type: ignore
+
+ assert isinstance(index, (int, str, slice)), "Index of IterableList should be an int or str"
+
if isinstance(index, int):
return list.__getitem__(self, index)
elif isinstance(index, slice):
@@ -954,12 +1011,13 @@ class IterableList(list):
raise IndexError("No item found with id %r" % (self._prefix + index)) from e
# END handle getattr
- def __delitem__(self, index: Union[int, str, slice]) -> None:
+ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None:
+
+ assert isinstance(index, (int, str)), "Index of IterableList should be an int or str"
delindex = cast(int, index)
if not isinstance(index, int):
delindex = -1
- assert not isinstance(index, slice)
name = self._prefix + index
for i, item in enumerate(self):
if getattr(item, self._id_attr) == name:
@@ -974,7 +1032,20 @@ class IterableList(list):
list.__delitem__(self, delindex)
-class Iterable(object):
+class IterableClassWatcher(type):
+ """ Metaclass that watches """
+ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None:
+ for base in bases:
+ if type(base) == IterableClassWatcher:
+ warnings.warn(f"GitPython Iterable subclassed by {name}. "
+ "Iterable is deprecated due to naming clash since v3.1.18"
+ " and will be removed in 3.1.20, "
+ "Use IterableObj instead \n",
+ DeprecationWarning,
+ stacklevel=2)
+
+
+class Iterable(metaclass=IterableClassWatcher):
"""Defines an interface for iterable items which is to assure a uniform
way to retrieve and iterate items within the git repository"""
@@ -982,8 +1053,9 @@ class Iterable(object):
_id_attribute_ = "attribute that most suitably identifies your instance"
@classmethod
- def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> 'IterableList':
+ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Any:
"""
+ Deprecated, use IterableObj instead.
Find all items of this type - subclasses can specify args and kwargs differently.
If no args are given, subclasses are obliged to return all items if no additional
arguments arg given.
@@ -991,17 +1063,52 @@ class Iterable(object):
:note: Favor the iter_items method as it will
:return:list(Item,...) list of item instances"""
- out_list = IterableList(cls._id_attribute_)
+ out_list: Any = IterableList(cls._id_attribute_)
out_list.extend(cls.iter_items(repo, *args, **kwargs))
return out_list
@classmethod
- def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> NoReturn:
+ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Any:
+ # return typed to be compatible with subtypes e.g. Remote
"""For more information about the arguments, see list_items
:return: iterator yielding Items"""
raise NotImplementedError("To be implemented by Subclass")
-#} END classes
+
+@runtime_checkable
+class IterableObj(Protocol):
+ """Defines an interface for iterable items which is to assure a uniform
+ way to retrieve and iterate items within the git repository
+
+ Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]"""
+
+ __slots__ = ()
+ _id_attribute_: str
+
+ @classmethod
+ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]:
+ """
+ Find all items of this type - subclasses can specify args and kwargs differently.
+ If no args are given, subclasses are obliged to return all items if no additional
+ arguments arg given.
+
+ :note: Favor the iter_items method as it will
+
+ :return:list(Item,...) list of item instances"""
+ out_list: IterableList = IterableList(cls._id_attribute_)
+ out_list.extend(cls.iter_items(repo, *args, **kwargs))
+ return out_list
+
+ @classmethod
+ @abstractmethod
+ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any
+ ) -> Iterator[T_IterableObj]: # Iterator[T_IterableObj]:
+ # return typed to be compatible with subtypes e.g. Remote
+ """For more information about the arguments, see list_items
+ :return: iterator yielding Items"""
+ raise NotImplementedError("To be implemented by Subclass")
+
+# } END classes
class NullHandler(logging.Handler):