From ad4079dde47ce721e7652f56a81a28063052a166 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 28 Feb 2021 20:56:27 +0000 Subject: [PATCH 0001/1849] add types to base.py and fun.py --- git/repo/base.py | 271 ++++++++++++++++++++++++++++------------------- git/repo/fun.py | 37 ++++--- 2 files changed, 187 insertions(+), 121 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index 8f1ef0a6e..253631063 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -4,7 +4,11 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -from collections import namedtuple + +from git.objects.tag import TagObject +from git.objects.blob import Blob +from git.objects.tree import Tree +from git.refs.symbolic import SymbolicReference import logging import os import re @@ -26,23 +30,34 @@ from git.objects import Submodule, RootModule, Commit from git.refs import HEAD, Head, Reference, TagReference from git.remote import Remote, add_progress, to_progress_instance -from git.util import Actor, finalize_process, decygpath, hex_to_bin, expand_path +from git.util import Actor, IterableList, finalize_process, decygpath, hex_to_bin, expand_path import os.path as osp from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir import gc import gitdb -try: - import pathlib -except ImportError: - pathlib = None +# Typing ------------------------------------------------------------------- +from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, + TextIO, Tuple, Type, Union, NamedTuple, cast,) +from typing_extensions import Literal +from git.types import PathLike, TBD -log = logging.getLogger(__name__) +Lit_config_levels = Literal['system', 'global', 'user', 'repository'] + + +# -------------------------------------------------------------------------- -BlameEntry = namedtuple('BlameEntry', ['commit', 'linenos', 'orig_path', 'orig_linenos']) +class BlameEntry(NamedTuple): + commit: Dict[str, TBD] # Any == 'Commit' type? + linenos: range + orig_path: Optional[str] + orig_linenos: range + + +log = logging.getLogger(__name__) __all__ = ('Repo',) @@ -63,11 +78,11 @@ class Repo(object): 'git_dir' is the .git repository directory, which is always set.""" DAEMON_EXPORT_FILE = 'git-daemon-export-ok' - git = None # Must exist, or __del__ will fail in case we raise on `__init__()` - working_dir = None - _working_tree_dir = None - git_dir = None - _common_dir = None + git = cast('Git', None) # Must exist, or __del__ will fail in case we raise on `__init__()` + working_dir = None # type: Optional[PathLike] + _working_tree_dir = None # type: Optional[PathLike] + git_dir = None # type: Optional[PathLike] + _common_dir = None # type: Optional[PathLike] # precompiled regex re_whitespace = re.compile(r'\s+') @@ -79,13 +94,14 @@ class Repo(object): # invariants # represents the configuration level of a configuration file - config_level = ("system", "user", "global", "repository") + config_level = ("system", "user", "global", "repository") # type: Tuple[Lit_config_levels, ...] # Subclass configuration # Subclasses may easily bring in their own custom types by placing a constructor or type here GitCommandWrapperType = Git - def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=False, expand_vars=True): + def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, + search_parent_directories: bool = False, expand_vars: bool = True) -> None: """Create a new Repo instance :param path: @@ -126,8 +142,9 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal warnings.warn("The use of environment variables in paths is deprecated" + "\nfor security reasons and may be removed in the future!!") epath = expand_path(epath, expand_vars) - if not os.path.exists(epath): - raise NoSuchPathError(epath) + if epath is not None: + if not os.path.exists(epath): + raise NoSuchPathError(epath) ## Walk up the path to find the `.git` dir. # @@ -178,6 +195,7 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal # END while curpath if self.git_dir is None: + self.git_dir = cast(PathLike, self.git_dir) raise InvalidGitRepositoryError(epath) self._bare = False @@ -190,7 +208,7 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal try: common_dir = open(osp.join(self.git_dir, 'commondir'), 'rt').readlines()[0].strip() self._common_dir = osp.join(self.git_dir, common_dir) - except (OSError, IOError): + except OSError: self._common_dir = None # adjust the wd in case we are actually bare - we didn't know that @@ -199,28 +217,28 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal self._working_tree_dir = None # END working dir handling - self.working_dir = self._working_tree_dir or self.common_dir + self.working_dir = self._working_tree_dir or self.common_dir # type: Optional[PathLike] self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times - args = [osp.join(self.common_dir, 'objects')] + args = [osp.join(self.common_dir, 'objects')] # type: List[Union[str, Git]] if issubclass(odbt, GitCmdObjectDB): args.append(self.git) self.odb = odbt(*args) - def __enter__(self): + def __enter__(self) -> 'Repo': return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: TBD, exc_value: TBD, traceback: TBD) -> None: self.close() - def __del__(self): + def __del__(self) -> None: try: self.close() except Exception: pass - def close(self): + def close(self) -> None: if self.git: self.git.clear_cache() # Tempfiles objects on Windows are holding references to @@ -235,25 +253,26 @@ def close(self): if is_win: gc.collect() - def __eq__(self, rhs): - if isinstance(rhs, Repo): + def __eq__(self, rhs: object) -> bool: + if isinstance(rhs, Repo) and self.git_dir: return self.git_dir == rhs.git_dir return False - def __ne__(self, rhs): + def __ne__(self, rhs: object) -> bool: return not self.__eq__(rhs) - def __hash__(self): + def __hash__(self) -> int: return hash(self.git_dir) # Description property - def _get_description(self): - filename = osp.join(self.git_dir, 'description') + def _get_description(self) -> str: + filename = osp.join(self.git_dir, 'description') if self.git_dir else "" with open(filename, 'rb') as fp: return fp.read().rstrip().decode(defenc) - def _set_description(self, descr): - filename = osp.join(self.git_dir, 'description') + def _set_description(self, descr: str) -> None: + + filename = osp.join(self.git_dir, 'description') if self.git_dir else "" with open(filename, 'wb') as fp: fp.write((descr + '\n').encode(defenc)) @@ -263,25 +282,31 @@ def _set_description(self, descr): del _set_description @property - def working_tree_dir(self): + def working_tree_dir(self) -> Optional[PathLike]: """:return: The working tree directory of our git repository. If this is a bare repository, None is returned. """ return self._working_tree_dir @property - def common_dir(self): + def common_dir(self) -> PathLike: """ :return: The git dir that holds everything except possibly HEAD, FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.""" - return self._common_dir or self.git_dir + if self._common_dir: + return self._common_dir + elif self.git_dir: + return self.git_dir + else: + # or could return "" + raise InvalidGitRepositoryError() @property - def bare(self): + def bare(self) -> bool: """:return: True if the repository is bare""" return self._bare @property - def heads(self): + def heads(self) -> IterableList: """A list of ``Head`` objects representing the branch heads in this repo @@ -289,7 +314,7 @@ def heads(self): return Head.list_items(self) @property - def references(self): + def references(self) -> IterableList: """A list of Reference objects representing tags, heads and remote references. :return: IterableList(Reference, ...)""" @@ -302,24 +327,24 @@ def references(self): branches = heads @property - def index(self): + def index(self) -> IndexFile: """:return: IndexFile representing this repository's index. :note: This property can be expensive, as the returned ``IndexFile`` will be reinitialized. It's recommended to re-use the object.""" return IndexFile(self) @property - def head(self): + def head(self) -> HEAD: """:return: HEAD Object pointing to the current head reference""" return HEAD(self, 'HEAD') @property - def remotes(self): + def remotes(self) -> IterableList: """A list of Remote objects allowing to access and manipulate remotes :return: ``git.IterableList(Remote, ...)``""" return Remote.list_items(self) - def remote(self, name='origin'): + def remote(self, name: str = 'origin') -> 'Remote': """:return: Remote with the specified name :raise ValueError: if no remote with such a name exists""" r = Remote(self, name) @@ -330,13 +355,13 @@ def remote(self, name='origin'): #{ Submodules @property - def submodules(self): + def submodules(self) -> IterableList: """ :return: git.IterableList(Submodule, ...) of direct submodules available from the current head""" return Submodule.list_items(self) - def submodule(self, name): + def submodule(self, name: str) -> IterableList: """ :return: Submodule with the given name :raise ValueError: If no such submodule exists""" try: @@ -345,7 +370,7 @@ def submodule(self, name): raise ValueError("Didn't find submodule named %r" % name) from e # END exception handling - def create_submodule(self, *args, **kwargs): + def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule: """Create a new submodule :note: See the documentation of Submodule.add for a description of the @@ -353,13 +378,13 @@ def create_submodule(self, *args, **kwargs): :return: created submodules""" return Submodule.add(self, *args, **kwargs) - def iter_submodules(self, *args, **kwargs): + def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator: """An iterator yielding Submodule instances, see Traversable interface for a description of args and kwargs :return: Iterator""" return RootModule(self).traverse(*args, **kwargs) - def submodule_update(self, *args, **kwargs): + def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator: """Update the submodules, keeping the repository consistent as it will take the previous state into consideration. For more information, please see the documentation of RootModule.update""" @@ -368,41 +393,45 @@ def submodule_update(self, *args, **kwargs): #}END submodules @property - def tags(self): + def tags(self) -> IterableList: """A list of ``Tag`` objects that are available in this repo :return: ``git.IterableList(TagReference, ...)`` """ return TagReference.list_items(self) - def tag(self, path): + def tag(self, path: PathLike) -> TagReference: """:return: TagReference Object, reference pointing to a Commit or Tag :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5 """ return TagReference(self, path) - def create_head(self, path, commit='HEAD', force=False, logmsg=None): + def create_head(self, path: PathLike, commit: str = 'HEAD', + force: bool = False, logmsg: Optional[str] = None + ) -> SymbolicReference: """Create a new head within the repository. For more documentation, please see the Head.create method. :return: newly created Head Reference""" return Head.create(self, path, commit, force, logmsg) - def delete_head(self, *heads, **kwargs): + def delete_head(self, *heads: HEAD, **kwargs: Any) -> None: """Delete the given heads :param kwargs: Additional keyword arguments to be passed to git-branch""" return Head.delete(self, *heads, **kwargs) - def create_tag(self, path, ref='HEAD', message=None, force=False, **kwargs): + def create_tag(self, path: PathLike, ref: str = 'HEAD', + message: Optional[str] = None, force: bool = False, **kwargs: Any + ) -> TagReference: """Create a new tag reference. For more documentation, please see the TagReference.create method. :return: TagReference object """ return TagReference.create(self, path, ref, message, force, **kwargs) - def delete_tag(self, *tags): + def delete_tag(self, *tags: TBD) -> None: """Delete the given tag references""" return TagReference.delete(self, *tags) - def create_remote(self, name, url, **kwargs): + def create_remote(self, name: str, url: PathLike, **kwargs: Any) -> Remote: """Create a new remote. For more information, please see the documentation of the Remote.create @@ -411,11 +440,11 @@ def create_remote(self, name, url, **kwargs): :return: Remote reference""" return Remote.create(self, name, url, **kwargs) - def delete_remote(self, remote): + def delete_remote(self, remote: 'Remote') -> Type['Remote']: """Delete the given remote.""" return Remote.remove(self, remote) - def _get_config_path(self, config_level): + def _get_config_path(self, config_level: Lit_config_levels) -> str: # we do not support an absolute path of the gitconfig on windows , # use the global config instead if is_win and config_level == "system": @@ -429,11 +458,16 @@ def _get_config_path(self, config_level): elif config_level == "global": return osp.normpath(osp.expanduser("~/.gitconfig")) elif config_level == "repository": - return osp.normpath(osp.join(self._common_dir or self.git_dir, "config")) + if self._common_dir: + return osp.normpath(osp.join(self._common_dir, "config")) + elif self.git_dir: + return osp.normpath(osp.join(self.git_dir, "config")) + else: + raise NotADirectoryError raise ValueError("Invalid configuration level: %r" % config_level) - def config_reader(self, config_level=None): + def config_reader(self, config_level: Optional[Lit_config_levels] = None) -> GitConfigParser: """ :return: GitConfigParser allowing to read the full git configuration, but not to write it @@ -454,7 +488,7 @@ def config_reader(self, config_level=None): files = [self._get_config_path(config_level)] return GitConfigParser(files, read_only=True, repo=self) - def config_writer(self, config_level="repository"): + def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser: """ :return: GitConfigParser allowing to write values of the specified configuration file level. @@ -469,7 +503,7 @@ def config_writer(self, config_level="repository"): repository = configuration file for this repository only""" return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self) - def commit(self, rev=None): + def commit(self, rev: Optional[TBD] = None,) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]: """The Commit object for the specified revision :param rev: revision specifier, see git-rev-parse for viable options. @@ -479,12 +513,12 @@ def commit(self, rev=None): return self.head.commit return self.rev_parse(str(rev) + "^0") - def iter_trees(self, *args, **kwargs): + def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator['Tree']: """:return: Iterator yielding Tree objects :note: Takes all arguments known to iter_commits method""" return (c.tree for c in self.iter_commits(*args, **kwargs)) - def tree(self, rev=None): + def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree': """The Tree object for the given treeish revision Examples:: @@ -501,7 +535,8 @@ def tree(self, rev=None): return self.head.commit.tree return self.rev_parse(str(rev) + "^{tree}") - def iter_commits(self, rev=None, paths='', **kwargs): + def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, List[PathLike]] = '', + **kwargs: Any,) -> Iterator[Commit]: """A list of Commit objects representing the history of a given ref/commit :param rev: @@ -525,7 +560,8 @@ def iter_commits(self, rev=None, paths='', **kwargs): return Commit.iter_items(self, rev, paths, **kwargs) - def merge_base(self, *rev, **kwargs): + def merge_base(self, *rev: TBD, **kwargs: Any, + ) -> List[Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]]: """Find the closest common ancestor for the given revision (e.g. Commits, Tags, References, etc) :param rev: At least two revs to find the common ancestor for. @@ -538,9 +574,9 @@ def merge_base(self, *rev, **kwargs): raise ValueError("Please specify at least two revs, got only %i" % len(rev)) # end handle input - res = [] + res = [] # type: List[Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]] try: - lines = self.git.merge_base(*rev, **kwargs).splitlines() + lines = self.git.merge_base(*rev, **kwargs).splitlines() # List[str] except GitCommandError as err: if err.status == 128: raise @@ -556,7 +592,7 @@ def merge_base(self, *rev, **kwargs): return res - def is_ancestor(self, ancestor_rev, rev): + def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: """Check if a commit is an ancestor of another :param ancestor_rev: Rev which should be an ancestor @@ -571,12 +607,12 @@ def is_ancestor(self, ancestor_rev, rev): raise return True - def _get_daemon_export(self): - filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) + def _get_daemon_export(self) -> bool: + filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) if self.git_dir else "" return osp.exists(filename) - def _set_daemon_export(self, value): - filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) + def _set_daemon_export(self, value: object) -> None: + filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) if self.git_dir else "" fileexists = osp.exists(filename) if value and not fileexists: touch(filename) @@ -588,11 +624,11 @@ def _set_daemon_export(self, value): del _get_daemon_export del _set_daemon_export - def _get_alternates(self): + def _get_alternates(self) -> List[str]: """The list of alternates for this repo from which objects can be retrieved :return: list of strings being pathnames of alternates""" - alternates_path = osp.join(self.git_dir, 'objects', 'info', 'alternates') + alternates_path = osp.join(self.git_dir, 'objects', 'info', 'alternates') if self.git_dir else "" if osp.exists(alternates_path): with open(alternates_path, 'rb') as f: @@ -600,7 +636,7 @@ def _get_alternates(self): return alts.strip().splitlines() return [] - def _set_alternates(self, alts): + def _set_alternates(self, alts: List[str]) -> None: """Sets the alternates :param alts: @@ -622,8 +658,8 @@ def _set_alternates(self, alts): alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") - def is_dirty(self, index=True, working_tree=True, untracked_files=False, - submodules=True, path=None): + def is_dirty(self, index: bool = True, working_tree: bool = True, untracked_files: bool = False, + submodules: bool = True, path: Optional[PathLike] = None) -> bool: """ :return: ``True``, the repository is considered dirty. By default it will react @@ -639,7 +675,7 @@ def is_dirty(self, index=True, working_tree=True, untracked_files=False, if not submodules: default_args.append('--ignore-submodules') if path: - default_args.extend(["--", path]) + default_args.extend(["--", str(path)]) if index: # diff index against HEAD if osp.isfile(self.index.path) and \ @@ -658,7 +694,7 @@ def is_dirty(self, index=True, working_tree=True, untracked_files=False, return False @property - def untracked_files(self): + def untracked_files(self) -> List[str]: """ :return: list(str,...) @@ -673,7 +709,7 @@ def untracked_files(self): consider caching it yourself.""" return self._get_untracked_files() - def _get_untracked_files(self, *args, **kwargs): + def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: # make sure we get all files, not only untracked directories proc = self.git.status(*args, porcelain=True, @@ -697,7 +733,7 @@ def _get_untracked_files(self, *args, **kwargs): finalize_process(proc) return untracked_files - def ignored(self, *paths): + def ignored(self, *paths: PathLike) -> List[PathLike]: """Checks if paths are ignored via .gitignore Doing so using the "git check-ignore" method. @@ -711,13 +747,13 @@ def ignored(self, *paths): return proc.replace("\\\\", "\\").replace('"', "").split("\n") @property - def active_branch(self): + def active_branch(self) -> 'SymbolicReference': """The name of the currently active branch. :return: Head to the active branch""" return self.head.reference - def blame_incremental(self, rev, file, **kwargs): + def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iterator['BlameEntry']]: """Iterator for blame information for the given file at the given revision. Unlike .blame(), this does not return the actual file's contents, only @@ -791,22 +827,24 @@ def blame_incremental(self, rev, file, **kwargs): safe_decode(orig_filename), range(orig_lineno, orig_lineno + num_lines)) - def blame(self, rev, file, incremental=False, **kwargs): + def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any + ) -> Union[List[List[Union[Optional['Commit'], List[str]]]], Optional[Iterator[BlameEntry]]]: """The blame information for the given file at the given revision. :param rev: revision specifier, see git-rev-parse for viable options. :return: list: [git.Commit, list: []] - A list of tuples associating a Commit object with a list of lines that + A list of lists associating a Commit object with a list of lines that changed within the given commit. The Commit objects will be given in order of appearance.""" if incremental: return self.blame_incremental(rev, file, **kwargs) data = self.git.blame(rev, '--', file, p=True, stdout_as_string=False, **kwargs) - commits = {} - blames = [] - info = None + commits = {} # type: Dict[str, Any] + blames = [] # type: List[List[Union[Optional['Commit'], List[str]]]] + + info = {} # type: Dict[str, Any] # use Any until TypedDict available keepends = True for line in data.splitlines(keepends): @@ -833,10 +871,12 @@ def blame(self, rev, file, incremental=False, **kwargs): digits = parts[-1].split(" ") if len(digits) == 3: info = {'id': firstpart} - blames.append([None, []]) - elif info['id'] != firstpart: + blames.append([None, [""]]) + elif not info or info['id'] != firstpart: info = {'id': firstpart} - blames.append([commits.get(firstpart), []]) + commits_firstpart = commits.get(firstpart) + blames.append([commits_firstpart, []]) + # END blame data initialization else: m = self.re_author_committer_start.search(firstpart) @@ -891,7 +931,10 @@ def blame(self, rev, file, incremental=False, **kwargs): pass # end handle line contents blames[-1][0] = c - blames[-1][1].append(line) + if blames[-1][1] is not None: + blames[-1][1].append(line) + else: + blames[-1][1] = [line] info = {'id': sha} # END if we collected commit info # END distinguish filename,summary,rest @@ -900,7 +943,8 @@ def blame(self, rev, file, incremental=False, **kwargs): return blames @classmethod - def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, **kwargs): + def init(cls, path: PathLike = None, mkdir: bool = True, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, + expand_vars: bool = True, **kwargs: Any,) -> 'Repo': """Initialize a git repository at the given path if specified :param path: @@ -938,9 +982,12 @@ def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, **kw return cls(path, odbt=odbt) @classmethod - def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, **kwargs): + def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Type[GitCmdObjectDB], + progress: Optional[Callable], + multi_options: Optional[List[str]] = None, **kwargs: Any, + ) -> 'Repo': if progress is not None: - progress = to_progress_instance(progress) + progress_checked = to_progress_instance(progress) odbt = kwargs.pop('odbt', odb_default_type) @@ -964,9 +1011,10 @@ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, if multi_options: multi = ' '.join(multi_options).split(' ') proc = git.clone(multi, Git.polish_url(url), clone_path, with_extended_output=True, as_process=True, - v=True, universal_newlines=True, **add_progress(kwargs, git, progress)) - if progress: - handle_process_output(proc, None, progress.new_message_handler(), finalize_process, decode_streams=False) + v=True, universal_newlines=True, **add_progress(kwargs, git, progress_checked)) + if progress_checked: + handle_process_output(proc, None, progress_checked.new_message_handler(), + finalize_process, decode_streams=False) else: (stdout, stderr) = proc.communicate() log.debug("Cmd(%s)'s unused stdout: %s", getattr(proc, 'args', ''), stdout) @@ -974,8 +1022,8 @@ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, # our git command could have a different working dir than our actual # environment, hence we prepend its working dir if required - if not osp.isabs(path) and git.working_dir: - path = osp.join(git._working_dir, path) + if not osp.isabs(path): + path = osp.join(git._working_dir, path) if git._working_dir is not None else path repo = cls(path, odbt=odbt) @@ -993,7 +1041,8 @@ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, # END handle remote repo return repo - def clone(self, path, progress=None, multi_options=None, **kwargs): + def clone(self, path: PathLike, progress: Optional[Callable] = None, + multi_options: Optional[List[str]] = None, **kwargs: Any) -> 'Repo': """Create a clone from this repository. :param path: is the full path of the new repo (traditionally ends with ./.git). @@ -1011,7 +1060,9 @@ def clone(self, path, progress=None, multi_options=None, **kwargs): return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs) @classmethod - def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, **kwargs): + def clone_from(cls, url: PathLike, to_path: PathLike, progress: Optional[Callable] = None, + env: Optional[Mapping[str, Any]] = None, + multi_options: Optional[List[str]] = None, **kwargs: Any) -> 'Repo': """Create a clone from the given URL :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS @@ -1031,7 +1082,8 @@ def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, * git.update_environment(**env) return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) - def archive(self, ostream, treeish=None, prefix=None, **kwargs): + def archive(self, ostream: Union[TextIO, BinaryIO], treeish: Optional[str] = None, + prefix: Optional[str] = None, **kwargs: Any) -> 'Repo': """Archive the tree at the given revision. :param ostream: file compatible stream object to which the archive will be written as bytes @@ -1052,14 +1104,14 @@ def archive(self, ostream, treeish=None, prefix=None, **kwargs): kwargs['prefix'] = prefix kwargs['output_stream'] = ostream path = kwargs.pop('path', []) + path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path) if not isinstance(path, (tuple, list)): path = [path] # end assure paths is list - self.git.archive(treeish, *path, **kwargs) return self - def has_separate_working_tree(self): + def has_separate_working_tree(self) -> bool: """ :return: True if our git_dir is not at the root of our working_tree_dir, but a .git file with a platform agnositic symbolic link. Our git_dir will be wherever the .git file points to @@ -1067,21 +1119,24 @@ def has_separate_working_tree(self): """ if self.bare: return False - return osp.isfile(osp.join(self.working_tree_dir, '.git')) + if self.working_tree_dir: + return osp.isfile(osp.join(self.working_tree_dir, '.git')) + else: + return False # or raise Error? rev_parse = rev_parse - def __repr__(self): + def __repr__(self) -> str: clazz = self.__class__ return '<%s.%s %r>' % (clazz.__module__, clazz.__name__, self.git_dir) - def currently_rebasing_on(self): + def currently_rebasing_on(self) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]: """ :return: The commit which is currently being replayed while rebasing. None if we are not currently rebasing. """ - rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") + rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") if self.git_dir else "" if not osp.isfile(rebase_head_file): return None return self.commit(open(rebase_head_file, "rt").readline().strip()) diff --git a/git/repo/fun.py b/git/repo/fun.py index 714d41221..b81845932 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -1,4 +1,5 @@ """Package with general repository related functions""" +from git.refs.tag import Tag import os import stat from string import digits @@ -15,18 +16,27 @@ import os.path as osp from git.cmd import Git +# Typing ---------------------------------------------------------------------- + +from .base import Repo +from git.db import GitCmdObjectDB +from git.objects import Commit, TagObject, Blob, Tree +from typing import AnyStr, Union, Optional, cast +from git.types import PathLike + +# ---------------------------------------------------------------------------- __all__ = ('rev_parse', 'is_git_dir', 'touch', 'find_submodule_git_dir', 'name_to_object', 'short_to_long', 'deref_tag', 'to_commit', 'find_worktree_git_dir') -def touch(filename): +def touch(filename: str) -> str: with open(filename, "ab"): pass return filename -def is_git_dir(d): +def is_git_dir(d: PathLike) -> bool: """ This is taken from the git setup.c:is_git_directory function. @@ -48,7 +58,7 @@ def is_git_dir(d): return False -def find_worktree_git_dir(dotgit): +def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]: """Search for a gitdir for this worktree.""" try: statbuf = os.stat(dotgit) @@ -67,7 +77,7 @@ def find_worktree_git_dir(dotgit): return None -def find_submodule_git_dir(d): +def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: """Search for a submodule repo.""" if is_git_dir(d): return d @@ -75,7 +85,7 @@ def find_submodule_git_dir(d): try: with open(d) as fp: content = fp.read().rstrip() - except (IOError, OSError): + except IOError: # it's probably not a file pass else: @@ -92,7 +102,7 @@ def find_submodule_git_dir(d): return None -def short_to_long(odb, hexsha): +def short_to_long(odb: GitCmdObjectDB, hexsha: AnyStr) -> Optional[bytes]: """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha or None if no candidate could be found. :param hexsha: hexsha with less than 40 byte""" @@ -103,14 +113,15 @@ def short_to_long(odb, hexsha): # END exception handling -def name_to_object(repo, name, return_ref=False): +def name_to_object(repo: Repo, name: str, return_ref: bool = False, + ) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree]: """ :return: object specified by the given name, hexshas ( short and long ) as well as references are supported :param return_ref: if name specifies a reference, we will return the reference instead of the object. Otherwise it will raise BadObject or BadName """ - hexsha = None + hexsha = None # type: Union[None, str, bytes] # is it a hexsha ? Try the most common ones, which is 7 to 40 if repo.re_hexsha_shortened.match(name): @@ -150,7 +161,7 @@ def name_to_object(repo, name, return_ref=False): return Object.new_from_sha(repo, hex_to_bin(hexsha)) -def deref_tag(tag): +def deref_tag(tag: Tag) -> TagObject: """Recursively dereference a tag and return the resulting object""" while True: try: @@ -161,7 +172,7 @@ def deref_tag(tag): return tag -def to_commit(obj): +def to_commit(obj: Object) -> Union[Commit, TagObject]: """Convert the given object to a commit if possible and return it""" if obj.type == 'tag': obj = deref_tag(obj) @@ -172,7 +183,7 @@ def to_commit(obj): return obj -def rev_parse(repo, rev): +def rev_parse(repo: Repo, rev: str) -> Union[Commit, Tag, Tree, Blob]: """ :return: Object at the given revision, either Commit, Tag, Tree or Blob :param rev: git-rev-parse compatible revision specification as string, please see @@ -188,7 +199,7 @@ def rev_parse(repo, rev): raise NotImplementedError("commit by message search ( regex )") # END handle search - obj = None + obj = cast(Object, None) # not ideal. Should use guards ref = None output_type = "commit" start = 0 @@ -238,7 +249,7 @@ def rev_parse(repo, rev): pass # error raised later # END exception handling elif output_type in ('', 'blob'): - if obj.type == 'tag': + if obj and obj.type == 'tag': obj = deref_tag(obj) else: # cannot do anything for non-tags From 5b0028e1e75e1ee0eea63ba78cb3160d49c1f3a3 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 28 Feb 2021 21:16:14 +0000 Subject: [PATCH 0002/1849] start add types to util.py --- git/cmd.py | 13 +-- git/compat.py | 21 +++-- git/config.py | 4 +- git/refs/symbolic.py | 2 +- git/util.py | 192 ++++++++++++++++++++++++------------------- 5 files changed, 135 insertions(+), 97 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 050efaedf..bac162176 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -19,6 +19,7 @@ import threading from collections import OrderedDict from textwrap import dedent +from typing import Any, Dict, List, Optional from git.compat import ( defenc, @@ -39,6 +40,8 @@ stream_copy, ) +from .types import PathLike + execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', @@ -516,7 +519,7 @@ def __del__(self): self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir=None): + def __init__(self, working_dir: Optional[PathLike]=None) -> None: """Initialize this instance with: :param working_dir: @@ -525,12 +528,12 @@ def __init__(self, working_dir=None): It is meant to be the working tree directory if available, or the .git directory in case of bare repositories.""" super(Git, self).__init__() - self._working_dir = expand_path(working_dir) + self._working_dir = expand_path(working_dir) if working_dir is not None else None self._git_options = () - self._persistent_git_options = [] + self._persistent_git_options = [] # type: List[str] # Extra environment variables to pass to git commands - self._environment = {} + self._environment = {} # type: Dict[str, Any] # cached command slots self.cat_file_header = None @@ -544,7 +547,7 @@ def __getattr__(self, name): return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - def set_persistent_git_options(self, **kwargs): + def set_persistent_git_options(self, **kwargs) -> None: """Specify command line options to the git executable for subsequent subcommand calls diff --git a/git/compat.py b/git/compat.py index de8a238ba..8d9e551d4 100644 --- a/git/compat.py +++ b/git/compat.py @@ -10,6 +10,7 @@ import locale import os import sys +from typing import AnyStr, Optional, Type from gitdb.utils.encoding import ( @@ -18,33 +19,38 @@ ) -is_win = (os.name == 'nt') +is_win = (os.name == 'nt') # type: bool is_posix = (os.name == 'posix') is_darwin = (os.name == 'darwin') defenc = sys.getfilesystemencoding() -def safe_decode(s): +def safe_decode(s: Optional[AnyStr]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): return s elif isinstance(s, bytes): return s.decode(defenc, 'surrogateescape') - elif s is not None: + elif s is None: + return None + else: raise TypeError('Expected bytes or text, but got %r' % (s,)) -def safe_encode(s): - """Safely decodes a binary string to unicode""" + +def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: + """Safely encodes a binary string to unicode""" if isinstance(s, str): return s.encode(defenc) elif isinstance(s, bytes): return s - elif s is not None: + elif s is None: + return None + else: raise TypeError('Expected bytes or text, but got %r' % (s,)) -def win_encode(s): +def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Encode unicodes for process arguments on Windows.""" if isinstance(s, str): return s.encode(locale.getpreferredencoding(False)) @@ -52,6 +58,7 @@ def win_encode(s): return s elif s is not None: raise TypeError('Expected bytes or text, but got %r' % (s,)) + return None def with_metaclass(meta, *bases): diff --git a/git/config.py b/git/config.py index 9f09efe2b..ffbbfab40 100644 --- a/git/config.py +++ b/git/config.py @@ -16,6 +16,8 @@ import fnmatch from collections import OrderedDict +from typing_extensions import Literal + from git.compat import ( defenc, force_text, @@ -194,7 +196,7 @@ def items_all(self): return [(k, self.getall(k)) for k in self] -def get_config_path(config_level): +def get_config_path(config_level: Literal['system','global','user','repository']) -> str: # we do not support an absolute path of the gitconfig on windows , # use the global config instead diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 60cfe554e..fb9b4f84b 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -513,7 +513,7 @@ def _create(cls, repo, path, resolve, reference, force, logmsg=None): return ref @classmethod - def create(cls, repo, path, reference='HEAD', force=False, logmsg=None): + def create(cls, repo, path, reference='HEAD', force=False, logmsg=None, **kwargs): """Create a new symbolic reference, hence a reference pointing to another reference. :param repo: diff --git a/git/util.py b/git/util.py index 04c967891..16c3e62a2 100644 --- a/git/util.py +++ b/git/util.py @@ -15,8 +15,16 @@ import stat from sys import maxsize import time +from typing import Any, AnyStr, Callable, Dict, Generator, List, NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast from unittest import SkipTest +import typing_extensions +from .types import PathLike, TBD +from pathlib import Path + +from typing_extensions import Literal + + from gitdb.util import (# NOQA @IgnorePep8 make_sha, LockedFD, # @UnusedImport @@ -29,7 +37,7 @@ hex_to_bin, # @UnusedImport ) -from git.compat import is_win +from .compat import is_win import os.path as osp from .exc import InvalidGitRepositoryError @@ -47,6 +55,9 @@ log = logging.getLogger(__name__) +# types############################################################ + + #: 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. @@ -56,22 +67,23 @@ #{ Utility Methods -def unbare_repo(func): +def unbare_repo(func: Callable) -> Callable: """Methods with this decorator raise InvalidGitRepositoryError if they encounter a bare repository""" @wraps(func) - def wrapper(self, *args, **kwargs): + def wrapper(self, *args: Any, **kwargs: Any) -> Callable: if self.repo.bare: raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) # END bare method return func(self, *args, **kwargs) # END wrapper + return wrapper @contextlib.contextmanager -def cwd(new_dir): +def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: old_dir = os.getcwd() os.chdir(new_dir) try: @@ -80,7 +92,7 @@ def cwd(new_dir): os.chdir(old_dir) -def rmtree(path): +def rmtree(path: PathLike) -> None: """Remove the given recursively. :note: we use shutil rmtree but adjust its behaviour to see whether files that @@ -100,7 +112,7 @@ def onerror(func, path, exc_info): return shutil.rmtree(path, False, onerror) -def rmfile(path): +def rmfile(path: PathLike) -> None: """Ensure file deleted also on *Windows* where read-only files need special treatment.""" if osp.isfile(path): if is_win: @@ -108,7 +120,7 @@ def rmfile(path): os.remove(path) -def stream_copy(source, destination, chunk_size=512 * 1024): +def stream_copy(source, destination, chunk_size: int = 512 * 1024) -> int: """Copy all data from the source stream into the destination stream in chunks of size chunk_size @@ -165,7 +177,7 @@ def join_path_native(a, *p): return to_native_path(join_path(a, *p)) -def assure_directory_exists(path, is_file=False): +def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool: """Assure that the directory pointed to by path exists. :param is_file: If True, path is assumed to be a file and handled correctly. @@ -180,18 +192,18 @@ def assure_directory_exists(path, is_file=False): return False -def _get_exe_extensions(): +def _get_exe_extensions() -> Sequence[str]: PATHEXT = os.environ.get('PATHEXT', None) - return tuple(p.upper() for p in PATHEXT.split(os.pathsep)) \ - if PATHEXT \ - else (('.BAT', 'COM', '.EXE') if is_win else ()) + return tuple(p.upper() for p in PATHEXT.split(os.pathsep)) if PATHEXT \ + else ('.BAT', 'COM', '.EXE') if is_win \ + else ('') -def py_where(program, path=None): +def py_where(program, path: Optional[PathLike]=None) -> List[str]: # From: http://stackoverflow.com/a/377028/548792 winprog_exts = _get_exe_extensions() - def is_exec(fpath): + 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)) @@ -199,7 +211,7 @@ def is_exec(fpath): progs = [] if not path: path = os.environ["PATH"] - for folder in path.split(os.pathsep): + for folder in str(path).split(os.pathsep): folder = folder.strip('"') if folder: exe_path = osp.join(folder, program) @@ -209,11 +221,11 @@ def is_exec(fpath): return progs -def _cygexpath(drive, path): +def _cygexpath(drive: Optional[str], path: PathLike) -> str: if osp.isabs(path) and not drive: ## Invoked from `cygpath()` directly with `D:Apps\123`? # It's an error, leave it alone just slashes) - p = path + p = path # convert to str if AnyPath given else: p = path and osp.normpath(osp.expandvars(osp.expanduser(path))) if osp.isabs(p): @@ -224,8 +236,8 @@ def _cygexpath(drive, path): p = cygpath(p) elif drive: p = '/cygdrive/%s/%s' % (drive.lower(), p) - - return p.replace('\\', '/') + p_str = str(p) # ensure it is a str and not AnyPath + return p_str.replace('\\', '/') _cygpath_parsers = ( @@ -237,27 +249,31 @@ def _cygexpath(drive, path): ), (re.compile(r"\\\\\?\\(\w):[/\\](.*)"), - _cygexpath, + (_cygexpath), False ), (re.compile(r"(\w):[/\\](.*)"), - _cygexpath, + (_cygexpath), False ), (re.compile(r"file:(.*)", re.I), (lambda rest_path: rest_path), - True), + True + ), (re.compile(r"(\w{2,}:.*)"), # remote URL, do nothing (lambda url: url), - False), -) + False + ), +) # type: Tuple[Tuple[Pattern[str], Callable, bool], ...] -def cygpath(path): + +def cygpath(path: PathLike) -> PathLike: """Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment.""" + path = str(path) # ensure is str and not AnyPath if not path.startswith(('/cygdrive', '//')): for regex, parser, recurse in _cygpath_parsers: match = regex.match(path) @@ -275,7 +291,8 @@ def cygpath(path): _decygpath_regex = re.compile(r"/cygdrive/(\w)(/.*)?") -def decygpath(path): +def decygpath(path: PathLike) -> str: + path = str(Path) m = _decygpath_regex.match(path) if m: drive, rest_path = m.groups() @@ -286,16 +303,16 @@ def decygpath(path): #: Store boolean flags denoting if a specific Git executable #: is from a Cygwin installation (since `cache_lru()` unsupported on PY2). -_is_cygwin_cache = {} +_is_cygwin_cache = {} # type: Dict[str, Optional[bool]] -def is_cygwin_git(git_executable): +def is_cygwin_git(git_executable) -> bool: if not is_win: return False #from subprocess import check_output - is_cygwin = _is_cygwin_cache.get(git_executable) + is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool] if is_cygwin is None: is_cygwin = False try: @@ -318,18 +335,18 @@ def is_cygwin_git(git_executable): return is_cygwin -def get_user_id(): +def get_user_id() -> str: """:return: string identifying the currently active system user as name@node""" return "%s@%s" % (getpass.getuser(), platform.node()) -def finalize_process(proc, **kwargs): +def finalize_process(proc: TBD, **kwargs: Any) -> None: """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" ## TODO: No close proc-streams?? proc.wait(**kwargs) -def expand_path(p, expand_vars=True): +def expand_path(p: PathLike, expand_vars: bool=True) -> Optional[PathLike]: try: p = osp.expanduser(p) if expand_vars: @@ -364,13 +381,13 @@ class RemoteProgress(object): re_op_absolute = re.compile(r"(remote: )?([\w\s]+):\s+()(\d+)()(.*)") re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") - def __init__(self): + def __init__(self) -> None: self._seen_ops = [] - self._cur_line = None + self._cur_line = None # type: Optional[str] self.error_lines = [] self.other_lines = [] - def _parse_progress_line(self, line): + def _parse_progress_line(self, line: AnyStr) -> None: """Parse progress information from the given line as retrieved by git-push or git-fetch. @@ -382,7 +399,12 @@ def _parse_progress_line(self, line): # Compressing objects: 50% (1/2) # Compressing objects: 100% (2/2) # Compressing objects: 100% (2/2), done. - self._cur_line = line = line.decode('utf-8') if isinstance(line, bytes) else line + if isinstance(line, bytes): # mypy argues about ternary assignment + line_str = line.decode('utf-8') + else: + line_str = line + self._cur_line = line_str + if self.error_lines or self._cur_line.startswith(('error:', 'fatal:')): self.error_lines.append(self._cur_line) return @@ -390,25 +412,25 @@ def _parse_progress_line(self, line): # find escape characters and cut them away - regex will not work with # them as they are non-ascii. As git might expect a tty, it will send them last_valid_index = None - for i, c in enumerate(reversed(line)): + for i, c in enumerate(reversed(line_str)): if ord(c) < 32: # its a slice index last_valid_index = -i - 1 # END character was non-ascii # END for each character in line if last_valid_index is not None: - line = line[:last_valid_index] + line_str = line_str[:last_valid_index] # END cut away invalid part - line = line.rstrip() + line_str = line_str.rstrip() cur_count, max_count = None, None - match = self.re_op_relative.match(line) + match = self.re_op_relative.match(line_str) if match is None: - match = self.re_op_absolute.match(line) + match = self.re_op_absolute.match(line_str) if not match: - self.line_dropped(line) - self.other_lines.append(line) + self.line_dropped(line_str) + self.other_lines.append(line_str) return # END could not get match @@ -437,7 +459,7 @@ def _parse_progress_line(self, line): # This can't really be prevented, so we drop the line verbosely # to make sure we get informed in case the process spits out new # commands at some point. - self.line_dropped(line) + self.line_dropped(line_str) # Note: Don't add this line to the other lines, as we have to silently # drop it return @@ -465,7 +487,7 @@ def _parse_progress_line(self, line): max_count and float(max_count), message) - def new_message_handler(self): + def new_message_handler(self) -> Callable[[str], None]: """ :return: a progress handler suitable for handle_process_output(), passing lines on to this Progress @@ -510,7 +532,7 @@ class CallableRemoteProgress(RemoteProgress): """An implementation forwarding updates to any callable""" __slots__ = ('_callable') - def __init__(self, fn): + def __init__(self, fn: Callable) -> None: self._callable = fn super(CallableRemoteProgress, self).__init__() @@ -539,27 +561,27 @@ class Actor(object): __slots__ = ('name', 'email') - def __init__(self, name, email): + def __init__(self, name: Optional[str], email: Optional[str]) -> None: self.name = name self.email = email - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return self.name == other.name and self.email == other.email - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash((self.name, self.email)) - def __str__(self): + def __str__(self) -> str: return self.name - def __repr__(self): + def __repr__(self) -> str: return '">' % (self.name, self.email) @classmethod - def _from_string(cls, string): + def _from_string(cls, string: str) -> 'Actor': """Create an Actor from a string. :param string: is the string, which is expected to be in regular git format @@ -580,17 +602,17 @@ def _from_string(cls, string): # END handle name/email matching @classmethod - def _main_actor(cls, env_name, env_email, config_reader=None): + def _main_actor(cls, env_name: str, env_email: str, config_reader: Optional[TBD]=None) -> 'Actor': actor = Actor('', '') user_id = None # We use this to avoid multiple calls to getpass.getuser() - def default_email(): + def default_email() -> str: nonlocal user_id if not user_id: user_id = get_user_id() return user_id - def default_name(): + def default_name() -> str: return default_email().split('@')[0] for attr, evar, cvar, default in (('name', env_name, cls.conf_name, default_name), @@ -609,7 +631,7 @@ def default_name(): return actor @classmethod - def committer(cls, config_reader=None): + def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor': """ :return: Actor instance corresponding to the configured committer. It behaves similar to the git implementation, such that the environment will override @@ -620,7 +642,7 @@ def committer(cls, config_reader=None): return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod - def author(cls, config_reader=None): + def author(cls, config_reader: Optional[TBD] = None): """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) @@ -654,16 +676,18 @@ class Stats(object): files = number of changed files as int""" __slots__ = ("total", "files") - def __init__(self, total, files): + def __init__(self, total: Dict[str, Dict[str, int]], files: Dict[str, Dict[str, int]]): self.total = total self.files = files @classmethod - def _list_from_string(cls, repo, text): + def _list_from_string(cls, repo, text: str) -> 'Stats': """Create a Stat object from output retrieved by git-diff. :return: git.Stat""" - hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}} + hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, + 'files': {} + } # type: Dict[str, Dict[str, TBD]] ## need typeddict or refactor for mypy for line in text.splitlines(): (raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != '-' and int(raw_insertions) or 0 @@ -689,7 +713,7 @@ class IndexFileSHA1Writer(object): :note: Based on the dulwich project""" __slots__ = ("f", "sha1") - def __init__(self, f): + def __init__(self, f) -> None: self.f = f self.sha1 = make_sha(b"") @@ -697,12 +721,12 @@ def write(self, data): self.sha1.update(data) return self.f.write(data) - def write_sha(self): + def write_sha(self) -> bytes: sha = self.sha1.digest() self.f.write(sha) return sha - def close(self): + def close(self) -> bytes: sha = self.write_sha() self.f.close() return sha @@ -721,23 +745,23 @@ class LockFile(object): Locks will automatically be released on destruction""" __slots__ = ("_file_path", "_owns_lock") - def __init__(self, file_path): + def __init__(self, file_path: PathLike) -> None: self._file_path = file_path self._owns_lock = False - def __del__(self): + def __del__(self) -> None: self._release_lock() - def _lock_file_path(self): + def _lock_file_path(self) -> str: """:return: Path to lockfile""" return "%s.lock" % (self._file_path) - def _has_lock(self): + def _has_lock(self) -> bool: """:return: True if we have a lock and if the lockfile still exists :raise AssertionError: if our lock-file does not exist""" return self._owns_lock - def _obtain_lock_or_raise(self): + def _obtain_lock_or_raise(self) -> None: """Create a lock file as flag for other instances, mark our instance as lock-holder :raise IOError: if a lock was already present or a lock file could not be written""" @@ -759,12 +783,12 @@ def _obtain_lock_or_raise(self): self._owns_lock = True - def _obtain_lock(self): + def _obtain_lock(self) -> None: """The default implementation will raise if a lock cannot be obtained. Subclasses may override this method to provide a different implementation""" return self._obtain_lock_or_raise() - def _release_lock(self): + def _release_lock(self) -> None: """Release our lock if we have one""" if not self._has_lock(): return @@ -789,7 +813,7 @@ class BlockingLockFile(LockFile): can never be obtained.""" __slots__ = ("_check_interval", "_max_block_time") - def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=maxsize): + def __init__(self, file_path: PathLike, check_interval_s: float=0.3, max_block_time_s: int=maxsize) -> None: """Configure the instance :param check_interval_s: @@ -801,7 +825,7 @@ def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=maxsize): self._check_interval = check_interval_s self._max_block_time = max_block_time_s - def _obtain_lock(self): + def _obtain_lock(self) -> None: """This method blocks until it obtained the lock, or raises IOError if it ran out of time or if the parent directory was not available anymore. If this method returns, you are guaranteed to own the lock""" @@ -851,11 +875,11 @@ class IterableList(list): def __new__(cls, id_attr, prefix=''): return super(IterableList, cls).__new__(cls) - def __init__(self, id_attr, prefix=''): + def __init__(self, id_attr: str, prefix: str='') -> None: self._id_attr = id_attr self._prefix = prefix - def __contains__(self, attr): + def __contains__(self, attr: object) -> bool: # first try identity match for performance try: rval = list.__contains__(self, attr) @@ -867,13 +891,13 @@ def __contains__(self, attr): # otherwise make a full name search try: - getattr(self, attr) + getattr(self, cast(str, attr)) # use cast to silence mypy return True except (AttributeError, TypeError): return False # END handle membership - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> object: attr = self._prefix + attr for item in self: if getattr(item, self._id_attr) == attr: @@ -881,20 +905,22 @@ def __getattr__(self, attr): # END for each item return list.__getattribute__(self, attr) - def __getitem__(self, index): + def __getitem__(self, index: Union[int, slice, str]) -> Any: if isinstance(index, int): return list.__getitem__(self, index) + assert not isinstance(index, slice) try: return getattr(self, index) except AttributeError as e: raise IndexError("No item found with id %r" % (self._prefix + index)) from e # END handle getattr - def __delitem__(self, index): - delindex = index + def __delitem__(self, index: Union[int, str, slice]) -> None: + 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: @@ -917,7 +943,7 @@ class Iterable(object): _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod - def list_items(cls, repo, *args, **kwargs): + def list_items(cls, repo, *args, **kwargs) -> 'IterableList': """ 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 @@ -931,7 +957,7 @@ def list_items(cls, repo, *args, **kwargs): return out_list @classmethod - def iter_items(cls, repo, *args, **kwargs): + def iter_items(cls, repo, *args, **kwargs) -> NoReturn: """For more information about the arguments, see list_items :return: iterator yielding Items""" raise NotImplementedError("To be implemented by Subclass") @@ -940,5 +966,5 @@ def iter_items(cls, repo, *args, **kwargs): class NullHandler(logging.Handler): - def emit(self, record): + def emit(self, record) -> None: pass From a094ac1808f7c5fa0653ac075074bb2232223ac1 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 1 Mar 2021 20:18:01 +0000 Subject: [PATCH 0003/1849] add types to git.util and git.__init__ --- git/__init__.py | 9 ++-- git/remote.py | 5 ++- git/util.py | 111 +++++++++++++++++++++++++----------------------- 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index 534408308..e2f960db7 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -8,15 +8,18 @@ import inspect import os import sys - import os.path as osp +from typing import Optional +from git.types import PathLike __version__ = 'git' + + #{ Initialization -def _init_externals(): +def _init_externals() -> None: """Initialize external projects by putting them into the path""" if __version__ == 'git' and 'PYOXIDIZER' not in os.environ: sys.path.insert(1, osp.join(osp.dirname(__file__), 'ext', 'gitdb')) @@ -65,7 +68,7 @@ def _init_externals(): #{ Initialize git executable path GIT_OK = None -def refresh(path=None): +def refresh(path:Optional[PathLike]=None) -> None: """Convenience method for setting the git executable path.""" global GIT_OK GIT_OK = False diff --git a/git/remote.py b/git/remote.py index 659166149..53349ce70 100644 --- a/git/remote.py +++ b/git/remote.py @@ -34,6 +34,9 @@ TagReference ) +# typing------------------------------------------------------- + +from git.repo.Base import Repo log = logging.getLogger('git.remote') log.addHandler(logging.NullHandler()) @@ -403,7 +406,7 @@ def __init__(self, repo, name): :param repo: The repository we are a remote of :param name: the name of the remote, i.e. 'origin'""" - self.repo = repo + self.repo = repo # type: 'Repo' self.name = name if is_win: diff --git a/git/util.py b/git/util.py index 16c3e62a2..b5cce59db 100644 --- a/git/util.py +++ b/git/util.py @@ -3,6 +3,8 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +from git.remote import Remote +from _typeshed import ReadableBuffer import contextlib from functools import wraps import getpass @@ -15,17 +17,16 @@ import stat from sys import maxsize import time -from typing import Any, AnyStr, Callable, Dict, Generator, List, NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast from unittest import SkipTest -import typing_extensions -from .types import PathLike, TBD -from pathlib import Path - +from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, List, NoReturn, Optional, Pattern, + Sequence, TextIO, Tuple, Union, cast) from typing_extensions import Literal +from git.repo.base import Repo +from .types import PathLike, TBD -from gitdb.util import (# NOQA @IgnorePep8 +from gitdb.util import ( # NOQA @IgnorePep8 make_sha, LockedFD, # @UnusedImport file_contents_ro, # @UnusedImport @@ -72,7 +73,7 @@ def unbare_repo(func: Callable) -> Callable: encounter a bare repository""" @wraps(func) - def wrapper(self, *args: Any, **kwargs: Any) -> Callable: + def wrapper(self: Remote, *args: Any, **kwargs: Any) -> TBD: if self.repo.bare: raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) # END bare method @@ -98,7 +99,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, path, exc_info): + def onerror(func: Callable, path: PathLike, exc_info: TBD) -> None: # Is the error an access error ? os.chmod(path, stat.S_IWUSR) @@ -120,7 +121,7 @@ def rmfile(path: PathLike) -> None: os.remove(path) -def stream_copy(source, destination, chunk_size: int = 512 * 1024) -> int: +def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * 1024) -> int: """Copy all data from the source stream into the destination stream in chunks of size chunk_size @@ -136,11 +137,12 @@ def stream_copy(source, destination, chunk_size: int = 512 * 1024) -> int: return br -def join_path(a, *p): +def join_path(a: PathLike, *p: PathLike) -> PathLike: """Join path tokens together similar to osp.join, but always use '/' instead of possibly '\' on windows.""" - path = a + path = str(a) for b in p: + b = str(b) if not b: continue if b.startswith('/'): @@ -154,22 +156,24 @@ def join_path(a, *p): if is_win: - def to_native_path_windows(path): + def to_native_path_windows(path: PathLike) -> PathLike: + path = str(path) return path.replace('/', '\\') - def to_native_path_linux(path): + def to_native_path_linux(path: PathLike) -> PathLike: + path = str(path) return path.replace('\\', '/') __all__.append("to_native_path_windows") to_native_path = to_native_path_windows else: # no need for any work on linux - def to_native_path_linux(path): + def to_native_path_linux(path: PathLike) -> PathLike: return path to_native_path = to_native_path_linux -def join_path_native(a, *p): +def join_path_native(a: PathLike, *p: PathLike) -> PathLike: """ As join path, but makes sure an OS native path is returned. This is only needed to play it safe on my dear windows and to assure nice paths that only @@ -199,7 +203,7 @@ def _get_exe_extensions() -> Sequence[str]: else ('') -def py_where(program, path: Optional[PathLike]=None) -> List[str]: +def py_where(program: str, path: Optional[PathLike] = None) -> List[str]: # From: http://stackoverflow.com/a/377028/548792 winprog_exts = _get_exe_extensions() @@ -249,7 +253,7 @@ def _cygexpath(drive: Optional[str], path: PathLike) -> str: ), (re.compile(r"\\\\\?\\(\w):[/\\](.*)"), - (_cygexpath), + (_cygexpath), False ), @@ -270,7 +274,6 @@ def _cygexpath(drive: Optional[str], path: PathLike) -> str: ) # type: Tuple[Tuple[Pattern[str], Callable, bool], ...] - def cygpath(path: PathLike) -> PathLike: """Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment.""" path = str(path) # ensure is str and not AnyPath @@ -292,7 +295,7 @@ def cygpath(path: PathLike) -> PathLike: def decygpath(path: PathLike) -> str: - path = str(Path) + path = str(path) m = _decygpath_regex.match(path) if m: drive, rest_path = m.groups() @@ -306,12 +309,12 @@ def decygpath(path: PathLike) -> str: _is_cygwin_cache = {} # type: Dict[str, Optional[bool]] -def is_cygwin_git(git_executable) -> bool: +def is_cygwin_git(git_executable: PathLike) -> bool: if not is_win: return False #from subprocess import check_output - + git_executable = str(git_executable) is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool] if is_cygwin is None: is_cygwin = False @@ -319,7 +322,7 @@ def is_cygwin_git(git_executable) -> bool: git_dir = osp.dirname(git_executable) if not git_dir: res = py_where(git_executable) - git_dir = osp.dirname(res[0]) if res else None + git_dir = osp.dirname(res[0]) if res else "" ## Just a name given, not a real path. uname_cmd = osp.join(git_dir, 'uname') @@ -346,7 +349,7 @@ def finalize_process(proc: TBD, **kwargs: Any) -> None: proc.wait(**kwargs) -def expand_path(p: PathLike, expand_vars: bool=True) -> Optional[PathLike]: +def expand_path(p: PathLike, expand_vars: bool = True) -> Optional[PathLike]: try: p = osp.expanduser(p) if expand_vars: @@ -382,10 +385,10 @@ class RemoteProgress(object): re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") def __init__(self) -> None: - self._seen_ops = [] + self._seen_ops = [] # type: List[TBD] self._cur_line = None # type: Optional[str] - self.error_lines = [] - self.other_lines = [] + self.error_lines = [] # type: List[str] + self.other_lines = [] # type: List[str] def _parse_progress_line(self, line: AnyStr) -> None: """Parse progress information from the given line as retrieved by git-push @@ -404,7 +407,7 @@ def _parse_progress_line(self, line: AnyStr) -> None: else: line_str = line self._cur_line = line_str - + if self.error_lines or self._cur_line.startswith(('error:', 'fatal:')): self.error_lines.append(self._cur_line) return @@ -462,7 +465,7 @@ def _parse_progress_line(self, line: AnyStr) -> None: self.line_dropped(line_str) # Note: Don't add this line to the other lines, as we have to silently # drop it - return + return None # END handle op code # figure out stage @@ -492,16 +495,17 @@ def new_message_handler(self) -> Callable[[str], None]: :return: a progress handler suitable for handle_process_output(), passing lines on to this Progress handler in a suitable format""" - def handler(line): + def handler(line: AnyStr) -> None: return self._parse_progress_line(line.rstrip()) # end return handler - def line_dropped(self, line): + def line_dropped(self, line: str) -> None: """Called whenever a line could not be understood and was therefore dropped.""" pass - def update(self, op_code, cur_count, max_count=None, message=''): + def update(self, op_code: int, cur_count: Union[str, float], max_count: Union[str, float, None] = None, + message: str = '',) -> None: """Called whenever the progress changes :param op_code: @@ -536,7 +540,7 @@ def __init__(self, fn: Callable) -> None: self._callable = fn super(CallableRemoteProgress, self).__init__() - def update(self, *args, **kwargs): + def update(self, *args: Any, **kwargs: Any) -> None: self._callable(*args, **kwargs) @@ -575,7 +579,7 @@ def __hash__(self) -> int: return hash((self.name, self.email)) def __str__(self) -> str: - return self.name + return self.name if self.name else "" def __repr__(self) -> str: return '">' % (self.name, self.email) @@ -602,7 +606,7 @@ def _from_string(cls, string: str) -> 'Actor': # 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: Optional[TBD] = None) -> 'Actor': actor = Actor('', '') user_id = None # We use this to avoid multiple calls to getpass.getuser() @@ -642,7 +646,7 @@ def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor': return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod - def author(cls, config_reader: Optional[TBD] = None): + def author(cls, config_reader: Optional[TBD] = 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) @@ -681,11 +685,11 @@ def __init__(self, total: Dict[str, Dict[str, int]], files: Dict[str, Dict[str, self.files = files @classmethod - def _list_from_string(cls, repo, text: str) -> 'Stats': + def _list_from_string(cls, repo: Repo, text: str) -> 'Stats': """Create a Stat object from output retrieved by git-diff. :return: git.Stat""" - hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, + hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {} } # type: Dict[str, Dict[str, TBD]] ## need typeddict or refactor for mypy for line in text.splitlines(): @@ -713,11 +717,11 @@ class IndexFileSHA1Writer(object): :note: Based on the dulwich project""" __slots__ = ("f", "sha1") - def __init__(self, f) -> None: + def __init__(self, f: IO) -> None: self.f = f self.sha1 = make_sha(b"") - def write(self, data): + def write(self, data: AnyStr) -> int: self.sha1.update(data) return self.f.write(data) @@ -731,7 +735,7 @@ def close(self) -> bytes: self.f.close() return sha - def tell(self): + def tell(self) -> int: return self.f.tell() @@ -813,7 +817,7 @@ class BlockingLockFile(LockFile): can never be obtained.""" __slots__ = ("_check_interval", "_max_block_time") - def __init__(self, file_path: PathLike, check_interval_s: float=0.3, max_block_time_s: int=maxsize) -> None: + def __init__(self, file_path: PathLike, check_interval_s: float = 0.3, max_block_time_s: int = maxsize) -> None: """Configure the instance :param check_interval_s: @@ -872,10 +876,10 @@ class IterableList(list): can be left out.""" __slots__ = ('_id_attr', '_prefix') - def __new__(cls, id_attr, prefix=''): + def __new__(cls, id_attr: str, prefix: str = '') -> 'IterableList': return super(IterableList, cls).__new__(cls) - def __init__(self, id_attr: str, prefix: str='') -> None: + def __init__(self, id_attr: str, prefix: str = '') -> None: self._id_attr = id_attr self._prefix = prefix @@ -897,7 +901,7 @@ def __contains__(self, attr: object) -> bool: return False # END handle membership - def __getattr__(self, attr: str) -> object: + def __getattr__(self, attr: str) -> Any: attr = self._prefix + attr for item in self: if getattr(item, self._id_attr) == attr: @@ -908,12 +912,13 @@ def __getattr__(self, attr: str) -> object: def __getitem__(self, index: Union[int, slice, str]) -> Any: if isinstance(index, int): return list.__getitem__(self, index) - - assert not isinstance(index, slice) - try: - return getattr(self, index) - except AttributeError as e: - raise IndexError("No item found with id %r" % (self._prefix + index)) from e + elif isinstance(index, slice): + raise ValueError("Index should be an int or str") + else: + try: + return getattr(self, index) + except AttributeError as e: + 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: @@ -943,7 +948,7 @@ class Iterable(object): _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod - def list_items(cls, repo, *args, **kwargs) -> 'IterableList': + def list_items(cls, repo: Repo, *args: Any, **kwargs: Any) -> 'IterableList': """ 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 @@ -957,7 +962,7 @@ def list_items(cls, repo, *args, **kwargs) -> 'IterableList': return out_list @classmethod - def iter_items(cls, repo, *args, **kwargs) -> NoReturn: + def iter_items(cls, repo: Repo, *args: Any, **kwargs: Any) -> NoReturn: """For more information about the arguments, see list_items :return: iterator yielding Items""" raise NotImplementedError("To be implemented by Subclass") @@ -966,5 +971,5 @@ def iter_items(cls, repo, *args, **kwargs) -> NoReturn: class NullHandler(logging.Handler): - def emit(self, record) -> None: + def emit(self, record: object) -> None: pass From 71e28b8e2ac1b8bc8990454721740b2073829110 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 1 Mar 2021 20:55:08 +0000 Subject: [PATCH 0004/1849] add types to git.db and git.exc --- git/db.py | 20 ++++++++++++-------- git/exc.py | 39 +++++++++++++++++++++++++++------------ mypy.ini | 2 ++ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/git/db.py b/git/db.py index de2e99910..e2d3910d8 100644 --- a/git/db.py +++ b/git/db.py @@ -6,12 +6,16 @@ ) from gitdb.db import GitDB # @UnusedImport from gitdb.db import LooseObjectDB +from gitdb.exc import BadObject -from .exc import ( - GitCommandError, - BadObject -) +from .exc import GitCommandError + +# typing------------------------------------------------- + +from .cmd import Git +from .types import PathLike, TBD +# -------------------------------------------------------- __all__ = ('GitCmdObjectDB', 'GitDB') @@ -28,23 +32,23 @@ class GitCmdObjectDB(LooseObjectDB): have packs and the other implementations """ - def __init__(self, root_path, git): + def __init__(self, root_path: PathLike, git: Git) -> None: """Initialize this instance with the root and a git command""" super(GitCmdObjectDB, self).__init__(root_path) self._git = git - def info(self, sha): + def info(self, sha: bytes) -> OInfo: hexsha, typename, size = self._git.get_object_header(bin_to_hex(sha)) return OInfo(hex_to_bin(hexsha), typename, size) - def stream(self, sha): + def stream(self, sha: bytes) -> OStream: """For now, all lookup is done by git itself""" hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(sha)) return OStream(hex_to_bin(hexsha), typename, size, stream) # { Interface - def partial_to_complete_sha_hex(self, partial_hexsha): + def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes: """:return: Full binary 20 byte sha from the given partial hexsha :raise AmbiguousObjectName: :raise BadObject: diff --git a/git/exc.py b/git/exc.py index 71a40bdfd..bd019c7fd 100644 --- a/git/exc.py +++ b/git/exc.py @@ -8,6 +8,13 @@ from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode +# typing ---------------------------------------------------- + +from git.repo.base import Repo +from git.types import PathLike +from typing import IO, List, Optional, Sequence, Tuple, Union + +# ------------------------------------------------------------------ class GitError(Exception): """ Base class for all package exceptions """ @@ -37,7 +44,9 @@ class CommandError(GitError): #: "'%s' failed%s" _msg = "Cmd('%s') failed%s" - def __init__(self, command, status=None, stderr=None, stdout=None): + def __init__(self, command: Union[List[str], Tuple[str, ...], str], + status: Union[str, None, Exception] = None, + stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None) -> None: if not isinstance(command, (tuple, list)): command = command.split() self.command = command @@ -53,12 +62,12 @@ def __init__(self, command, status=None, stderr=None, stdout=None): status = "'%s'" % s if isinstance(status, str) else s self._cmd = safe_decode(command[0]) - self._cmdline = ' '.join(safe_decode(i) for i in command) + self._cmdline = ' '.join(str(safe_decode(i)) for i in command) self._cause = status and " due to: %s" % status or "!" - self.stdout = stdout and "\n stdout: '%s'" % safe_decode(stdout) or '' - self.stderr = stderr and "\n stderr: '%s'" % safe_decode(stderr) or '' + self.stdout = stdout and "\n stdout: '%s'" % safe_decode(str(stdout)) or '' + self.stderr = stderr and "\n stderr: '%s'" % safe_decode(str(stderr)) or '' - def __str__(self): + def __str__(self) -> str: return (self._msg + "\n cmdline: %s%s%s") % ( self._cmd, self._cause, self._cmdline, self.stdout, self.stderr) @@ -66,7 +75,8 @@ def __str__(self): class GitCommandNotFound(CommandError): """Thrown if we cannot find the `git` executable in the PATH or at the path given by the GIT_PYTHON_GIT_EXECUTABLE environment variable""" - def __init__(self, command, cause): + + def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None: super(GitCommandNotFound, self).__init__(command, cause) self._msg = "Cmd('%s') not found%s" @@ -74,7 +84,11 @@ def __init__(self, command, cause): class GitCommandError(CommandError): """ Thrown if execution of the git command fails with non-zero status code. """ - def __init__(self, command, status, stderr=None, stdout=None): + def __init__(self, command: Union[List[str], Tuple[str, ...], str], + status: Union[str, None, Exception] = None, + stderr: Optional[IO[str]] = None, + stdout: Optional[IO[str]] = None, + ) -> None: super(GitCommandError, self).__init__(command, status, stderr, stdout) @@ -92,13 +106,13 @@ class CheckoutError(GitError): were checked out successfully and hence match the version stored in the index""" - def __init__(self, message, failed_files, valid_files, failed_reasons): + def __init__(self, message: str, failed_files: List[PathLike], valid_files: List[PathLike], failed_reasons: List[str]) -> None: Exception.__init__(self, message) self.failed_files = failed_files self.failed_reasons = failed_reasons self.valid_files = valid_files - def __str__(self): + def __str__(self) -> str: return Exception.__str__(self) + ":%s" % self.failed_files @@ -116,7 +130,8 @@ class HookExecutionError(CommandError): """Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned via standard output""" - def __init__(self, command, status, stderr=None, stdout=None): + def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Optional[str], + stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None) -> None: super(HookExecutionError, self).__init__(command, status, stderr, stdout) self._msg = "Hook('%s') failed%s" @@ -124,9 +139,9 @@ def __init__(self, command, status, stderr=None, stdout=None): class RepositoryDirtyError(GitError): """Thrown whenever an operation on a repository fails as it has uncommitted changes that would be overwritten""" - def __init__(self, repo, message): + def __init__(self, repo: Repo, message: str) -> None: self.repo = repo self.message = message - def __str__(self): + def __str__(self) -> str: return "Operation cannot be performed on %r: %s" % (self.repo, self.message) diff --git a/mypy.ini b/mypy.ini index 349266b77..47c0fb0c0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,3 +2,5 @@ [mypy] disallow_untyped_defs = True + +mypy_path = 'git' From 2fd9f6ee5c8b4ae4e01a40dc398e2768d838210d Mon Sep 17 00:00:00 2001 From: yobmod Date: Tue, 2 Mar 2021 21:46:17 +0000 Subject: [PATCH 0005/1849] add types to git.compat and git.diff --- git/compat.py | 16 ++++--- git/db.py | 5 ++- git/diff.py | 114 +++++++++++++++++++++++++++++--------------------- git/exc.py | 11 +++-- git/util.py | 9 ++-- 5 files changed, 91 insertions(+), 64 deletions(-) diff --git a/git/compat.py b/git/compat.py index 8d9e551d4..4fe394ae0 100644 --- a/git/compat.py +++ b/git/compat.py @@ -10,14 +10,15 @@ import locale import os import sys -from typing import AnyStr, Optional, Type - from gitdb.utils.encoding import ( force_bytes, # @UnusedImport force_text # @UnusedImport ) +from typing import Any, AnyStr, Dict, Optional, Type +from git.types import TBD + is_win = (os.name == 'nt') # type: bool is_posix = (os.name == 'posix') @@ -61,14 +62,17 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: return None -def with_metaclass(meta, *bases): +def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation """copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15""" - class metaclass(meta): + + class metaclass(meta): # type: ignore __call__ = type.__call__ - __init__ = type.__init__ + __init__ = type.__init__ # type: ignore - def __new__(cls, name, nbases, d): + def __new__(cls, name: str, nbases: Optional[int], d: Dict[str, Any]) -> TBD: if nbases is None: return type.__new__(cls, name, (), d) return meta(name, bases, d) + return metaclass(meta.__name__ + 'Helper', None, {}) + diff --git a/git/db.py b/git/db.py index e2d3910d8..ef2b0b2ef 100644 --- a/git/db.py +++ b/git/db.py @@ -1,4 +1,5 @@ """Module with our own gitdb implementation - it uses the git command""" +from typing import AnyStr from git.util import bin_to_hex, hex_to_bin from gitdb.base import ( OInfo, @@ -13,7 +14,7 @@ # typing------------------------------------------------- from .cmd import Git -from .types import PathLike, TBD +from .types import PathLike # -------------------------------------------------------- @@ -48,7 +49,7 @@ def stream(self, sha: bytes) -> OStream: # { Interface - def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes: + def partial_to_complete_sha_hex(self, partial_hexsha: AnyStr) -> bytes: """:return: Full binary 20 byte sha from the given partial hexsha :raise AmbiguousObjectName: :raise BadObject: diff --git a/git/diff.py b/git/diff.py index a9dc4b572..b25aadc76 100644 --- a/git/diff.py +++ b/git/diff.py @@ -3,8 +3,8 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -import re +import re from git.cmd import handle_process_output from git.compat import defenc from git.util import finalize_process, hex_to_bin @@ -13,22 +13,33 @@ from .objects.util import mode_str_to_int +# typing ------------------------------------------------------------------ + +from .objects.tree import Tree +from git.repo.base import Repo +from typing_extensions import Final, Literal +from git.types import TBD +from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union +Lit_change_type = Literal['A', 'D', 'M', 'R', 'T'] + +# ------------------------------------------------------------------------ + __all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE') # Special object to compare against the empty tree in diffs -NULL_TREE = object() +NULL_TREE: Final[object] = object() _octal_byte_re = re.compile(b'\\\\([0-9]{3})') -def _octal_repl(matchobj): +def _octal_repl(matchobj: Match) -> bytes: value = matchobj.group(1) value = int(value, 8) value = bytes(bytearray((value,))) return value -def decode_path(path, has_ab_prefix=True): +def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]: if path == b'/dev/null': return None @@ -60,7 +71,7 @@ class Diffable(object): class Index(object): pass - def _process_diff_args(self, args): + def _process_diff_args(self, args: List[Union[str, 'Diffable', object]]) -> List[Union[str, 'Diffable', object]]: """ :return: possibly altered version of the given args list. @@ -68,7 +79,9 @@ def _process_diff_args(self, args): Subclasses can use it to alter the behaviour of the superclass""" return args - def diff(self, other=Index, paths=None, create_patch=False, **kwargs): + def diff(self, other: Union[Type[Index], Type[Tree], object, None, str] = Index, + paths: Union[str, List[str], Tuple[str, ...], None] = None, + create_patch: bool = False, **kwargs: Any) -> 'DiffIndex': """Creates diffs between two items being trees, trees and index or an index and the working tree. It will detect renames automatically. @@ -99,7 +112,7 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs): :note: On a bare repository, 'other' needs to be provided as Index or as as Tree/Commit, or a git command error will occur""" - args = [] + args = [] # type: List[Union[str, Diffable, object]] args.append("--abbrev=40") # we need full shas args.append("--full-index") # get full index paths, not only filenames @@ -117,6 +130,9 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs): if paths is not None and not isinstance(paths, (tuple, list)): paths = [paths] + if hasattr(self, 'repo'): # else raise Error? + self.repo = self.repo # type: 'Repo' + diff_cmd = self.repo.git.diff if other is self.Index: args.insert(0, '--cached') @@ -163,7 +179,7 @@ class DiffIndex(list): # T = Changed in the type change_type = ("A", "C", "D", "R", "M", "T") - def iter_change_type(self, change_type): + def iter_change_type(self, change_type: Lit_change_type) -> Iterator['Diff']: """ :return: iterator yielding Diff instances that match the given change_type @@ -180,7 +196,7 @@ def iter_change_type(self, change_type): if change_type not in self.change_type: raise ValueError("Invalid change type: %s" % change_type) - for diff in self: + for diff in self: # type: 'Diff' if diff.change_type == change_type: yield diff elif change_type == "A" and diff.new_file: @@ -255,22 +271,21 @@ class Diff(object): "new_file", "deleted_file", "copied_file", "raw_rename_from", "raw_rename_to", "diff", "change_type", "score") - def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode, - b_mode, new_file, deleted_file, copied_file, raw_rename_from, - raw_rename_to, diff, change_type, score): - - self.a_mode = a_mode - self.b_mode = b_mode + def __init__(self, repo: Repo, + a_rawpath: Optional[bytes], b_rawpath: Optional[bytes], + a_blob_id: Union[str, bytes, None], b_blob_id: Union[str, bytes, None], + a_mode: Union[bytes, str, None], b_mode: Union[bytes, str, None], + new_file: bool, deleted_file: bool, copied_file: bool, + raw_rename_from: Optional[bytes], raw_rename_to: Optional[bytes], + diff: Union[str, bytes, None], change_type: Optional[str], score: Optional[int]) -> None: assert a_rawpath is None or isinstance(a_rawpath, bytes) assert b_rawpath is None or isinstance(b_rawpath, bytes) self.a_rawpath = a_rawpath self.b_rawpath = b_rawpath - if self.a_mode: - self.a_mode = mode_str_to_int(self.a_mode) - if self.b_mode: - self.b_mode = mode_str_to_int(self.b_mode) + self.a_mode = mode_str_to_int(a_mode) if a_mode else None + self.b_mode = mode_str_to_int(b_mode) if b_mode else None # Determine whether this diff references a submodule, if it does then # we need to overwrite "repo" to the corresponding submodule's repo instead @@ -305,27 +320,27 @@ def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode, self.change_type = change_type self.score = score - def __eq__(self, other): + def __eq__(self, other: object) -> bool: for name in self.__slots__: if getattr(self, name) != getattr(other, name): return False # END for each name return True - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash(tuple(getattr(self, n) for n in self.__slots__)) - def __str__(self): - h = "%s" + def __str__(self) -> str: + h = "%s" # type: str if self.a_blob: h %= self.a_blob.path elif self.b_blob: h %= self.b_blob.path - msg = '' + msg = '' # type: str line = None # temp line line_length = 0 # line length for b, n in zip((self.a_blob, self.b_blob), ('lhs', 'rhs')): @@ -354,7 +369,7 @@ def __str__(self): if self.diff: msg += '\n---' try: - msg += self.diff.decode(defenc) + msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff except UnicodeDecodeError: msg += 'OMITTED BINARY DATA' # end handle encoding @@ -368,36 +383,36 @@ def __str__(self): return res @property - def a_path(self): + def a_path(self) -> Optional[str]: return self.a_rawpath.decode(defenc, 'replace') if self.a_rawpath else None @property - def b_path(self): + def b_path(self) -> Optional[str]: return self.b_rawpath.decode(defenc, 'replace') if self.b_rawpath else None @property - def rename_from(self): + def rename_from(self) -> Optional[str]: return self.raw_rename_from.decode(defenc, 'replace') if self.raw_rename_from else None @property - def rename_to(self): + def rename_to(self) -> Optional[str]: return self.raw_rename_to.decode(defenc, 'replace') if self.raw_rename_to else None @property - def renamed(self): + def renamed(self) -> bool: """:returns: True if the blob of our diff has been renamed :note: This property is deprecated, please use ``renamed_file`` instead. """ return self.renamed_file @property - def renamed_file(self): + def renamed_file(self) -> bool: """:returns: True if the blob of our diff has been renamed """ return self.rename_from != self.rename_to @classmethod - def _pick_best_path(cls, path_match, rename_match, path_fallback_match): + def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]: if path_match: return decode_path(path_match) @@ -410,21 +425,23 @@ def _pick_best_path(cls, path_match, rename_match, path_fallback_match): return None @classmethod - def _index_from_patch_format(cls, repo, proc): + def _index_from_patch_format(cls, repo: Repo, proc: TBD) -> DiffIndex: """Create a new DiffIndex from the given text which must be in patch format :param repo: is the repository we are operating on - it is required :param stream: result of 'git diff' as a stream (supporting file protocol) :return: git.DiffIndex """ ## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise. - text = [] - handle_process_output(proc, text.append, None, finalize_process, decode_streams=False) + text_list = [] # type: List[bytes] + handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False) # for now, we have to bake the stream - text = b''.join(text) + text = b''.join(text_list) index = DiffIndex() previous_header = None header = None + a_path, b_path = None, None # for mypy + a_mode, b_mode = None, None # for mypy for _header in cls.re_header.finditer(text): a_path_fallback, b_path_fallback, \ old_mode, new_mode, \ @@ -464,14 +481,14 @@ def _index_from_patch_format(cls, repo, proc): previous_header = _header header = _header # end for each header we parse - if index: + if index and header: index[-1].diff = text[header.end():] # end assign last diff return index @classmethod - def _index_from_raw_format(cls, repo, proc): + def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: """Create a new DiffIndex from the given stream which must be in raw format. :return: git.DiffIndex""" # handles @@ -479,12 +496,13 @@ def _index_from_raw_format(cls, repo, proc): index = DiffIndex() - def handle_diff_line(lines): - lines = lines.decode(defenc) + def handle_diff_line(lines_bytes: bytes) -> None: + lines = lines_bytes.decode(defenc) for line in lines.split(':')[1:]: meta, _, path = line.partition('\x00') path = path.rstrip('\x00') + a_blob_id, b_blob_id = None, None # Type: Optional[str] old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) # Change type can be R100 # R: status letter @@ -504,20 +522,20 @@ def handle_diff_line(lines): # NOTE: We cannot conclude from the existence of a blob to change type # as diffs with the working do not have blobs yet if change_type == 'D': - b_blob_id = None + b_blob_id = None # Optional[str] deleted_file = True elif change_type == 'A': a_blob_id = None new_file = True elif change_type == 'C': copied_file = True - a_path, b_path = path.split('\x00', 1) - a_path = a_path.encode(defenc) - b_path = b_path.encode(defenc) + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) elif change_type == 'R': - a_path, b_path = path.split('\x00', 1) - a_path = a_path.encode(defenc) - b_path = b_path.encode(defenc) + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) rename_from, rename_to = a_path, b_path elif change_type == 'T': # Nothing to do diff --git a/git/exc.py b/git/exc.py index bd019c7fd..c02b2b3a3 100644 --- a/git/exc.py +++ b/git/exc.py @@ -12,10 +12,11 @@ from git.repo.base import Repo from git.types import PathLike -from typing import IO, List, Optional, Sequence, Tuple, Union +from typing import IO, List, Optional, Tuple, Union # ------------------------------------------------------------------ + class GitError(Exception): """ Base class for all package exceptions """ @@ -44,7 +45,7 @@ class CommandError(GitError): #: "'%s' failed%s" _msg = "Cmd('%s') failed%s" - def __init__(self, command: Union[List[str], Tuple[str, ...], str], + def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Union[str, None, Exception] = None, stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None) -> None: if not isinstance(command, (tuple, list)): @@ -84,7 +85,7 @@ def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, class GitCommandError(CommandError): """ Thrown if execution of the git command fails with non-zero status code. """ - def __init__(self, command: Union[List[str], Tuple[str, ...], str], + def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Union[str, None, Exception] = None, stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None, @@ -106,7 +107,9 @@ class CheckoutError(GitError): were checked out successfully and hence match the version stored in the index""" - def __init__(self, message: str, failed_files: List[PathLike], valid_files: List[PathLike], failed_reasons: List[str]) -> None: + def __init__(self, message: str, failed_files: List[PathLike], valid_files: List[PathLike], + failed_reasons: List[str]) -> None: + Exception.__init__(self, message) self.failed_files = failed_files self.failed_reasons = failed_reasons diff --git a/git/util.py b/git/util.py index b5cce59db..2b0c81715 100644 --- a/git/util.py +++ b/git/util.py @@ -4,7 +4,6 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php from git.remote import Remote -from _typeshed import ReadableBuffer import contextlib from functools import wraps import getpass @@ -19,12 +18,14 @@ import time from unittest import SkipTest -from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, List, NoReturn, Optional, Pattern, - Sequence, TextIO, Tuple, Union, cast) -from typing_extensions import Literal +# typing --------------------------------------------------------- +from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, List, + NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast) from git.repo.base import Repo from .types import PathLike, TBD +# --------------------------------------------------------------------- + from gitdb.util import ( # NOQA @IgnorePep8 make_sha, From 45d1cd59d39227ee6841042eab85116a59a26d22 Mon Sep 17 00:00:00 2001 From: Bert Wesarg Date: Thu, 11 Mar 2021 16:35:24 +0100 Subject: [PATCH 0006/1849] Remove support for Python 3.5 --- .appveyor.yml | 29 ++--------------------------- .github/workflows/pythonpackage.yml | 4 ++-- .travis.yml | 4 +--- README.md | 2 +- doc/source/intro.rst | 2 +- git/cmd.py | 9 --------- setup.py | 4 ++-- 7 files changed, 9 insertions(+), 45 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0a86c1a75..833f5c7b9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,29 +6,12 @@ environment: CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%" matrix: - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Miniconda35-x64" - PYTHON_VERSION: "3.5" - IS_CONDA: "yes" - MAYFAIL: "yes" - GIT_PATH: "%GIT_DAEMON_PATH%" - ## Cygwin - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - IS_CYGWIN: "yes" - MAYFAIL: "yes" - GIT_PATH: "%CYGWIN64_GIT_PATH%" matrix: allow_failures: @@ -76,18 +59,10 @@ install: build: false test_script: - - IF "%IS_CYGWIN%" == "yes" ( - nosetests -v - ) ELSE ( - IF "%PYTHON_VERSION%" == "3.5" ( - nosetests -v --with-coverage - ) ELSE ( - nosetests -v - ) - ) + - nosetests -v on_success: - - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov) + - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov) # Enable this to be able to login to the build worker. You can use the # `remmina` program in Ubuntu, use the login information that the line below diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5e94cd05e..618d6b97a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -56,4 +56,4 @@ jobs: run: | set -x pip install -r doc/requirements.txt - make -C doc html \ No newline at end of file + make -C doc html diff --git a/.travis.yml b/.travis.yml index 1fbb1ddb8..570beaad6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ # UNUSED, only for reference. If adjustments are needed, please see github actions language: python python: - - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" @@ -38,7 +36,7 @@ script: - ulimit -n - coverage run --omit="test/*" -m unittest --buffer - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi + - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi after_success: - codecov diff --git a/README.md b/README.md index 0d0edeb43..4725d3aeb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. * Git (1.7.x or newer) -* Python >= 3.5 +* Python >= 3.6 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 7168c91b1..956a36073 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.5 +* `Python`_ >= 3.6 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/git/cmd.py b/git/cmd.py index ec630d93c..72ec0381a 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,9 +17,7 @@ import subprocess import sys import threading -from collections import OrderedDict from textwrap import dedent -import warnings from git.compat import ( defenc, @@ -903,13 +901,6 @@ def transform_kwarg(self, name, value, split_single_char_options): def transform_kwargs(self, split_single_char_options=True, **kwargs): """Transforms Python style kwargs into git command line options.""" - # Python 3.6 preserves the order of kwargs and thus has a stable - # order. For older versions sort the kwargs by the key to get a stable - # order. - if sys.version_info[:2] < (3, 6): - kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) - warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" + - "It does not preserve the order for key-word arguments and enforce lexical sorting instead.") args = [] for k, v in kwargs.items(): if isinstance(v, (list, tuple)): diff --git a/setup.py b/setup.py index f8829c386..850d680d4 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): include_package_data=True, py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={'git': 'git'}, - python_requires='>=3.5', + python_requires='>=3.6', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -127,6 +127,6 @@ def build_py_modules(basedir, excludes=[]): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9" ] ) From 14fc8bd3e5a8249224b774ea9052c9a701fc8e0f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 28 Mar 2021 09:36:07 +0800 Subject: [PATCH 0007/1849] Create FUNDING.yml Allow people to say thanks. --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..80819f5d8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: byron From 651a81ded00eb993977bcdc6d65f157c751edb02 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Apr 2021 13:54:23 +0800 Subject: [PATCH 0008/1849] refactor; add failing test to validate #1210 --- git/diff.py | 107 +++++++++++++++-------------- git/ext/gitdb | 2 +- test/fixtures/diff_file_with_colon | Bin 0 -> 351 bytes test/test_diff.py | 7 ++ 4 files changed, 62 insertions(+), 54 deletions(-) create mode 100644 test/fixtures/diff_file_with_colon diff --git a/git/diff.py b/git/diff.py index 129223cb3..deedb635c 100644 --- a/git/diff.py +++ b/git/diff.py @@ -490,6 +490,58 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: return index + @staticmethod + def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: TBD) -> None: + lines = lines_bytes.decode(defenc) + + for line in lines.split(':')[1:]: + meta, _, path = line.partition('\x00') + path = path.rstrip('\x00') + a_blob_id, b_blob_id = None, None # Type: Optional[str] + old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) + # Change type can be R100 + # R: status letter + # 100: score (in case of copy and rename) + change_type = _change_type[0] + score_str = ''.join(_change_type[1:]) + score = int(score_str) if score_str.isdigit() else None + path = path.strip() + a_path = path.encode(defenc) + b_path = path.encode(defenc) + deleted_file = False + new_file = False + copied_file = False + rename_from = None + rename_to = None + + # NOTE: We cannot conclude from the existence of a blob to change type + # as diffs with the working do not have blobs yet + if change_type == 'D': + b_blob_id = None # Optional[str] + deleted_file = True + elif change_type == 'A': + a_blob_id = None + new_file = True + elif change_type == 'C': + copied_file = True + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) + elif change_type == 'R': + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) + rename_from, rename_to = a_path, b_path + elif change_type == 'T': + # Nothing to do + pass + # END add/remove handling + + diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, + new_file, deleted_file, copied_file, rename_from, rename_to, + '', change_type, score) + index.append(diff) + @classmethod def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: """Create a new DiffIndex from the given stream which must be in raw format. @@ -498,58 +550,7 @@ def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: # :100644 100644 687099101... 37c5e30c8... M .gitignore index = DiffIndex() - - def handle_diff_line(lines_bytes: bytes) -> None: - lines = lines_bytes.decode(defenc) - - for line in lines.split(':')[1:]: - meta, _, path = line.partition('\x00') - path = path.rstrip('\x00') - a_blob_id, b_blob_id = None, None # Type: Optional[str] - old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) - # Change type can be R100 - # R: status letter - # 100: score (in case of copy and rename) - change_type = _change_type[0] - score_str = ''.join(_change_type[1:]) - score = int(score_str) if score_str.isdigit() else None - path = path.strip() - a_path = path.encode(defenc) - b_path = path.encode(defenc) - deleted_file = False - new_file = False - copied_file = False - rename_from = None - rename_to = None - - # NOTE: We cannot conclude from the existence of a blob to change type - # as diffs with the working do not have blobs yet - if change_type == 'D': - b_blob_id = None # Optional[str] - deleted_file = True - elif change_type == 'A': - a_blob_id = None - new_file = True - elif change_type == 'C': - copied_file = True - a_path_str, b_path_str = path.split('\x00', 1) - a_path = a_path_str.encode(defenc) - b_path = b_path_str.encode(defenc) - elif change_type == 'R': - a_path_str, b_path_str = path.split('\x00', 1) - a_path = a_path_str.encode(defenc) - b_path = b_path_str.encode(defenc) - rename_from, rename_to = a_path, b_path - elif change_type == 'T': - # Nothing to do - pass - # END add/remove handling - - diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, - new_file, deleted_file, copied_file, rename_from, rename_to, - '', change_type, score) - index.append(diff) - - handle_process_output(proc, handle_diff_line, None, finalize_process, decode_streams=False) + handle_process_output(proc, lambda bytes: cls._handle_diff_line( + bytes, repo, index), None, finalize_process, decode_streams=False) return index diff --git a/git/ext/gitdb b/git/ext/gitdb index e45fd0792..03ab3a1d4 160000 --- a/git/ext/gitdb +++ b/git/ext/gitdb @@ -1 +1 @@ -Subproject commit e45fd0792ee9a987a4df26e3139f5c3b107f0092 +Subproject commit 03ab3a1d40c04d6a944299c21db61cf9ce30f6bb diff --git a/test/fixtures/diff_file_with_colon b/test/fixtures/diff_file_with_colon new file mode 100644 index 0000000000000000000000000000000000000000..4058b1715bd164ef8c13c73a458426bc43dcc5d0 GIT binary patch literal 351 zcmY+9L2g4a2nBN#pCG{o^G)v1GgM%p{ZiCa>X(|{zOIx_Suo3abFBbORGtVHk0xf# zt1}_luqGPY*44*sL1T85T9COlPLmCE!c-N<>|J8`qgK z10mULiQo3)5|87u=(dFaN+(aTwH5}_u&!;nb*vEUE4_8K%$Smeu+|L?+-VFY9wIF} c#^^1?Cph;RUU3PJ_&P3s@74Fr^XJd$7YhDgzW@LL literal 0 HcmV?d00001 diff --git a/test/test_diff.py b/test/test_diff.py index c6c9b67a0..9b20893a4 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -7,6 +7,7 @@ import ddt import shutil import tempfile +import unittest from git import ( Repo, GitCommandError, @@ -220,6 +221,12 @@ def test_diff_index_raw_format(self): self.assertIsNotNone(res[0].deleted_file) self.assertIsNone(res[0].b_path,) + @unittest.skip("This currently fails and would need someone to improve diff parsing") + def test_diff_file_with_colon(self): + output = fixture('diff_file_with_colon') + res = [] + Diff._handle_diff_line(output, None, res) + def test_diff_initial_commit(self): initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781') From 9e7314c57ef56aaf5fd27a311bfa6a01d18366a2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 17 Apr 2021 13:37:42 +0800 Subject: [PATCH 0009/1849] Restore CI operation Renaming is easier, but GitHub seems to miss CI which is quite a foot/head gun --- .github/workflows/pythonpackage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5e94cd05e..eb5c894e9 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: @@ -56,4 +56,4 @@ jobs: run: | set -x pip install -r doc/requirements.txt - make -C doc html \ No newline at end of file + make -C doc html From c03da67aabaab6852020edf8c28533d88c87e43f Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 17 Apr 2021 05:31:33 +0000 Subject: [PATCH 0010/1849] Set daemon attribute instead of using setDaemon method that was deprecated in Python 3.10 --- git/cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/cmd.py b/git/cmd.py index 40e32e370..e38261a07 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -103,7 +103,7 @@ def pump_stream(cmdline, name, stream, is_decode, handler): for name, stream, handler in pumps: t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler)) - t.setDaemon(True) + t.daemon = True t.start() threads.append(t) From 46b204d1b2eb6de6eaa31deacf4dd0a9095ca3fa Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 21 Apr 2021 13:36:04 +0800 Subject: [PATCH 0011/1849] bump patch level --- VERSION | 2 +- doc/source/changes.rst | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 2a399f7d1..b5f785d2d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.14 +3.1.15 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 93f65a2f7..1b916f30f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,11 +2,14 @@ Changelog ========= -3.1.15 (UNRELEASED) -=================== +3.1.15 +====== * add deprectation warning for python 3.5 +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 + 3.1.14 ====== @@ -15,6 +18,9 @@ Changelog * Add python 3.9 support * Drop python 3.4 support +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/46?closed=1 + 3.1.13 ====== From b10de37fe036b3dd96384763ece9dc1478836287 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 21 Apr 2021 13:38:54 +0800 Subject: [PATCH 0012/1849] Fix publishing branches --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 709813ff2..f5d8a1089 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ release: clean make force_release force_release: clean - git push --tags origin master + git push --tags origin main python3 setup.py sdist bdist_wheel twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* @@ -24,7 +24,7 @@ docker-build: test: docker-build # NOTE!!! - # NOTE!!! If you are not running from master or have local changes then tests will fail + # NOTE!!! If you are not running from main or have local changes then tests will fail # NOTE!!! docker run --rm -v ${CURDIR}:/src -w /src -t gitpython:xenial tox From 184cc1fc280979945dfd16b0bb7275d8b3c27e95 Mon Sep 17 00:00:00 2001 From: Spring Burst <16273755+Andor233@users.noreply.github.com> Date: Wed, 21 Apr 2021 15:08:39 +0800 Subject: [PATCH 0013/1849] Remove windows special handling Remove windows special handling when create Remote --- git/remote.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/git/remote.py b/git/remote.py index 4194af1f0..b2bf607ae 100644 --- a/git/remote.py +++ b/git/remote.py @@ -9,7 +9,7 @@ import re from git.cmd import handle_process_output, Git -from git.compat import (defenc, force_text, is_win) +from git.compat import (defenc, force_text) from git.exc import GitCommandError from git.util import ( LazyMixin, @@ -414,15 +414,6 @@ def __init__(self, repo, name): self.repo = repo # type: 'Repo' self.name = name - if is_win: - # some oddity: on windows, python 2.5, it for some reason does not realize - # that it has the config_writer property, but instead calls __getattr__ - # which will not yield the expected results. 'pinging' the members - # with a dir call creates the config_writer property that we require - # ... bugs like these make me wonder whether python really wants to be used - # for production. It doesn't happen on linux though. - dir(self) - # END windows special handling def __getattr__(self, attr): """Allows to call this instance like From d0fb22b4f5f94da44075d8c43da24b344ae3f0da Mon Sep 17 00:00:00 2001 From: Spring Burst <16273755+Andor233@users.noreply.github.com> Date: Wed, 21 Apr 2021 15:29:12 +0800 Subject: [PATCH 0014/1849] Update remote.py Format code --- git/remote.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index b2bf607ae..194db9386 100644 --- a/git/remote.py +++ b/git/remote.py @@ -414,7 +414,6 @@ def __init__(self, repo, name): self.repo = repo # type: 'Repo' self.name = name - def __getattr__(self, attr): """Allows to call this instance like remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" From 3211ae9dbfc6aadd2dd1d7d0f9f3af37ead19383 Mon Sep 17 00:00:00 2001 From: jmcgill298 Date: Wed, 21 Apr 2021 16:35:27 -0400 Subject: [PATCH 0015/1849] Revert compiling GitCommand shell messages --- git/compat.py | 4 ++-- git/exc.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/git/compat.py b/git/compat.py index a0aea1ac4..c9b83ba46 100644 --- a/git/compat.py +++ b/git/compat.py @@ -18,7 +18,7 @@ # typing -------------------------------------------------------------------- -from typing import Any, AnyStr, Dict, Optional, Type +from typing import IO, Any, AnyStr, Dict, Optional, Type, Union from git.types import TBD # --------------------------------------------------------------------------- @@ -30,7 +30,7 @@ defenc = sys.getfilesystemencoding() -def safe_decode(s: Optional[AnyStr]) -> Optional[str]: +def safe_decode(s: Union[IO[str], AnyStr, None]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): return s diff --git a/git/exc.py b/git/exc.py index c066e5e2f..358e7ae46 100644 --- a/git/exc.py +++ b/git/exc.py @@ -65,10 +65,12 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], status = "'%s'" % s if isinstance(status, str) else s self._cmd = safe_decode(command[0]) - self._cmdline = ' '.join(str(safe_decode(i)) for i in command) + self._cmdline = ' '.join(safe_decode(i) for i in command) self._cause = status and " due to: %s" % status or "!" - self.stdout = stdout and "\n stdout: '%s'" % safe_decode(str(stdout)) or '' - self.stderr = stderr and "\n stderr: '%s'" % safe_decode(str(stderr)) or '' + stdout_decode = safe_decode(stdout) + stderr_decode = safe_decode(stderr) + self.stdout = stdout_decode and "\n stdout: '%s'" % stdout_decode or '' + self.stderr = stderr_decode and "\n stderr: '%s'" % stderr_decode or '' def __str__(self) -> str: return (self._msg + "\n cmdline: %s%s%s") % ( From 36440f79bddc2c1aa4a7a3dd8c2557dca3926639 Mon Sep 17 00:00:00 2001 From: Jingyang Liang Date: Thu, 22 Apr 2021 15:26:18 +0800 Subject: [PATCH 0016/1849] Fix missing stderr when the progress parameter of _clone is None --- git/repo/base.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index a28c9d289..b1d0cdbc6 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -988,8 +988,6 @@ def init(cls, path: PathLike = None, mkdir: bool = True, odbt: Type[GitCmdObject def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Type[GitCmdObjectDB], progress: Optional[Callable], multi_options: Optional[List[str]] = None, **kwargs: Any ) -> 'Repo': - progress_checked = to_progress_instance(progress) - odbt = kwargs.pop('odbt', odb_default_type) # when pathlib.Path or other classbased path is passed @@ -1012,9 +1010,9 @@ def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Typ if multi_options: multi = ' '.join(multi_options).split(' ') proc = git.clone(multi, Git.polish_url(url), clone_path, with_extended_output=True, as_process=True, - v=True, universal_newlines=True, **add_progress(kwargs, git, progress_checked)) - if progress_checked: - handle_process_output(proc, None, progress_checked.new_message_handler(), + v=True, universal_newlines=True, **add_progress(kwargs, git, progress)) + if progress: + handle_process_output(proc, None, to_progress_instance(progress).new_message_handler(), finalize_process, decode_streams=False) else: (stdout, stderr) = proc.communicate() From e0a7824253ae412cf7cc27348ee98c919d382cf2 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Thu, 22 Apr 2021 10:29:38 +0200 Subject: [PATCH 0017/1849] test(clone): verify stderr for a failing clone into a non-empty dir Addresses #1221, #1223 --- test/test_clone.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/test_clone.py diff --git a/test/test_clone.py b/test/test_clone.py new file mode 100644 index 000000000..e4eb9fe13 --- /dev/null +++ b/test/test_clone.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +from pathlib import Path +import re + +import git + +from .lib import ( + TestBase, + with_rw_directory, +) + +class TestClone(TestBase): + @with_rw_directory + def test_checkout_in_non_empty_dir(self, rw_dir): + non_empty_dir = Path(rw_dir) + garbage_file = non_empty_dir / 'not-empty' + garbage_file.write_text('Garbage!') + + # Verify that cloning into the non-empty dir fails while complaining about the target directory not being empty/non-existent + try: + self.rorepo.clone(non_empty_dir) + except git.GitCommandError as exc: + self.assertTrue(exc.stderr, "GitCommandError's 'stderr' is unexpectedly empty") + expr = re.compile(r'(?is).*\bfatal:\s+destination\s+path\b.*\bexists\b.*\bnot\b.*\bempty\s+directory\b') + self.assertTrue(expr.search(exc.stderr), '"%s" does not match "%s"' % (expr.pattern, exc.stderr)) + else: + self.fail("GitCommandError not raised") From b85fec1e333896ac0f27775469482f860e09e5bc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 22 Apr 2021 16:45:15 +0800 Subject: [PATCH 0018/1849] fix flake --- test/test_clone.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_clone.py b/test/test_clone.py index e4eb9fe13..e9f6714d3 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -12,6 +12,7 @@ with_rw_directory, ) + class TestClone(TestBase): @with_rw_directory def test_checkout_in_non_empty_dir(self, rw_dir): @@ -19,7 +20,8 @@ def test_checkout_in_non_empty_dir(self, rw_dir): garbage_file = non_empty_dir / 'not-empty' garbage_file.write_text('Garbage!') - # Verify that cloning into the non-empty dir fails while complaining about the target directory not being empty/non-existent + # Verify that cloning into the non-empty dir fails while complaining about + # the target directory not being empty/non-existent try: self.rorepo.clone(non_empty_dir) except git.GitCommandError as exc: From 0767cc527bf3d86c164a6e4f40f39b8f920e05d3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 23 Apr 2021 07:27:53 +0800 Subject: [PATCH 0019/1849] Ask contributors to keep commits small (even though PRs can be big) Related to #1223 --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4217cbaf9..f685e7e72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ The following is a short step-by-step rundown of what one typically would do to * [fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. * For setting up the environment to run the self tests, please look at `.travis.yml`. * Please try to **write a test that fails unless the contribution is present.** -* Feel free to add yourself to AUTHORS file. +* Try to avoid massive commits and prefer to take small steps, with one commit for each. +* Feel free to add yourself to AUTHORS file. * Create a pull request. From 9f12c8c34371a7c46dad6788a48cf092042027ec Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 14:42:03 +0200 Subject: [PATCH 0020/1849] fix(types): get the os.PathLike type as correctly as possible This should make our internal PathLike type compatible with Python < 3.6 and < 3.9. --- git/types.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/git/types.py b/git/types.py index dc44c1231..3e33ae0c9 100644 --- a/git/types.py +++ b/git/types.py @@ -1,6 +1,20 @@ -import os # @UnusedImport ## not really unused, is in type string +# -*- coding: utf-8 -*- +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +import sys from typing import Union, Any TBD = Any -PathLike = Union[str, 'os.PathLike[str]'] + +if sys.version_info[:2] < (3, 6): + # os.PathLike (PEP-519) only got introduced with Python 3.6 + PathLike = str +elif sys.version_info[:2] < (3, 9): + # Python >= 3.6, < 3.9 + PathLike = Union[str, os.PathLike] +elif sys.version_info[:2] >= (3, 9): + # os.PathLike only becomes subscriptable from Python 3.9 onwards + PathLike = Union[str, os.PathLike[str]] From 70aa1ab69c84ac712d91c92b36a5ed7045cc647c Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 10:44:47 +0200 Subject: [PATCH 0021/1849] test: sort MANIFEST.in and add missing test-requirements.txt Without the presence of 'test-requirements.txt' 'tox' is unusable. --- MANIFEST.in | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5fd771db3..f02721fc6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ -include VERSION -include LICENSE -include CHANGES include AUTHORS +include CHANGES include CONTRIBUTING.md +include LICENSE include README.md +include VERSION include requirements.txt +include test-requirements.txt recursive-include doc * recursive-exclude test * From 4f9d492e479eda07c5bbe838319eecac459a6042 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 11:18:22 +0200 Subject: [PATCH 0022/1849] test(tox): verify type annotations --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tox.ini b/tox.ini index d9d1594d4..a0cb1c9f1 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,14 @@ commands = coverage run --omit="git/test/*" -m unittest --buffer {posargs} [testenv:flake8] commands = flake8 --ignore=W293,E265,E266,W503,W504,E731 {posargs} +[testenv:type] +description = type check ourselves +deps = + {[testenv]deps} + mypy +commands = + mypy -p git + [testenv:venv] commands = {posargs} From 6d06f5af4311e6a1d17213dde57a261e30dbf669 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 11:20:09 +0200 Subject: [PATCH 0023/1849] test(mypy): don't give errors for every unannotated function Because there's too many to fix quickly. --- mypy.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 349266b77..69e977574 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,5 @@ [mypy] -disallow_untyped_defs = True +# TODO: enable when we've fully annotated everything +#disallow_untyped_defs = True From 74a1b17a29390660abe79d83d837a666141f8625 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 11:21:10 +0200 Subject: [PATCH 0024/1849] test(mypy): don't complain about missing type hints for 'gitdb' --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 69e977574..b63d68fd3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,3 +3,7 @@ # TODO: enable when we've fully annotated everything #disallow_untyped_defs = True + +# TODO: remove when 'gitdb' is fully annotated +[mypy-gitdb.*] +ignore_missing_imports = True From 6a233359ce1ec30386f97d4acdf989f1c3570842 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 11:37:54 +0200 Subject: [PATCH 0025/1849] fix(mypy): properly describe link between parameter and return types This gives mypy all information that it needs to determine what the return type of a function call is *iff* it knows the argument's type. As a result it can now stop complaining about passing None to str.join() in exc.py. --- git/compat.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/git/compat.py b/git/compat.py index c9b83ba46..c4bd2aa36 100644 --- a/git/compat.py +++ b/git/compat.py @@ -18,7 +18,16 @@ # typing -------------------------------------------------------------------- -from typing import IO, Any, AnyStr, Dict, Optional, Type, Union +from typing import ( + Any, + AnyStr, + Dict, + IO, + Optional, + Type, + Union, + overload, +) from git.types import TBD # --------------------------------------------------------------------------- @@ -30,6 +39,12 @@ defenc = sys.getfilesystemencoding() +@overload +def safe_decode(s: None) -> None: ... + +@overload +def safe_decode(s: Union[IO[str], AnyStr]) -> str: ... + def safe_decode(s: Union[IO[str], AnyStr, None]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): @@ -42,6 +57,12 @@ def safe_decode(s: Union[IO[str], AnyStr, None]) -> Optional[str]: raise TypeError('Expected bytes or text, but got %r' % (s,)) +@overload +def safe_encode(s: None) -> None: ... + +@overload +def safe_encode(s: AnyStr) -> bytes: ... + def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Safely encodes a binary string to unicode""" if isinstance(s, str): @@ -54,6 +75,12 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: raise TypeError('Expected bytes or text, but got %r' % (s,)) +@overload +def win_encode(s: None) -> None: ... + +@overload +def win_encode(s: AnyStr) -> bytes: ... + def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Encode unicodes for process arguments on Windows.""" if isinstance(s, str): @@ -65,7 +92,6 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: return None - def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation """copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15""" From c885781858ade2f660818e983915a6dae5672241 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 12:22:12 +0200 Subject: [PATCH 0026/1849] improvement: teach mypy that Object.type is not always supposed to be None --- git/objects/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/objects/base.py b/git/objects/base.py index cccb5ec66..59f0e8368 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -7,6 +7,7 @@ import gitdb.typ as dbtyp import os.path as osp +from typing import Optional # noqa: F401 unused import from .util import get_object_type_by_name @@ -24,7 +25,7 @@ class Object(LazyMixin): TYPES = (dbtyp.str_blob_type, dbtyp.str_tree_type, dbtyp.str_commit_type, dbtyp.str_tag_type) __slots__ = ("repo", "binsha", "size") - type = None # to be set by subclass + type = None # type: Optional[str] # to be set by subclass def __init__(self, repo, binsha): """Initialize an object by identifying it by its binary sha. From 8470777b44bed4da87aad9474f88e7f0774252a6 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 12:24:07 +0200 Subject: [PATCH 0027/1849] improvement: teach mypy how to deal with wildcard-imported objects By telling it where it's imported from in one case and telling it to ignore it in another. --- git/exc.py | 1 + git/objects/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/git/exc.py b/git/exc.py index 358e7ae46..6e646921c 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,6 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ +from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode diff --git a/git/objects/__init__.py b/git/objects/__init__.py index 23b2416ae..897eb98fa 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -16,8 +16,8 @@ from .tree import * # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base -smutil.IndexObject = IndexObject -smutil.Object = Object +smutil.IndexObject = IndexObject # type: ignore[attr-defined] +smutil.Object = Object # type: ignore[attr-defined] del(smutil) # must come after submodule was made available From 76ba0924be14d55d01db0506b3e6a930cc72bf0d Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 12:24:53 +0200 Subject: [PATCH 0028/1849] improvement(mypy): ignore false positives --- git/cmd.py | 2 +- git/config.py | 2 +- git/refs/reference.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index e38261a07..ac3ca2ec1 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -138,7 +138,7 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()): ## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards, # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal -PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP +PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] if is_win else 0) diff --git a/git/config.py b/git/config.py index aadb0aac0..1cb80475c 100644 --- a/git/config.py +++ b/git/config.py @@ -216,7 +216,7 @@ def get_config_path(config_level: Literal['system', 'global', 'user', 'repositor raise ValueError("Invalid configuration level: %r" % config_level) -class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): +class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): # type: ignore ## mypy does not understand dynamic class creation # noqa: E501 """Implements specifics required to read git style configuration files. diff --git a/git/refs/reference.py b/git/refs/reference.py index aaa9b63fe..9014f5558 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -103,7 +103,7 @@ def iter_items(cls, repo, common_path=None): #{ Remote Interface - @property + @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) @require_remote_ref_path def remote_name(self): """ @@ -114,7 +114,7 @@ def remote_name(self): # /refs/remotes// return tokens[2] - @property + @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) @require_remote_ref_path def remote_head(self): """:return: Name of the remote head itself, i.e. master. From 043e15fe140cfff8725d4f615f42fa1c55779402 Mon Sep 17 00:00:00 2001 From: Giel van Schijndel Date: Fri, 23 Apr 2021 12:50:07 +0200 Subject: [PATCH 0029/1849] ci: check types with mypy This will result in _partial_ type checking of the type annotations by using mypy. Keep in mind though that mypy is performing _static_ analysis in a _dynamic_ language so it can only partially check for correctness. Some other tool(s) will be needed to have more complete type checking at runtime. E.g. [typeguard]. [typeguard]: https://pypi.org/project/typeguard/ --- .github/workflows/pythonpackage.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index eb5c894e9..3c7215cbe 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -47,6 +47,11 @@ jobs: pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 --ignore=W293,E265,E266,W503,W504,E731 --count --show-source --statistics + - name: Check types with mypy + run: | + set -x + pip install tox + tox -e type - name: Test with nose run: | set -x From f8e223263c73a7516e2b216a546079e9a144b3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 21 Apr 2021 10:03:11 +0200 Subject: [PATCH 0030/1849] Use typing-extensions only on Python < 3.8 All necessary attributes are available in the built-in typing module since Python 3.8. Use typing-extensions only for older versions of Python, and avoid the unnecessary dep in 3.8+. --- git/{compat.py => compat/__init__.py} | 0 git/compat/typing.py | 13 +++++++++++++ git/config.py | 3 +-- git/diff.py | 2 +- git/repo/base.py | 2 +- requirements.txt | 2 +- test-requirements.txt | 2 +- 7 files changed, 18 insertions(+), 6 deletions(-) rename git/{compat.py => compat/__init__.py} (100%) create mode 100644 git/compat/typing.py diff --git a/git/compat.py b/git/compat/__init__.py similarity index 100% rename from git/compat.py rename to git/compat/__init__.py diff --git a/git/compat/typing.py b/git/compat/typing.py new file mode 100644 index 000000000..4bab4cddd --- /dev/null +++ b/git/compat/typing.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# config.py +# Copyright (C) 2021 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import sys + +if sys.version_info[:2] >= (3, 8): + from typing import Final, Literal +else: + from typing_extensions import Final, Literal diff --git a/git/config.py b/git/config.py index 1cb80475c..0c8d975db 100644 --- a/git/config.py +++ b/git/config.py @@ -16,14 +16,13 @@ import fnmatch from collections import OrderedDict -from typing_extensions import Literal - from git.compat import ( defenc, force_text, with_metaclass, is_win, ) +from git.compat.typing import Literal from git.util import LockFile import os.path as osp diff --git a/git/diff.py b/git/diff.py index deedb635c..943916ea8 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,7 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING -from typing_extensions import Final, Literal +from git.compat.typing import Final, Literal from git.types import TBD if TYPE_CHECKING: diff --git a/git/repo/base.py b/git/repo/base.py index b1d0cdbc6..ed0a810e4 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -34,8 +34,8 @@ # typing ------------------------------------------------------ +from git.compat.typing import Literal from git.types import TBD, PathLike -from typing_extensions import Literal from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, TextIO, Tuple, Type, Union, diff --git a/requirements.txt b/requirements.txt index 626a916a9..d980f6682 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0 +typing-extensions>=3.7.4.0;python_version<"3.8" diff --git a/test-requirements.txt b/test-requirements.txt index 0734820f7..e06d2be14 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,4 +5,4 @@ tox virtualenv nose gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0 +typing-extensions>=3.7.4.0;python_version<"3.8" From 9448c082b158dcab960d33982e8189f2d2da4729 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 24 Apr 2021 09:49:45 +0800 Subject: [PATCH 0031/1849] Fix flake8 --- git/compat/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/compat/typing.py b/git/compat/typing.py index 4bab4cddd..925c5ba2e 100644 --- a/git/compat/typing.py +++ b/git/compat/typing.py @@ -8,6 +8,6 @@ import sys if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal + from typing import Final, Literal # noqa: F401 else: - from typing_extensions import Final, Literal + from typing_extensions import Final, Literal # noqa: F401 From 6752fad0e93d1d2747f56be30a52fea212bd15d6 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 15:59:07 +0100 Subject: [PATCH 0032/1849] add initial types to remote.py --- .github/FUNDING.yml | 1 + CONTRIBUTING.md | 3 +- MANIFEST.in | 7 +- Makefile | 4 +- VERSION | 2 +- doc/source/changes.rst | 13 ++- git/__init__.py | 9 +- git/cmd.py | 56 +++++----- git/{compat.py => compat/__init__.py} | 39 ++++++- git/compat/typing.py | 13 +++ git/config.py | 7 +- git/db.py | 17 +-- git/diff.py | 126 ++++++++++----------- git/exc.py | 17 ++- git/objects/__init__.py | 4 +- git/objects/base.py | 3 +- git/refs/reference.py | 4 +- git/refs/symbolic.py | 2 +- git/remote.py | 37 +++---- git/repo/base.py | 151 +++++++++++++------------- git/repo/fun.py | 21 ++-- git/types.py | 18 ++- git/util.py | 47 ++++++-- mypy.ini | 7 +- requirements.txt | 1 + test-requirements.txt | 2 + test/fixtures/diff_file_with_colon | Bin 0 -> 351 bytes test/test_clone.py | 32 ++++++ test/test_diff.py | 7 ++ test/test_repo.py | 15 +++ test/test_util.py | 20 +++- tox.ini | 11 +- 32 files changed, 453 insertions(+), 243 deletions(-) create mode 100644 .github/FUNDING.yml rename git/{compat.py => compat/__init__.py} (75%) create mode 100644 git/compat/typing.py create mode 100644 test/fixtures/diff_file_with_colon create mode 100644 test/test_clone.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..80819f5d8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: byron diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4217cbaf9..f685e7e72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ The following is a short step-by-step rundown of what one typically would do to * [fork this project](https://github.com/gitpython-developers/GitPython/fork) on GitHub. * For setting up the environment to run the self tests, please look at `.travis.yml`. * Please try to **write a test that fails unless the contribution is present.** -* Feel free to add yourself to AUTHORS file. +* Try to avoid massive commits and prefer to take small steps, with one commit for each. +* Feel free to add yourself to AUTHORS file. * Create a pull request. diff --git a/MANIFEST.in b/MANIFEST.in index 5fd771db3..f02721fc6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ -include VERSION -include LICENSE -include CHANGES include AUTHORS +include CHANGES include CONTRIBUTING.md +include LICENSE include README.md +include VERSION include requirements.txt +include test-requirements.txt recursive-include doc * recursive-exclude test * diff --git a/Makefile b/Makefile index 709813ff2..f5d8a1089 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ release: clean make force_release force_release: clean - git push --tags origin master + git push --tags origin main python3 setup.py sdist bdist_wheel twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* @@ -24,7 +24,7 @@ docker-build: test: docker-build # NOTE!!! - # NOTE!!! If you are not running from master or have local changes then tests will fail + # NOTE!!! If you are not running from main or have local changes then tests will fail # NOTE!!! docker run --rm -v ${CURDIR}:/src -w /src -t gitpython:xenial tox diff --git a/VERSION b/VERSION index 55f20a1a9..b5f785d2d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.13 +3.1.15 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 405179d0c..1b916f30f 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,7 +2,15 @@ Changelog ========= -3.1.?? +3.1.15 +====== + +* add deprectation warning for python 3.5 + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/47?closed=1 + +3.1.14 ====== * git.Commit objects now have a ``replace`` method that will return a @@ -10,6 +18,9 @@ Changelog * Add python 3.9 support * Drop python 3.4 support +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/46?closed=1 + 3.1.13 ====== diff --git a/git/__init__.py b/git/__init__.py index e2f960db7..ae9254a26 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -5,6 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php # flake8: noqa #@PydevCodeAnalysisIgnore +from git.exc import * # @NoMove @IgnorePep8 import inspect import os import sys @@ -16,8 +17,6 @@ __version__ = 'git' - - #{ Initialization def _init_externals() -> None: """Initialize external projects by putting them into the path""" @@ -32,13 +31,13 @@ def _init_externals() -> None: #} END initialization + ################# _init_externals() ################# #{ Imports -from git.exc import * # @NoMove @IgnorePep8 try: from git.config import GitConfigParser # @NoMove @IgnorePep8 from git.objects import * # @NoMove @IgnorePep8 @@ -68,7 +67,8 @@ def _init_externals() -> None: #{ Initialize git executable path GIT_OK = None -def refresh(path:Optional[PathLike]=None) -> None: + +def refresh(path: Optional[PathLike] = None) -> None: """Convenience method for setting the git executable path.""" global GIT_OK GIT_OK = False @@ -81,6 +81,7 @@ def refresh(path:Optional[PathLike]=None) -> None: GIT_OK = True #} END initialize git executable path + ################# try: refresh() diff --git a/git/cmd.py b/git/cmd.py index bac162176..ac3ca2ec1 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -19,7 +19,7 @@ import threading from collections import OrderedDict from textwrap import dedent -from typing import Any, Dict, List, Optional +import warnings from git.compat import ( defenc, @@ -29,7 +29,7 @@ is_win, ) from git.exc import CommandError -from git.util import is_cygwin_git, cygpath, expand_path +from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present from .exc import ( GitCommandError, @@ -40,8 +40,6 @@ stream_copy, ) -from .types import PathLike - execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', @@ -85,8 +83,8 @@ def pump_stream(cmdline, name, stream, is_decode, handler): line = line.decode(defenc) handler(line) except Exception as ex: - log.error("Pumping %r of cmd(%s) failed due to: %r", name, cmdline, ex) - raise CommandError(['<%s-pump>' % name] + cmdline, ex) from ex + log.error("Pumping %r of cmd(%s) failed due to: %r", name, remove_password_if_present(cmdline), ex) + raise CommandError(['<%s-pump>' % name] + remove_password_if_present(cmdline), ex) from ex finally: stream.close() @@ -105,7 +103,7 @@ def pump_stream(cmdline, name, stream, is_decode, handler): for name, stream, handler in pumps: t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler)) - t.setDaemon(True) + t.daemon = True t.start() threads.append(t) @@ -140,7 +138,7 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()): ## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards, # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal -PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP +PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] if is_win else 0) @@ -212,7 +210,7 @@ def refresh(cls, path=None): # - a GitCommandNotFound error is spawned by ourselves # - a PermissionError is spawned if the git executable provided # cannot be executed for whatever reason - + has_git = False try: cls().version() @@ -408,7 +406,7 @@ def read_all_from_possibly_closed_stream(stream): if status != 0: errstr = read_all_from_possibly_closed_stream(self.proc.stderr) log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) - raise GitCommandError(self.args, status, errstr) + raise GitCommandError(remove_password_if_present(self.args), status, errstr) # END status handling return status # END auto interrupt @@ -500,7 +498,7 @@ def readlines(self, size=-1): # skipcq: PYL-E0301 def __iter__(self): return self - + def __next__(self): return self.next() @@ -519,7 +517,7 @@ def __del__(self): self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir: Optional[PathLike]=None) -> None: + def __init__(self, working_dir=None): """Initialize this instance with: :param working_dir: @@ -528,12 +526,12 @@ def __init__(self, working_dir: Optional[PathLike]=None) -> None: It is meant to be the working tree directory if available, or the .git directory in case of bare repositories.""" super(Git, self).__init__() - self._working_dir = expand_path(working_dir) if working_dir is not None else None + self._working_dir = expand_path(working_dir) self._git_options = () - self._persistent_git_options = [] # type: List[str] + self._persistent_git_options = [] # Extra environment variables to pass to git commands - self._environment = {} # type: Dict[str, Any] + self._environment = {} # cached command slots self.cat_file_header = None @@ -547,7 +545,7 @@ def __getattr__(self, name): return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - def set_persistent_git_options(self, **kwargs) -> None: + def set_persistent_git_options(self, **kwargs): """Specify command line options to the git executable for subsequent subcommand calls @@ -641,7 +639,7 @@ def execute(self, command, :param env: A dictionary of environment variables to be passed to `subprocess.Popen`. - + :param max_chunk_size: Maximum number of bytes in one chunk of data passed to the output_stream in one invocation of write() method. If the given number is not positive then @@ -685,8 +683,10 @@ def execute(self, command, :note: If you add additional keyword arguments to the signature of this method, you must update the execute_kwargs tuple housed in this module.""" + # Remove password for the command if present + redacted_command = remove_password_if_present(command) if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != 'full' or as_process): - log.info(' '.join(command)) + log.info(' '.join(redacted_command)) # Allow the user to have the command executed in their working dir. cwd = self._working_dir or os.getcwd() @@ -707,7 +707,7 @@ def execute(self, command, if is_win: cmd_not_found_exception = OSError if kill_after_timeout: - raise GitCommandError(command, '"kill_after_timeout" feature is not supported on Windows.') + raise GitCommandError(redacted_command, '"kill_after_timeout" feature is not supported on Windows.') else: if sys.version_info[0] > 2: cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable @@ -722,7 +722,7 @@ def execute(self, command, if istream: istream_ok = "" log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", - command, cwd, universal_newlines, shell, istream_ok) + redacted_command, cwd, universal_newlines, shell, istream_ok) try: proc = Popen(command, env=env, @@ -738,7 +738,7 @@ def execute(self, command, **subprocess_kwargs ) except cmd_not_found_exception as err: - raise GitCommandNotFound(command, err) from err + raise GitCommandNotFound(redacted_command, err) from err if as_process: return self.AutoInterrupt(proc, command) @@ -788,7 +788,7 @@ def _kill_process(pid): watchdog.cancel() if kill_check.isSet(): stderr_value = ('Timeout: the command "%s" did not complete in %d ' - 'secs.' % (" ".join(command), kill_after_timeout)) + 'secs.' % (" ".join(redacted_command), kill_after_timeout)) if not universal_newlines: stderr_value = stderr_value.encode(defenc) # strip trailing "\n" @@ -812,7 +812,7 @@ def _kill_process(pid): proc.stderr.close() if self.GIT_PYTHON_TRACE == 'full': - cmdstr = " ".join(command) + cmdstr = " ".join(redacted_command) def as_text(stdout_value): return not output_stream and safe_decode(stdout_value) or '' @@ -828,7 +828,7 @@ def as_text(stdout_value): # END handle debug printing if with_exceptions and status != 0: - raise GitCommandError(command, status, stderr_value, stdout_value) + raise GitCommandError(redacted_command, status, stderr_value, stdout_value) if isinstance(stdout_value, bytes) and stdout_as_string: # could also be output_stream stdout_value = safe_decode(stdout_value) @@ -905,8 +905,14 @@ def transform_kwarg(self, name, value, split_single_char_options): def transform_kwargs(self, split_single_char_options=True, **kwargs): """Transforms Python style kwargs into git command line options.""" + # Python 3.6 preserves the order of kwargs and thus has a stable + # order. For older versions sort the kwargs by the key to get a stable + # order. + if sys.version_info[:2] < (3, 6): + kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) + warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" + + "It does not preserve the order for key-word arguments and enforce lexical sorting instead.") args = [] - kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) for k, v in kwargs.items(): if isinstance(v, (list, tuple)): for value in v: diff --git a/git/compat.py b/git/compat/__init__.py similarity index 75% rename from git/compat.py rename to git/compat/__init__.py index 4fe394ae0..c4bd2aa36 100644 --- a/git/compat.py +++ b/git/compat/__init__.py @@ -16,9 +16,22 @@ force_text # @UnusedImport ) -from typing import Any, AnyStr, Dict, Optional, Type +# typing -------------------------------------------------------------------- + +from typing import ( + Any, + AnyStr, + Dict, + IO, + Optional, + Type, + Union, + overload, +) from git.types import TBD +# --------------------------------------------------------------------------- + is_win = (os.name == 'nt') # type: bool is_posix = (os.name == 'posix') @@ -26,7 +39,13 @@ defenc = sys.getfilesystemencoding() -def safe_decode(s: Optional[AnyStr]) -> Optional[str]: +@overload +def safe_decode(s: None) -> None: ... + +@overload +def safe_decode(s: Union[IO[str], AnyStr]) -> str: ... + +def safe_decode(s: Union[IO[str], AnyStr, None]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): return s @@ -38,6 +57,11 @@ def safe_decode(s: Optional[AnyStr]) -> Optional[str]: raise TypeError('Expected bytes or text, but got %r' % (s,)) +@overload +def safe_encode(s: None) -> None: ... + +@overload +def safe_encode(s: AnyStr) -> bytes: ... def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Safely encodes a binary string to unicode""" @@ -51,6 +75,12 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: raise TypeError('Expected bytes or text, but got %r' % (s,)) +@overload +def win_encode(s: None) -> None: ... + +@overload +def win_encode(s: AnyStr) -> bytes: ... + def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Encode unicodes for process arguments on Windows.""" if isinstance(s, str): @@ -62,9 +92,9 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: return None -def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation +def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation """copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15""" - + class metaclass(meta): # type: ignore __call__ = type.__call__ __init__ = type.__init__ # type: ignore @@ -75,4 +105,3 @@ def __new__(cls, name: str, nbases: Optional[int], d: Dict[str, Any]) -> TBD: return meta(name, bases, d) return metaclass(meta.__name__ + 'Helper', None, {}) - diff --git a/git/compat/typing.py b/git/compat/typing.py new file mode 100644 index 000000000..925c5ba2e --- /dev/null +++ b/git/compat/typing.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# config.py +# Copyright (C) 2021 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import sys + +if sys.version_info[:2] >= (3, 8): + from typing import Final, Literal # noqa: F401 +else: + from typing_extensions import Final, Literal # noqa: F401 diff --git a/git/config.py b/git/config.py index ffbbfab40..0c8d975db 100644 --- a/git/config.py +++ b/git/config.py @@ -16,14 +16,13 @@ import fnmatch from collections import OrderedDict -from typing_extensions import Literal - from git.compat import ( defenc, force_text, with_metaclass, is_win, ) +from git.compat.typing import Literal from git.util import LockFile import os.path as osp @@ -196,7 +195,7 @@ def items_all(self): return [(k, self.getall(k)) for k in self] -def get_config_path(config_level: Literal['system','global','user','repository']) -> str: +def get_config_path(config_level: Literal['system', 'global', 'user', 'repository']) -> str: # we do not support an absolute path of the gitconfig on windows , # use the global config instead @@ -216,7 +215,7 @@ def get_config_path(config_level: Literal['system','global','user','repository'] raise ValueError("Invalid configuration level: %r" % config_level) -class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): +class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): # type: ignore ## mypy does not understand dynamic class creation # noqa: E501 """Implements specifics required to read git style configuration files. diff --git a/git/db.py b/git/db.py index ef2b0b2ef..dc60c5552 100644 --- a/git/db.py +++ b/git/db.py @@ -1,5 +1,4 @@ """Module with our own gitdb implementation - it uses the git command""" -from typing import AnyStr from git.util import bin_to_hex, hex_to_bin from gitdb.base import ( OInfo, @@ -7,21 +6,23 @@ ) from gitdb.db import GitDB # @UnusedImport from gitdb.db import LooseObjectDB -from gitdb.exc import BadObject -from .exc import GitCommandError +from gitdb.exc import BadObject +from git.exc import GitCommandError # typing------------------------------------------------- -from .cmd import Git -from .types import PathLike +from typing import TYPE_CHECKING, AnyStr +from git.types import PathLike + +if TYPE_CHECKING: + from git.cmd import Git + # -------------------------------------------------------- __all__ = ('GitCmdObjectDB', 'GitDB') -# class GitCmdObjectDB(CompoundDB, ObjectDBW): - class GitCmdObjectDB(LooseObjectDB): @@ -33,7 +34,7 @@ class GitCmdObjectDB(LooseObjectDB): have packs and the other implementations """ - def __init__(self, root_path: PathLike, git: Git) -> None: + def __init__(self, root_path: PathLike, git: 'Git') -> None: """Initialize this instance with the root and a git command""" super(GitCmdObjectDB, self).__init__(root_path) self._git = git diff --git a/git/diff.py b/git/diff.py index b25aadc76..943916ea8 100644 --- a/git/diff.py +++ b/git/diff.py @@ -15,11 +15,14 @@ # typing ------------------------------------------------------------------ -from .objects.tree import Tree -from git.repo.base import Repo -from typing_extensions import Final, Literal +from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING +from git.compat.typing import Final, Literal from git.types import TBD -from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union + +if TYPE_CHECKING: + from .objects.tree import Tree + from git.repo.base import Repo + Lit_change_type = Literal['A', 'D', 'M', 'R', 'T'] # ------------------------------------------------------------------------ @@ -27,7 +30,7 @@ __all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE') # Special object to compare against the empty tree in diffs -NULL_TREE: Final[object] = object() +NULL_TREE = object() # type: Final[object] _octal_byte_re = re.compile(b'\\\\([0-9]{3})') @@ -79,7 +82,7 @@ def _process_diff_args(self, args: List[Union[str, 'Diffable', object]]) -> List Subclasses can use it to alter the behaviour of the superclass""" return args - def diff(self, other: Union[Type[Index], Type[Tree], object, None, str] = Index, + def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Index, paths: Union[str, List[str], Tuple[str, ...], None] = None, create_patch: bool = False, **kwargs: Any) -> 'DiffIndex': """Creates diffs between two items being trees, trees and index or an @@ -271,7 +274,7 @@ class Diff(object): "new_file", "deleted_file", "copied_file", "raw_rename_from", "raw_rename_to", "diff", "change_type", "score") - def __init__(self, repo: Repo, + def __init__(self, repo: 'Repo', a_rawpath: Optional[bytes], b_rawpath: Optional[bytes], a_blob_id: Union[str, bytes, None], b_blob_id: Union[str, bytes, None], a_mode: Union[bytes, str, None], b_mode: Union[bytes, str, None], @@ -425,7 +428,7 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m return None @classmethod - def _index_from_patch_format(cls, repo: Repo, proc: TBD) -> DiffIndex: + def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: """Create a new DiffIndex from the given text which must be in patch format :param repo: is the repository we are operating on - it is required :param stream: result of 'git diff' as a stream (supporting file protocol) @@ -487,6 +490,58 @@ def _index_from_patch_format(cls, repo: Repo, proc: TBD) -> DiffIndex: return index + @staticmethod + def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: TBD) -> None: + lines = lines_bytes.decode(defenc) + + for line in lines.split(':')[1:]: + meta, _, path = line.partition('\x00') + path = path.rstrip('\x00') + a_blob_id, b_blob_id = None, None # Type: Optional[str] + old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) + # Change type can be R100 + # R: status letter + # 100: score (in case of copy and rename) + change_type = _change_type[0] + score_str = ''.join(_change_type[1:]) + score = int(score_str) if score_str.isdigit() else None + path = path.strip() + a_path = path.encode(defenc) + b_path = path.encode(defenc) + deleted_file = False + new_file = False + copied_file = False + rename_from = None + rename_to = None + + # NOTE: We cannot conclude from the existence of a blob to change type + # as diffs with the working do not have blobs yet + if change_type == 'D': + b_blob_id = None # Optional[str] + deleted_file = True + elif change_type == 'A': + a_blob_id = None + new_file = True + elif change_type == 'C': + copied_file = True + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) + elif change_type == 'R': + a_path_str, b_path_str = path.split('\x00', 1) + a_path = a_path_str.encode(defenc) + b_path = b_path_str.encode(defenc) + rename_from, rename_to = a_path, b_path + elif change_type == 'T': + # Nothing to do + pass + # END add/remove handling + + diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, + new_file, deleted_file, copied_file, rename_from, rename_to, + '', change_type, score) + index.append(diff) + @classmethod def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: """Create a new DiffIndex from the given stream which must be in raw format. @@ -495,58 +550,7 @@ def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: # :100644 100644 687099101... 37c5e30c8... M .gitignore index = DiffIndex() - - def handle_diff_line(lines_bytes: bytes) -> None: - lines = lines_bytes.decode(defenc) - - for line in lines.split(':')[1:]: - meta, _, path = line.partition('\x00') - path = path.rstrip('\x00') - a_blob_id, b_blob_id = None, None # Type: Optional[str] - old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) - # Change type can be R100 - # R: status letter - # 100: score (in case of copy and rename) - change_type = _change_type[0] - score_str = ''.join(_change_type[1:]) - score = int(score_str) if score_str.isdigit() else None - path = path.strip() - a_path = path.encode(defenc) - b_path = path.encode(defenc) - deleted_file = False - new_file = False - copied_file = False - rename_from = None - rename_to = None - - # NOTE: We cannot conclude from the existence of a blob to change type - # as diffs with the working do not have blobs yet - if change_type == 'D': - b_blob_id = None # Optional[str] - deleted_file = True - elif change_type == 'A': - a_blob_id = None - new_file = True - elif change_type == 'C': - copied_file = True - a_path_str, b_path_str = path.split('\x00', 1) - a_path = a_path_str.encode(defenc) - b_path = b_path_str.encode(defenc) - elif change_type == 'R': - a_path_str, b_path_str = path.split('\x00', 1) - a_path = a_path_str.encode(defenc) - b_path = b_path_str.encode(defenc) - rename_from, rename_to = a_path, b_path - elif change_type == 'T': - # Nothing to do - pass - # END add/remove handling - - diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode, - new_file, deleted_file, copied_file, rename_from, rename_to, - '', change_type, score) - index.append(diff) - - handle_process_output(proc, handle_diff_line, None, finalize_process, decode_streams=False) + handle_process_output(proc, lambda bytes: cls._handle_diff_line( + bytes, repo, index), None, finalize_process, decode_streams=False) return index diff --git a/git/exc.py b/git/exc.py index c02b2b3a3..6e646921c 100644 --- a/git/exc.py +++ b/git/exc.py @@ -5,14 +5,17 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """ Module containing all exceptions thrown throughout the git package, """ +from gitdb.exc import BadName # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from gitdb.exc import * # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614 from git.compat import safe_decode # typing ---------------------------------------------------- -from git.repo.base import Repo +from typing import IO, List, Optional, Tuple, Union, TYPE_CHECKING from git.types import PathLike -from typing import IO, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from git.repo.base import Repo # ------------------------------------------------------------------ @@ -63,10 +66,12 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], status = "'%s'" % s if isinstance(status, str) else s self._cmd = safe_decode(command[0]) - self._cmdline = ' '.join(str(safe_decode(i)) for i in command) + self._cmdline = ' '.join(safe_decode(i) for i in command) self._cause = status and " due to: %s" % status or "!" - self.stdout = stdout and "\n stdout: '%s'" % safe_decode(str(stdout)) or '' - self.stderr = stderr and "\n stderr: '%s'" % safe_decode(str(stderr)) or '' + stdout_decode = safe_decode(stdout) + stderr_decode = safe_decode(stderr) + self.stdout = stdout_decode and "\n stdout: '%s'" % stdout_decode or '' + self.stderr = stderr_decode and "\n stderr: '%s'" % stderr_decode or '' def __str__(self) -> str: return (self._msg + "\n cmdline: %s%s%s") % ( @@ -142,7 +147,7 @@ def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Opti class RepositoryDirtyError(GitError): """Thrown whenever an operation on a repository fails as it has uncommitted changes that would be overwritten""" - def __init__(self, repo: Repo, message: str) -> None: + def __init__(self, repo: 'Repo', message: str) -> None: self.repo = repo self.message = message diff --git a/git/objects/__init__.py b/git/objects/__init__.py index 23b2416ae..897eb98fa 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -16,8 +16,8 @@ from .tree import * # Fix import dependency - add IndexObject to the util module, so that it can be # imported by the submodule.base -smutil.IndexObject = IndexObject -smutil.Object = Object +smutil.IndexObject = IndexObject # type: ignore[attr-defined] +smutil.Object = Object # type: ignore[attr-defined] del(smutil) # must come after submodule was made available diff --git a/git/objects/base.py b/git/objects/base.py index cccb5ec66..59f0e8368 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -7,6 +7,7 @@ import gitdb.typ as dbtyp import os.path as osp +from typing import Optional # noqa: F401 unused import from .util import get_object_type_by_name @@ -24,7 +25,7 @@ class Object(LazyMixin): TYPES = (dbtyp.str_blob_type, dbtyp.str_tree_type, dbtyp.str_commit_type, dbtyp.str_tag_type) __slots__ = ("repo", "binsha", "size") - type = None # to be set by subclass + type = None # type: Optional[str] # to be set by subclass def __init__(self, repo, binsha): """Initialize an object by identifying it by its binary sha. diff --git a/git/refs/reference.py b/git/refs/reference.py index aaa9b63fe..9014f5558 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -103,7 +103,7 @@ def iter_items(cls, repo, common_path=None): #{ Remote Interface - @property + @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) @require_remote_ref_path def remote_name(self): """ @@ -114,7 +114,7 @@ def remote_name(self): # /refs/remotes// return tokens[2] - @property + @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) @require_remote_ref_path def remote_head(self): """:return: Name of the remote head itself, i.e. master. diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index fb9b4f84b..22d9c1d51 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -87,7 +87,7 @@ def _iter_packed_refs(cls, repo): """Returns an iterator yielding pairs of sha1/path pairs (as bytes) for the corresponding refs. :note: The packed refs file will be kept open as long as we iterate""" try: - with open(cls._get_packed_refs_path(repo), 'rt') as fp: + with open(cls._get_packed_refs_path(repo), 'rt', encoding='UTF-8') as fp: for line in fp: line = line.strip() if not line: diff --git a/git/remote.py b/git/remote.py index 53349ce70..20b5a5514 100644 --- a/git/remote.py +++ b/git/remote.py @@ -9,7 +9,7 @@ import re from git.cmd import handle_process_output, Git -from git.compat import (defenc, force_text, is_win) +from git.compat import (defenc, force_text) from git.exc import GitCommandError from git.util import ( LazyMixin, @@ -36,7 +36,15 @@ # typing------------------------------------------------------- -from git.repo.Base import Repo +from typing import Any, Optional, Set, TYPE_CHECKING, Union + +from git.types import PathLike + +if TYPE_CHECKING: + from git.repo.base import Repo + from git.objects.commit import Commit + +# ------------------------------------------------------------- log = logging.getLogger('git.remote') log.addHandler(logging.NullHandler()) @@ -47,7 +55,7 @@ #{ Utilities -def add_progress(kwargs, git, progress): +def add_progress(kwargs: Any, git: Git, progress: RemoteProgress) -> Any: """Add the --progress flag to the given kwargs dict if supported by the git command. If the actual progress in the given progress instance is not given, we do not request any progress @@ -63,7 +71,7 @@ def add_progress(kwargs, git, progress): #} END utilities -def to_progress_instance(progress): +def to_progress_instance(progress: Optional[RemoteProgress]) -> Union[RemoteProgress, CallableRemoteProgress]: """Given the 'progress' return a suitable object derived from RemoteProgress(). """ @@ -224,7 +232,7 @@ class FetchInfo(object): } @classmethod - def refresh(cls): + def refresh(cls) -> bool: """This gets called by the refresh function (see the top level __init__). """ @@ -247,7 +255,8 @@ def refresh(cls): return True - def __init__(self, ref, flags, note='', old_commit=None, remote_ref_path=None): + def __init__(self, ref: SymbolicReference, flags: Set[int], note: str = '', old_commit: Optional[Commit] = None, + remote_ref_path: Optional[PathLike] = None): """ Initialize a new instance """ @@ -257,16 +266,16 @@ def __init__(self, ref, flags, note='', old_commit=None, remote_ref_path=None): self.old_commit = old_commit self.remote_ref_path = remote_ref_path - def __str__(self): + def __str__(self) -> str: return self.name @property - def name(self): + def name(self) -> str: """:return: Name of our remote ref""" return self.ref.name @property - def commit(self): + def commit(self) -> 'Commit': """:return: Commit of our remote ref""" return self.ref.commit @@ -409,16 +418,6 @@ def __init__(self, repo, name): self.repo = repo # type: 'Repo' self.name = name - if is_win: - # some oddity: on windows, python 2.5, it for some reason does not realize - # that it has the config_writer property, but instead calls __getattr__ - # which will not yield the expected results. 'pinging' the members - # with a dir call creates the config_writer property that we require - # ... bugs like these make me wonder whether python really wants to be used - # for production. It doesn't happen on linux though. - dir(self) - # END windows special handling - def __getattr__(self, attr): """Allows to call this instance like remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" diff --git a/git/repo/base.py b/git/repo/base.py index 253631063..ed0a810e4 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -4,11 +4,6 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - -from git.objects.tag import TagObject -from git.objects.blob import Blob -from git.objects.tree import Tree -from git.refs.symbolic import SymbolicReference import logging import os import re @@ -30,38 +25,44 @@ from git.objects import Submodule, RootModule, Commit from git.refs import HEAD, Head, Reference, TagReference from git.remote import Remote, add_progress, to_progress_instance -from git.util import Actor, IterableList, finalize_process, decygpath, hex_to_bin, expand_path +from git.util import Actor, finalize_process, decygpath, hex_to_bin, expand_path, remove_password_if_present import os.path as osp from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir import gc import gitdb -# Typing ------------------------------------------------------------------- - -from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, - TextIO, Tuple, Type, Union, NamedTuple, cast,) -from typing_extensions import Literal -from git.types import PathLike, TBD - -Lit_config_levels = Literal['system', 'global', 'user', 'repository'] - +# typing ------------------------------------------------------ -# -------------------------------------------------------------------------- +from git.compat.typing import Literal +from git.types import TBD, PathLike +from typing import (Any, BinaryIO, Callable, Dict, + Iterator, List, Mapping, Optional, + TextIO, Tuple, Type, Union, + NamedTuple, cast, TYPE_CHECKING) +if TYPE_CHECKING: # only needed for types + from git.util import IterableList + from git.refs.symbolic import SymbolicReference + from git.objects import TagObject, Blob, Tree # NOQA: F401 -class BlameEntry(NamedTuple): - commit: Dict[str, TBD] # Any == 'Commit' type? - linenos: range - orig_path: Optional[str] - orig_linenos: range +Lit_config_levels = Literal['system', 'global', 'user', 'repository'] +# ----------------------------------------------------------- log = logging.getLogger(__name__) __all__ = ('Repo',) +BlameEntry = NamedTuple('BlameEntry', [ + ('commit', Dict[str, TBD]), + ('linenos', range), + ('orig_path', Optional[str]), + ('orig_linenos', range)] +) + + class Repo(object): """Represents a git repository and allows you to query references, gather commit information, generate diffs, create and clone repositories query @@ -221,10 +222,11 @@ def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = self.git = self.GitCommandWrapperType(self.working_dir) # special handling, in special times - args = [osp.join(self.common_dir, 'objects')] # type: List[Union[str, Git]] + rootpath = osp.join(self.common_dir, 'objects') if issubclass(odbt, GitCmdObjectDB): - args.append(self.git) - self.odb = odbt(*args) + self.odb = odbt(rootpath, self.git) + else: + self.odb = odbt(rootpath) def __enter__(self) -> 'Repo': return self @@ -266,13 +268,14 @@ def __hash__(self) -> int: # Description property def _get_description(self) -> str: - filename = osp.join(self.git_dir, 'description') if self.git_dir else "" + if self.git_dir: + filename = osp.join(self.git_dir, 'description') with open(filename, 'rb') as fp: return fp.read().rstrip().decode(defenc) def _set_description(self, descr: str) -> None: - - filename = osp.join(self.git_dir, 'description') if self.git_dir else "" + if self.git_dir: + filename = osp.join(self.git_dir, 'description') with open(filename, 'wb') as fp: fp.write((descr + '\n').encode(defenc)) @@ -306,7 +309,7 @@ def bare(self) -> bool: return self._bare @property - def heads(self) -> IterableList: + def heads(self) -> 'IterableList': """A list of ``Head`` objects representing the branch heads in this repo @@ -314,7 +317,7 @@ def heads(self) -> IterableList: return Head.list_items(self) @property - def references(self) -> IterableList: + def references(self) -> 'IterableList': """A list of Reference objects representing tags, heads and remote references. :return: IterableList(Reference, ...)""" @@ -327,19 +330,19 @@ def references(self) -> IterableList: branches = heads @property - def index(self) -> IndexFile: + def index(self) -> 'IndexFile': """:return: IndexFile representing this repository's index. :note: This property can be expensive, as the returned ``IndexFile`` will be reinitialized. It's recommended to re-use the object.""" return IndexFile(self) @property - def head(self) -> HEAD: + def head(self) -> 'HEAD': """:return: HEAD Object pointing to the current head reference""" return HEAD(self, 'HEAD') @property - def remotes(self) -> IterableList: + def remotes(self) -> 'IterableList': """A list of Remote objects allowing to access and manipulate remotes :return: ``git.IterableList(Remote, ...)``""" return Remote.list_items(self) @@ -355,13 +358,13 @@ def remote(self, name: str = 'origin') -> 'Remote': #{ Submodules @property - def submodules(self) -> IterableList: + def submodules(self) -> 'IterableList': """ :return: git.IterableList(Submodule, ...) of direct submodules available from the current head""" return Submodule.list_items(self) - def submodule(self, name: str) -> IterableList: + def submodule(self, name: str) -> 'IterableList': """ :return: Submodule with the given name :raise ValueError: If no such submodule exists""" try: @@ -393,7 +396,7 @@ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator: #}END submodules @property - def tags(self) -> IterableList: + def tags(self) -> 'IterableList': """A list of ``Tag`` objects that are available in this repo :return: ``git.IterableList(TagReference, ...)`` """ return TagReference.list_items(self) @@ -405,14 +408,14 @@ def tag(self, path: PathLike) -> TagReference: def create_head(self, path: PathLike, commit: str = 'HEAD', force: bool = False, logmsg: Optional[str] = None - ) -> SymbolicReference: + ) -> 'SymbolicReference': """Create a new head within the repository. For more documentation, please see the Head.create method. :return: newly created Head Reference""" return Head.create(self, path, commit, force, logmsg) - def delete_head(self, *heads: HEAD, **kwargs: Any) -> None: + def delete_head(self, *heads: 'SymbolicReference', **kwargs: Any) -> None: """Delete the given heads :param kwargs: Additional keyword arguments to be passed to git-branch""" @@ -458,12 +461,11 @@ def _get_config_path(self, config_level: Lit_config_levels) -> str: elif config_level == "global": return osp.normpath(osp.expanduser("~/.gitconfig")) elif config_level == "repository": - if self._common_dir: - return osp.normpath(osp.join(self._common_dir, "config")) - elif self.git_dir: - return osp.normpath(osp.join(self.git_dir, "config")) - else: + repo_dir = self._common_dir or self.git_dir + if not repo_dir: raise NotADirectoryError + else: + return osp.normpath(osp.join(repo_dir, "config")) raise ValueError("Invalid configuration level: %r" % config_level) @@ -503,7 +505,8 @@ def config_writer(self, config_level: Lit_config_levels = "repository") -> GitCo repository = configuration file for this repository only""" return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self) - def commit(self, rev: Optional[TBD] = None,) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]: + def commit(self, rev: Optional[TBD] = None + ) -> Union['SymbolicReference', Commit, 'TagObject', 'Blob', 'Tree']: """The Commit object for the specified revision :param rev: revision specifier, see git-rev-parse for viable options. @@ -536,7 +539,7 @@ def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree': return self.rev_parse(str(rev) + "^{tree}") def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, List[PathLike]] = '', - **kwargs: Any,) -> Iterator[Commit]: + **kwargs: Any) -> Iterator[Commit]: """A list of Commit objects representing the history of a given ref/commit :param rev: @@ -560,8 +563,8 @@ def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, List[Pa return Commit.iter_items(self, rev, paths, **kwargs) - def merge_base(self, *rev: TBD, **kwargs: Any, - ) -> List[Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]]: + def merge_base(self, *rev: TBD, **kwargs: Any + ) -> List[Union['SymbolicReference', Commit, 'TagObject', 'Blob', 'Tree', None]]: """Find the closest common ancestor for the given revision (e.g. Commits, Tags, References, etc) :param rev: At least two revs to find the common ancestor for. @@ -574,7 +577,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any, raise ValueError("Please specify at least two revs, got only %i" % len(rev)) # end handle input - res = [] # type: List[Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]] + res = [] # type: List[Union['SymbolicReference', Commit, 'TagObject', 'Blob', 'Tree', None]] try: lines = self.git.merge_base(*rev, **kwargs).splitlines() # List[str] except GitCommandError as err: @@ -608,11 +611,13 @@ def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: return True def _get_daemon_export(self) -> bool: - filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) if self.git_dir else "" + if self.git_dir: + filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) return osp.exists(filename) def _set_daemon_export(self, value: object) -> None: - filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) if self.git_dir else "" + if self.git_dir: + filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) fileexists = osp.exists(filename) if value and not fileexists: touch(filename) @@ -628,7 +633,8 @@ def _get_alternates(self) -> List[str]: """The list of alternates for this repo from which objects can be retrieved :return: list of strings being pathnames of alternates""" - alternates_path = osp.join(self.git_dir, 'objects', 'info', 'alternates') if self.git_dir else "" + if self.git_dir: + alternates_path = osp.join(self.git_dir, 'objects', 'info', 'alternates') if osp.exists(alternates_path): with open(alternates_path, 'rb') as f: @@ -768,7 +774,7 @@ def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iter should get a continuous range spanning all line numbers in the file. """ data = self.git.blame(rev, '--', file, p=True, incremental=True, stdout_as_string=False, **kwargs) - commits = {} + commits = {} # type: Dict[str, TBD] stream = (line for line in data.split(b'\n') if line) while True: @@ -776,10 +782,11 @@ def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iter line = next(stream) # when exhausted, causes a StopIteration, terminating this function except StopIteration: return - hexsha, orig_lineno, lineno, num_lines = line.split() - lineno = int(lineno) - num_lines = int(num_lines) - orig_lineno = int(orig_lineno) + split_line = line.split() # type: Tuple[str, str, str, str] + hexsha, orig_lineno_str, lineno_str, num_lines_str = split_line + lineno = int(lineno_str) + num_lines = int(num_lines_str) + orig_lineno = int(orig_lineno_str) if hexsha not in commits: # Now read the next few lines and build up a dict of properties # for this commit @@ -871,12 +878,10 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any digits = parts[-1].split(" ") if len(digits) == 3: info = {'id': firstpart} - blames.append([None, [""]]) - elif not info or info['id'] != firstpart: + blames.append([None, []]) + elif info['id'] != firstpart: info = {'id': firstpart} - commits_firstpart = commits.get(firstpart) - blames.append([commits_firstpart, []]) - + blames.append([commits.get(firstpart), []]) # END blame data initialization else: m = self.re_author_committer_start.search(firstpart) @@ -933,8 +938,6 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any blames[-1][0] = c if blames[-1][1] is not None: blames[-1][1].append(line) - else: - blames[-1][1] = [line] info = {'id': sha} # END if we collected commit info # END distinguish filename,summary,rest @@ -944,7 +947,7 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any @classmethod def init(cls, path: PathLike = None, mkdir: bool = True, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, - expand_vars: bool = True, **kwargs: Any,) -> 'Repo': + expand_vars: bool = True, **kwargs: Any) -> 'Repo': """Initialize a git repository at the given path if specified :param path: @@ -983,12 +986,8 @@ def init(cls, path: PathLike = None, mkdir: bool = True, odbt: Type[GitCmdObject @classmethod def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Type[GitCmdObjectDB], - progress: Optional[Callable], - multi_options: Optional[List[str]] = None, **kwargs: Any, + progress: Optional[Callable], multi_options: Optional[List[str]] = None, **kwargs: Any ) -> 'Repo': - if progress is not None: - progress_checked = to_progress_instance(progress) - odbt = kwargs.pop('odbt', odb_default_type) # when pathlib.Path or other classbased path is passed @@ -1011,13 +1010,16 @@ def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Typ if multi_options: multi = ' '.join(multi_options).split(' ') proc = git.clone(multi, Git.polish_url(url), clone_path, with_extended_output=True, as_process=True, - v=True, universal_newlines=True, **add_progress(kwargs, git, progress_checked)) - if progress_checked: - handle_process_output(proc, None, progress_checked.new_message_handler(), + v=True, universal_newlines=True, **add_progress(kwargs, git, progress)) + if progress: + handle_process_output(proc, None, to_progress_instance(progress).new_message_handler(), finalize_process, decode_streams=False) else: (stdout, stderr) = proc.communicate() - log.debug("Cmd(%s)'s unused stdout: %s", getattr(proc, 'args', ''), stdout) + cmdline = getattr(proc, 'args', '') + cmdline = remove_password_if_present(cmdline) + + log.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout) finalize_process(proc, stderr=stderr) # our git command could have a different working dir than our actual @@ -1130,13 +1132,14 @@ def __repr__(self) -> str: clazz = self.__class__ return '<%s.%s %r>' % (clazz.__module__, clazz.__name__, self.git_dir) - def currently_rebasing_on(self) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree, None]: + def currently_rebasing_on(self) -> Union['SymbolicReference', Commit, 'TagObject', 'Blob', 'Tree', None]: """ :return: The commit which is currently being replayed while rebasing. None if we are not currently rebasing. """ - rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") if self.git_dir else "" + if self.git_dir: + rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") if not osp.isfile(rebase_head_file): return None return self.commit(open(rebase_head_file, "rt").readline().strip()) diff --git a/git/repo/fun.py b/git/repo/fun.py index b81845932..703940819 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -18,11 +18,12 @@ # Typing ---------------------------------------------------------------------- -from .base import Repo -from git.db import GitCmdObjectDB -from git.objects import Commit, TagObject, Blob, Tree -from typing import AnyStr, Union, Optional, cast +from typing import AnyStr, Union, Optional, cast, TYPE_CHECKING from git.types import PathLike +if TYPE_CHECKING: + from .base import Repo + from git.db import GitCmdObjectDB + from git.objects import Commit, TagObject, Blob, Tree # ---------------------------------------------------------------------------- @@ -102,7 +103,7 @@ def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: return None -def short_to_long(odb: GitCmdObjectDB, hexsha: AnyStr) -> Optional[bytes]: +def short_to_long(odb: 'GitCmdObjectDB', hexsha: AnyStr) -> Optional[bytes]: """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha or None if no candidate could be found. :param hexsha: hexsha with less than 40 byte""" @@ -113,8 +114,8 @@ def short_to_long(odb: GitCmdObjectDB, hexsha: AnyStr) -> Optional[bytes]: # END exception handling -def name_to_object(repo: Repo, name: str, return_ref: bool = False, - ) -> Union[SymbolicReference, Commit, TagObject, Blob, Tree]: +def name_to_object(repo: 'Repo', name: str, return_ref: bool = False + ) -> Union[SymbolicReference, 'Commit', 'TagObject', 'Blob', 'Tree']: """ :return: object specified by the given name, hexshas ( short and long ) as well as references are supported @@ -161,7 +162,7 @@ def name_to_object(repo: Repo, name: str, return_ref: bool = False, return Object.new_from_sha(repo, hex_to_bin(hexsha)) -def deref_tag(tag: Tag) -> TagObject: +def deref_tag(tag: Tag) -> 'TagObject': """Recursively dereference a tag and return the resulting object""" while True: try: @@ -172,7 +173,7 @@ def deref_tag(tag: Tag) -> TagObject: return tag -def to_commit(obj: Object) -> Union[Commit, TagObject]: +def to_commit(obj: Object) -> Union['Commit', 'TagObject']: """Convert the given object to a commit if possible and return it""" if obj.type == 'tag': obj = deref_tag(obj) @@ -183,7 +184,7 @@ 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) -> Union['Commit', 'Tag', 'Tree', 'Blob']: """ :return: Object at the given revision, either Commit, Tag, Tree or Blob :param rev: git-rev-parse compatible revision specification as string, please see diff --git a/git/types.py b/git/types.py index dc44c1231..3e33ae0c9 100644 --- a/git/types.py +++ b/git/types.py @@ -1,6 +1,20 @@ -import os # @UnusedImport ## not really unused, is in type string +# -*- coding: utf-8 -*- +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +import sys from typing import Union, Any TBD = Any -PathLike = Union[str, 'os.PathLike[str]'] + +if sys.version_info[:2] < (3, 6): + # os.PathLike (PEP-519) only got introduced with Python 3.6 + PathLike = str +elif sys.version_info[:2] < (3, 9): + # Python >= 3.6, < 3.9 + PathLike = Union[str, os.PathLike] +elif sys.version_info[:2] >= (3, 9): + # os.PathLike only becomes subscriptable from Python 3.9 onwards + PathLike = Union[str, os.PathLike[str]] diff --git a/git/util.py b/git/util.py index 2b0c81715..af4990286 100644 --- a/git/util.py +++ b/git/util.py @@ -3,7 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -from git.remote import Remote + import contextlib from functools import wraps import getpass @@ -17,11 +17,15 @@ from sys import maxsize import time from unittest import SkipTest +from urllib.parse import urlsplit, urlunsplit # typing --------------------------------------------------------- + from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, List, - NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast) -from git.repo.base import Repo + NoReturn, Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING) +if TYPE_CHECKING: + from git.remote import Remote + from git.repo.base import Repo from .types import PathLike, TBD # --------------------------------------------------------------------- @@ -74,7 +78,7 @@ def unbare_repo(func: Callable) -> Callable: encounter a bare repository""" @wraps(func) - def wrapper(self: Remote, *args: Any, **kwargs: Any) -> TBD: + def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> TBD: if self.repo.bare: raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) # END bare method @@ -359,6 +363,34 @@ def expand_path(p: PathLike, expand_vars: bool = True) -> Optional[PathLike]: except Exception: return None + +def remove_password_if_present(cmdline): + """ + Parse any command line argument and if on of the element is an URL with a + password, replace it by stars (in-place). + + If nothing found just returns the command line as-is. + + This should be used for every log line that print a command line. + """ + new_cmdline = [] + for index, to_parse in enumerate(cmdline): + new_cmdline.append(to_parse) + try: + url = urlsplit(to_parse) + # Remove password from the URL if present + if url.password is None: + continue + + edited_url = url._replace( + netloc=url.netloc.replace(url.password, "*****")) + new_cmdline[index] = urlunsplit(edited_url) + except ValueError: + # This is not a valid URL + continue + return new_cmdline + + #} END utilities #{ Classes @@ -686,7 +718,7 @@ def __init__(self, total: Dict[str, Dict[str, int]], files: Dict[str, Dict[str, self.files = files @classmethod - def _list_from_string(cls, repo: Repo, text: str) -> 'Stats': + def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': """Create a Stat object from output retrieved by git-diff. :return: git.Stat""" @@ -924,6 +956,7 @@ def __getitem__(self, index: Union[int, slice, str]) -> Any: def __delitem__(self, index: Union[int, str, slice]) -> None: + delindex = cast(int, index) if not isinstance(index, int): delindex = -1 assert not isinstance(index, slice) @@ -949,7 +982,7 @@ 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) -> 'IterableList': """ 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 @@ -963,7 +996,7 @@ def list_items(cls, repo: Repo, *args: Any, **kwargs: Any) -> 'IterableList': 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) -> NoReturn: """For more information about the arguments, see list_items :return: iterator yielding Items""" raise NotImplementedError("To be implemented by Subclass") diff --git a/mypy.ini b/mypy.ini index 47c0fb0c0..b63d68fd3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,9 @@ [mypy] -disallow_untyped_defs = True +# TODO: enable when we've fully annotated everything +#disallow_untyped_defs = True -mypy_path = 'git' +# TODO: remove when 'gitdb' is fully annotated +[mypy-gitdb.*] +ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt index c4e8340d8..d980f6682 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ gitdb>=4.0.1,<5 +typing-extensions>=3.7.4.0;python_version<"3.8" diff --git a/test-requirements.txt b/test-requirements.txt index abda95cf0..e06d2be14 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,3 +4,5 @@ flake8 tox virtualenv nose +gitdb>=4.0.1,<5 +typing-extensions>=3.7.4.0;python_version<"3.8" diff --git a/test/fixtures/diff_file_with_colon b/test/fixtures/diff_file_with_colon new file mode 100644 index 0000000000000000000000000000000000000000..4058b1715bd164ef8c13c73a458426bc43dcc5d0 GIT binary patch literal 351 zcmY+9L2g4a2nBN#pCG{o^G)v1GgM%p{ZiCa>X(|{zOIx_Suo3abFBbORGtVHk0xf# zt1}_luqGPY*44*sL1T85T9COlPLmCE!c-N<>|J8`qgK z10mULiQo3)5|87u=(dFaN+(aTwH5}_u&!;nb*vEUE4_8K%$Smeu+|L?+-VFY9wIF} c#^^1?Cph;RUU3PJ_&P3s@74Fr^XJd$7YhDgzW@LL literal 0 HcmV?d00001 diff --git a/test/test_clone.py b/test/test_clone.py new file mode 100644 index 000000000..e9f6714d3 --- /dev/null +++ b/test/test_clone.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +from pathlib import Path +import re + +import git + +from .lib import ( + TestBase, + with_rw_directory, +) + + +class TestClone(TestBase): + @with_rw_directory + def test_checkout_in_non_empty_dir(self, rw_dir): + non_empty_dir = Path(rw_dir) + garbage_file = non_empty_dir / 'not-empty' + garbage_file.write_text('Garbage!') + + # Verify that cloning into the non-empty dir fails while complaining about + # the target directory not being empty/non-existent + try: + self.rorepo.clone(non_empty_dir) + except git.GitCommandError as exc: + self.assertTrue(exc.stderr, "GitCommandError's 'stderr' is unexpectedly empty") + expr = re.compile(r'(?is).*\bfatal:\s+destination\s+path\b.*\bexists\b.*\bnot\b.*\bempty\s+directory\b') + self.assertTrue(expr.search(exc.stderr), '"%s" does not match "%s"' % (expr.pattern, exc.stderr)) + else: + self.fail("GitCommandError not raised") diff --git a/test/test_diff.py b/test/test_diff.py index c6c9b67a0..9b20893a4 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -7,6 +7,7 @@ import ddt import shutil import tempfile +import unittest from git import ( Repo, GitCommandError, @@ -220,6 +221,12 @@ def test_diff_index_raw_format(self): self.assertIsNotNone(res[0].deleted_file) self.assertIsNone(res[0].b_path,) + @unittest.skip("This currently fails and would need someone to improve diff parsing") + def test_diff_file_with_colon(self): + output = fixture('diff_file_with_colon') + res = [] + Diff._handle_diff_line(output, None, res) + def test_diff_initial_commit(self): initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781') diff --git a/test/test_repo.py b/test/test_repo.py index d5ea8664a..8dc178337 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -238,6 +238,21 @@ def test_clone_from_with_path_contains_unicode(self): except UnicodeEncodeError: self.fail('Raised UnicodeEncodeError') + @with_rw_directory + def test_leaking_password_in_clone_logs(self, rw_dir): + password = "fakepassword1234" + try: + Repo.clone_from( + url="https://fakeuser:{}@fakerepo.example.com/testrepo".format( + password), + to_path=rw_dir) + except GitCommandError as err: + assert password not in str(err), "The error message '%s' should not contain the password" % err + # Working example from a blank private project + Repo.clone_from( + url="https://gitlab+deploy-token-392045:mLWhVus7bjLsy8xj8q2V@gitlab.com/mercierm/test_git_python", + to_path=rw_dir) + @with_rw_repo('HEAD') def test_max_chunk_size(self, repo): class TestOutputStream(TestBase): diff --git a/test/test_util.py b/test/test_util.py index 5eba6c500..ddc5f628f 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -30,7 +30,8 @@ Actor, IterableList, cygpath, - decygpath + decygpath, + remove_password_if_present, ) @@ -322,3 +323,20 @@ def test_pickle_tzoffset(self): t2 = pickle.loads(pickle.dumps(t1)) self.assertEqual(t1._offset, t2._offset) self.assertEqual(t1._name, t2._name) + + def test_remove_password_from_command_line(self): + password = "fakepassword1234" + url_with_pass = "https://fakeuser:{}@fakerepo.example.com/testrepo".format(password) + url_without_pass = "https://fakerepo.example.com/testrepo" + + cmd_1 = ["git", "clone", "-v", url_with_pass] + cmd_2 = ["git", "clone", "-v", url_without_pass] + cmd_3 = ["no", "url", "in", "this", "one"] + + redacted_cmd_1 = remove_password_if_present(cmd_1) + assert password not in " ".join(redacted_cmd_1) + # Check that we use a copy + assert cmd_1 is not redacted_cmd_1 + assert password in " ".join(cmd_1) + assert cmd_2 == remove_password_if_present(cmd_2) + assert cmd_3 == remove_password_if_present(cmd_3) diff --git a/tox.ini b/tox.ini index ad126ed4e..a0cb1c9f1 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,14 @@ commands = coverage run --omit="git/test/*" -m unittest --buffer {posargs} [testenv:flake8] commands = flake8 --ignore=W293,E265,E266,W503,W504,E731 {posargs} +[testenv:type] +description = type check ourselves +deps = + {[testenv]deps} + mypy +commands = + mypy -p git + [testenv:venv] commands = {posargs} @@ -23,6 +31,7 @@ commands = {posargs} # E266 = too many leading '#' for block comment # E731 = do not assign a lambda expression, use a def # W293 = Blank line contains whitespace -ignore = E265,W293,E266,E731 +# W504 = Line break after operator +ignore = E265,W293,E266,E731, W504 max-line-length = 120 exclude = .tox,.venv,build,dist,doc,git/ext/ From 18b75d9e63f513e972cbc09c06b040bcdb15aa05 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 16:02:11 +0100 Subject: [PATCH 0033/1849] copy sys.version checks for literal and final to git.types --- git/types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/git/types.py b/git/types.py index 3e33ae0c9..40d4f7885 100644 --- a/git/types.py +++ b/git/types.py @@ -6,6 +6,11 @@ import sys from typing import Union, Any +if sys.version_info[:2] >= (3, 8): + from typing import Final, Literal # noqa: F401 +else: + from typing_extensions import Final, Literal # noqa: F401 + TBD = Any From a1fa8506d177fa49552ffa84527c35d32f193abe Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 16:05:09 +0100 Subject: [PATCH 0034/1849] update type of FetchInfo.refresh() to use Literal --- git/remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index 20b5a5514..7d6c0f6f6 100644 --- a/git/remote.py +++ b/git/remote.py @@ -38,7 +38,7 @@ from typing import Any, Optional, Set, TYPE_CHECKING, Union -from git.types import PathLike +from git.types import PathLike, Literal if TYPE_CHECKING: from git.repo.base import Repo @@ -232,7 +232,7 @@ class FetchInfo(object): } @classmethod - def refresh(cls) -> bool: + def refresh(cls) -> Literal[True]: """This gets called by the refresh function (see the top level __init__). """ From c08f592cc0238054ec57b6024521a04cf70e692f Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 16:16:14 +0100 Subject: [PATCH 0035/1849] add types to PushInfo.__init__() .remote_ref() and .old_commit() --- git/remote.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/remote.py b/git/remote.py index 7d6c0f6f6..8404b1907 100644 --- a/git/remote.py +++ b/git/remote.py @@ -115,8 +115,8 @@ class PushInfo(object): '=': UP_TO_DATE, '!': ERROR} - def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None, - summary=''): + def __init__(self, flags: Set[int], local_ref: SymbolicReference, remote_ref_string: str, remote, + old_commit: Optional[bytes] = None, summary: str = '') -> None: """ Initialize a new instance """ self.flags = flags self.local_ref = local_ref @@ -126,11 +126,11 @@ def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None, self.summary = summary @property - def old_commit(self): + def old_commit(self) -> Optional[bool]: return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None @property - def remote_ref(self): + def remote_ref(self) -> Union[RemoteReference, TagReference]: """ :return: Remote Reference or TagReference in the local repository corresponding From baec2e293158ccffd5657abf4acdae18256c6c90 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 16:35:06 +0100 Subject: [PATCH 0036/1849] make progress types more general --- git/remote.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/remote.py b/git/remote.py index 8404b1907..8093fa9d2 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,7 +36,7 @@ # typing------------------------------------------------------- -from typing import Any, Optional, Set, TYPE_CHECKING, Union +from typing import Any, Callable, Optional, Set, TYPE_CHECKING, Union from git.types import PathLike, Literal @@ -55,7 +55,7 @@ #{ Utilities -def add_progress(kwargs: Any, git: Git, progress: RemoteProgress) -> Any: +def add_progress(kwargs: Any, git: Git, progress: Optional[Callable[..., Any]]) -> Any: """Add the --progress flag to the given kwargs dict if supported by the git command. If the actual progress in the given progress instance is not given, we do not request any progress @@ -71,7 +71,7 @@ def add_progress(kwargs: Any, git: Git, progress: RemoteProgress) -> Any: #} END utilities -def to_progress_instance(progress: Optional[RemoteProgress]) -> Union[RemoteProgress, CallableRemoteProgress]: +def to_progress_instance(progress: Callable[..., Any]) -> Union[RemoteProgress, CallableRemoteProgress]: """Given the 'progress' return a suitable object derived from RemoteProgress(). """ From e37ebaa5407408ee73479a12ada0c4a75e602092 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 16:40:21 +0100 Subject: [PATCH 0037/1849] change a type (Commit) to a forward ref --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 8093fa9d2..5b6b29a72 100644 --- a/git/remote.py +++ b/git/remote.py @@ -255,7 +255,7 @@ def refresh(cls) -> Literal[True]: return True - def __init__(self, ref: SymbolicReference, flags: Set[int], note: str = '', old_commit: Optional[Commit] = None, + def __init__(self, ref: SymbolicReference, flags: Set[int], note: str = '', old_commit: Optional['Commit'] = None, remote_ref_path: Optional[PathLike] = None): """ Initialize a new instance From f97d37881d50da8f9702681bc1928a8d44119e88 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 17:18:11 +0100 Subject: [PATCH 0038/1849] change flags type to int --- git/remote.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/remote.py b/git/remote.py index 5b6b29a72..d73da7d4b 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,7 +36,7 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Optional, Set, TYPE_CHECKING, Union +from typing import Any, Callable, Optional, TYPE_CHECKING, Union from git.types import PathLike, Literal @@ -115,7 +115,7 @@ class PushInfo(object): '=': UP_TO_DATE, '!': ERROR} - def __init__(self, flags: Set[int], local_ref: SymbolicReference, remote_ref_string: str, remote, + def __init__(self, flags: int, local_ref: SymbolicReference, remote_ref_string: str, remote, old_commit: Optional[bytes] = None, summary: str = '') -> None: """ Initialize a new instance """ self.flags = flags @@ -255,8 +255,8 @@ def refresh(cls) -> Literal[True]: return True - def __init__(self, ref: SymbolicReference, flags: Set[int], note: str = '', old_commit: Optional['Commit'] = None, - remote_ref_path: Optional[PathLike] = None): + def __init__(self, ref: SymbolicReference, flags: int, note: str = '', old_commit: Optional['Commit'] = None, + remote_ref_path: Optional[PathLike] = None) -> None: """ Initialize a new instance """ From 90fefb0a8cc5dc793d40608e2d6a2398acecef12 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 17:49:36 +0100 Subject: [PATCH 0039/1849] add overloads to to_progress_instance() --- git/remote.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index d73da7d4b..0071b9237 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,7 +36,7 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Optional, TYPE_CHECKING, Union +from typing import Any, Callable, Optional, TYPE_CHECKING, Union, overload from git.types import PathLike, Literal @@ -71,7 +71,23 @@ def add_progress(kwargs: Any, git: Git, progress: Optional[Callable[..., Any]]) #} END utilities -def to_progress_instance(progress: Callable[..., Any]) -> Union[RemoteProgress, CallableRemoteProgress]: +@overload +def to_progress_instance(progress: None) -> RemoteProgress: + ... + + +@overload +def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: + ... + + +@overload +def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: + ... + + +def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, None] + ) -> Union[RemoteProgress, CallableRemoteProgress]: """Given the 'progress' return a suitable object derived from RemoteProgress(). """ From 559ddb3b60e36a1b9c4a145d7a00a295a37d46a8 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 18:15:58 +0100 Subject: [PATCH 0040/1849] add types to _from_line() --- git/remote.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/git/remote.py b/git/remote.py index 0071b9237..8aa390ff6 100644 --- a/git/remote.py +++ b/git/remote.py @@ -131,9 +131,10 @@ class PushInfo(object): '=': UP_TO_DATE, '!': ERROR} - def __init__(self, flags: int, local_ref: SymbolicReference, remote_ref_string: str, remote, - old_commit: Optional[bytes] = None, summary: str = '') -> None: - """ Initialize a new instance """ + def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote_ref_string: str, remote, + old_commit: Optional[str] = None, summary: str = '') -> None: + """ Initialize a new instance + local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None """ self.flags = flags self.local_ref = local_ref self.remote_ref_string = remote_ref_string @@ -162,7 +163,7 @@ def remote_ref(self) -> Union[RemoteReference, TagReference]: # END @classmethod - def _from_line(cls, remote, line): + def _from_line(cls, remote, line: str) -> 'PushInfo': """Create a new PushInfo instance as parsed from line which is expected to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes""" control_character, from_to, summary = line.split('\t', 3) @@ -178,7 +179,7 @@ def _from_line(cls, remote, line): # from_to handling from_ref_string, to_ref_string = from_to.split(':') if flags & cls.DELETED: - from_ref = None + from_ref = None # type: Union[SymbolicReference, None] else: if from_ref_string == "(delete)": from_ref = None @@ -186,7 +187,7 @@ def _from_line(cls, remote, line): from_ref = Reference.from_path(remote.repo, from_ref_string) # commit handling, could be message or commit info - old_commit = None + old_commit = None # type: Optional[str] if summary.startswith('['): if "[rejected]" in summary: flags |= cls.REJECTED From 1b16037a4ff17f0e25add382c3550323373c4398 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 21:17:25 +0100 Subject: [PATCH 0041/1849] second pass of adding types --- git/refs/symbolic.py | 2 +- git/remote.py | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 22d9c1d51..64a6591aa 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -45,7 +45,7 @@ class SymbolicReference(object): _remote_common_path_default = "refs/remotes" _id_attribute_ = "name" - def __init__(self, repo, path): + def __init__(self, repo, path, check_path=None): self.repo = repo self.path = path diff --git a/git/remote.py b/git/remote.py index 8aa390ff6..34d653e63 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,14 +36,18 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Optional, TYPE_CHECKING, Union, overload +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING, Union, cast, overload from git.types import PathLike, Literal if TYPE_CHECKING: from git.repo.base import Repo from git.objects.commit import Commit + from git.objects.blob import Blob + from git.objects.tree import Tree + from git.objects.tag import TagObject +flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't'] # ------------------------------------------------------------- log = logging.getLogger('git.remote') @@ -131,7 +135,7 @@ class PushInfo(object): '=': UP_TO_DATE, '!': ERROR} - def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote_ref_string: str, remote, + def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote_ref_string: str, remote: 'Remote', old_commit: Optional[str] = None, summary: str = '') -> None: """ Initialize a new instance local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None """ @@ -143,7 +147,7 @@ def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote self.summary = summary @property - def old_commit(self) -> Optional[bool]: + def old_commit(self) -> Union[str, SymbolicReference, 'Commit', 'TagObject', 'Blob', 'Tree', None]: return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None @property @@ -246,7 +250,7 @@ class FetchInfo(object): '=': HEAD_UPTODATE, ' ': FAST_FORWARD, '-': TAG_UPDATE, - } + } # type: Dict[flagKeyLiteral, int] @classmethod def refresh(cls) -> Literal[True]: @@ -297,7 +301,7 @@ def commit(self) -> 'Commit': return self.ref.commit @classmethod - def _from_line(cls, repo, line, fetch_line): + def _from_line(cls, repo: Repo, line: str, fetch_line) -> 'FetchInfo': """Parse information from the given line as returned by git-fetch -v and return a new FetchInfo object representing this information. @@ -319,7 +323,9 @@ def _from_line(cls, repo, line, fetch_line): raise ValueError("Failed to parse line: %r" % line) # parse lines - control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() + control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() + control_character = cast(flagKeyLiteral, control_character) # can do this neater once 3.5 dropped + try: _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") ref_type_name, fetch_note = fetch_note.split(' ', 1) @@ -359,7 +365,7 @@ def _from_line(cls, repo, line, fetch_line): # the fetch result is stored in FETCH_HEAD which destroys the rule we usually # have. In that case we use a symbolic reference which is detached ref_type = None - if remote_local_ref == "FETCH_HEAD": + if remote_local_ref_str == "FETCH_HEAD": ref_type = SymbolicReference elif ref_type_name == "tag" or is_tag_operation: # the ref_type_name can be branch, whereas we are still seeing a tag operation. It happens during @@ -387,21 +393,21 @@ def _from_line(cls, repo, line, fetch_line): # by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the # 'tags/' subdirectory in its path. # We don't want to test for actual existence, but try to figure everything out analytically. - ref_path = None - remote_local_ref = remote_local_ref.strip() - if remote_local_ref.startswith(Reference._common_path_default + "/"): + ref_path = None # type: Optional[PathLike] + remote_local_ref_str = remote_local_ref_str.strip() + if remote_local_ref_str.startswith(Reference._common_path_default + "/"): # always use actual type if we get absolute paths # Will always be the case if something is fetched outside of refs/remotes (if its not a tag) - ref_path = remote_local_ref + ref_path = remote_local_ref_str if ref_type is not TagReference and not \ - remote_local_ref.startswith(RemoteReference._common_path_default + "/"): + remote_local_ref_str.startswith(RemoteReference._common_path_default + "/"): ref_type = Reference # END downgrade remote reference - elif ref_type is TagReference and 'tags/' in remote_local_ref: + elif ref_type is TagReference and 'tags/' in remote_local_ref_str: # even though its a tag, it is located in refs/remotes - ref_path = join_path(RemoteReference._common_path_default, remote_local_ref) + ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str) else: - ref_path = join_path(ref_type._common_path_default, remote_local_ref) + ref_path = join_path(ref_type._common_path_default, remote_local_ref_str) # END obtain refpath # even though the path could be within the git conventions, we make From 96f8f17d5d63c0e0c044ac3f56e94a1aa2e45ec3 Mon Sep 17 00:00:00 2001 From: yobmod Date: Mon, 3 May 2021 21:20:29 +0100 Subject: [PATCH 0042/1849] fix Repo forward ref --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 34d653e63..2eeafcc41 100644 --- a/git/remote.py +++ b/git/remote.py @@ -301,7 +301,7 @@ def commit(self) -> 'Commit': return self.ref.commit @classmethod - def _from_line(cls, repo: Repo, line: str, fetch_line) -> 'FetchInfo': + def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': """Parse information from the given line as returned by git-fetch -v and return a new FetchInfo object representing this information. From 6a2f5d05f4a8e3427d6dd2a5981f148a9f6bef84 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 16:37:23 +0100 Subject: [PATCH 0043/1849] Add types to Remote. init getattr exists --- git/remote.py | 6 +++--- mypy.ini | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/git/remote.py b/git/remote.py index 2eeafcc41..a6c76e19b 100644 --- a/git/remote.py +++ b/git/remote.py @@ -433,7 +433,7 @@ class Remote(LazyMixin, Iterable): __slots__ = ("repo", "name", "_config_reader") _id_attribute_ = "name" - def __init__(self, repo, name): + def __init__(self, repo: 'Repo', name: str) -> None: """Initialize a remote instance :param repo: The repository we are a remote of @@ -441,7 +441,7 @@ def __init__(self, repo, name): self.repo = repo # type: 'Repo' self.name = name - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: """Allows to call this instance like remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" if attr == "_config_reader": @@ -481,7 +481,7 @@ def __ne__(self, other): def __hash__(self): return hash(self.name) - def exists(self): + def exists(self) -> bool: """ :return: True if this is a valid, existing remote. Valid remotes have an entry in the repository's configuration""" diff --git a/mypy.ini b/mypy.ini index b63d68fd3..d55d21647 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ [mypy] # TODO: enable when we've fully annotated everything -#disallow_untyped_defs = True +disallow_untyped_defs = True # TODO: remove when 'gitdb' is fully annotated [mypy-gitdb.*] From 37cef2340d3e074a226c0e81eaf000b5b90dfa55 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 18:31:38 +0100 Subject: [PATCH 0044/1849] flake8 fixes --- git/{compat/__init__.py => compat.py} | 7 +- git/compat/typing.py | 13 ---- git/config.py | 15 ++++- git/diff.py | 3 +- git/remote.py | 97 +++++++++++++++------------ git/repo/base.py | 7 +- git/util.py | 7 +- mypy.ini | 2 +- 8 files changed, 80 insertions(+), 71 deletions(-) rename git/{compat/__init__.py => compat.py} (89%) delete mode 100644 git/compat/typing.py diff --git a/git/compat/__init__.py b/git/compat.py similarity index 89% rename from git/compat/__init__.py rename to git/compat.py index c4bd2aa36..4ecd19a9a 100644 --- a/git/compat/__init__.py +++ b/git/compat.py @@ -24,6 +24,7 @@ Dict, IO, Optional, + Tuple, Type, Union, overload, @@ -92,16 +93,16 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: return None -def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation +def with_metaclass(meta: Type[Any], *bases: Any) -> TBD: # type: ignore ## mypy cannot understand dynamic class creation """copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15""" class metaclass(meta): # type: ignore __call__ = type.__call__ __init__ = type.__init__ # type: ignore - def __new__(cls, name: str, nbases: Optional[int], d: Dict[str, Any]) -> TBD: + def __new__(cls, name: str, nbases: Optional[Tuple[int, ...]], d: Dict[str, Any]) -> TBD: if nbases is None: return type.__new__(cls, name, (), d) return meta(name, bases, d) - return metaclass(meta.__name__ + 'Helper', None, {}) + return metaclass(meta.__name__ + 'Helper', None, {}) # type: ignore diff --git a/git/compat/typing.py b/git/compat/typing.py deleted file mode 100644 index 925c5ba2e..000000000 --- a/git/compat/typing.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# config.py -# Copyright (C) 2021 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import sys - -if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal # noqa: F401 -else: - from typing_extensions import Final, Literal # noqa: F401 diff --git a/git/config.py b/git/config.py index 0c8d975db..ea7302f4c 100644 --- a/git/config.py +++ b/git/config.py @@ -22,13 +22,23 @@ with_metaclass, is_win, ) -from git.compat.typing import Literal + from git.util import LockFile import os.path as osp import configparser as cp +# typing------------------------------------------------------- + +from typing import TYPE_CHECKING, Tuple + +from git.types import Literal + +if TYPE_CHECKING: + pass + +# ------------------------------------------------------------- __all__ = ('GitConfigParser', 'SectionConstraint') @@ -38,7 +48,8 @@ # invariants # represents the configuration level of a configuration file -CONFIG_LEVELS = ("system", "user", "global", "repository") +CONFIG_LEVELS = ("system", "user", "global", "repository" + ) # type: Tuple[Literal['system'], Literal['user'], Literal['global'], Literal['repository']] # Section pattern to detect conditional includes. # https://git-scm.com/docs/git-config#_conditional_includes diff --git a/git/diff.py b/git/diff.py index 943916ea8..5a7b189fc 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,8 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING -from git.compat.typing import Final, Literal -from git.types import TBD +from git.types import TBD, Final, Literal if TYPE_CHECKING: from .objects.tree import Tree diff --git a/git/remote.py b/git/remote.py index a6c76e19b..e17f7bb8c 100644 --- a/git/remote.py +++ b/git/remote.py @@ -16,7 +16,7 @@ Iterable, IterableList, RemoteProgress, - CallableRemoteProgress + CallableRemoteProgress, ) from git.util import ( join_path, @@ -36,9 +36,9 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Dict, Optional, TYPE_CHECKING, Union, cast, overload +from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, cast, overload -from git.types import PathLike, Literal +from git.types import PathLike, Literal, TBD if TYPE_CHECKING: from git.repo.base import Repo @@ -59,7 +59,7 @@ #{ Utilities -def add_progress(kwargs: Any, git: Git, progress: Optional[Callable[..., Any]]) -> Any: +def add_progress(kwargs: Any, git: Git, progress: Union[Callable[..., Any], None]) -> Any: """Add the --progress flag to the given kwargs dict if supported by the git command. If the actual progress in the given progress instance is not given, we do not request any progress @@ -167,7 +167,7 @@ def remote_ref(self) -> Union[RemoteReference, TagReference]: # END @classmethod - def _from_line(cls, remote, line: str) -> 'PushInfo': + def _from_line(cls, remote: 'Remote', line: str) -> 'PushInfo': """Create a new PushInfo instance as parsed from line which is expected to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes""" control_character, from_to, summary = line.split('\t', 3) @@ -276,7 +276,8 @@ def refresh(cls) -> Literal[True]: return True - def __init__(self, ref: SymbolicReference, flags: int, note: str = '', old_commit: Optional['Commit'] = None, + def __init__(self, ref: SymbolicReference, flags: int, note: str = '', + old_commit: Union['Commit', TagReference, 'Tree', 'Blob', None] = None, remote_ref_path: Optional[PathLike] = None) -> None: """ Initialize a new instance @@ -341,7 +342,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': # END control char exception handling # parse operation string for more info - makes no sense for symbolic refs, but we parse it anyway - old_commit = None + old_commit = None # type: Union[Commit, TagReference, Tree, Blob, None] is_tag_operation = False if 'rejected' in operation: flags |= cls.REJECTED @@ -455,10 +456,10 @@ def __getattr__(self, attr: str) -> Any: return super(Remote, self).__getattr__(attr) # END handle exception - def _config_section_name(self): + def _config_section_name(self) -> str: return 'remote "%s"' % self.name - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> Any: if attr == "_config_reader": # NOTE: This is cached as __getattr__ is overridden to return remote config values implicitly, such as # in print(r.pushurl) @@ -466,19 +467,19 @@ def _set_cache_(self, attr): else: super(Remote, self)._set_cache_(attr) - def __str__(self): + def __str__(self) -> str: return self.name - def __repr__(self): + def __repr__(self) -> str: return '' % (self.__class__.__name__, self.name) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return isinstance(other, type(self)) and self.name == other.name - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash(self.name) def exists(self) -> bool: @@ -496,7 +497,7 @@ def exists(self) -> bool: # end @classmethod - def iter_items(cls, repo): + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator['Remote']: """:return: Iterator yielding Remote objects of the given repository""" for section in repo.config_reader("repository").sections(): if not section.startswith('remote '): @@ -508,7 +509,7 @@ def iter_items(cls, repo): yield Remote(repo, section[lbound + 1:rbound]) # END for each configuration section - def set_url(self, new_url, old_url=None, **kwargs): + def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> 'Remote': """Configure URLs on current remote (cf command git remote set_url) This command manages URLs on the remote. @@ -525,7 +526,7 @@ def set_url(self, new_url, old_url=None, **kwargs): self.repo.git.remote(scmd, self.name, new_url, **kwargs) return self - def add_url(self, url, **kwargs): + def add_url(self, url: str, **kwargs: Any) -> 'Remote': """Adds a new url on current remote (special case of git remote set_url) This command adds new URLs to a given remote, making it possible to have @@ -536,7 +537,7 @@ def add_url(self, url, **kwargs): """ return self.set_url(url, add=True) - def delete_url(self, url, **kwargs): + def delete_url(self, url: str, **kwargs: Any) -> 'Remote': """Deletes a new url on current remote (special case of git remote set_url) This command deletes new URLs to a given remote, making it possible to have @@ -548,10 +549,11 @@ def delete_url(self, url, **kwargs): return self.set_url(url, delete=True) @property - def urls(self): + def urls(self) -> Iterator[str]: """:return: Iterator yielding all configured URL targets on a remote as strings""" try: - remote_details = self.repo.git.remote("get-url", "--all", self.name) + # can replace cast with type assert? + remote_details = cast(str, self.repo.git.remote("get-url", "--all", self.name)) for line in remote_details.split('\n'): yield line except GitCommandError as ex: @@ -562,23 +564,23 @@ def urls(self): # if 'Unknown subcommand: get-url' in str(ex): try: - remote_details = self.repo.git.remote("show", self.name) + remote_details = cast(str, self.repo.git.remote("show", self.name)) for line in remote_details.split('\n'): if ' Push URL:' in line: yield line.split(': ')[-1] - except GitCommandError as ex: - if any(msg in str(ex) for msg in ['correct access rights', 'cannot run ssh']): + except GitCommandError as _ex: + if any(msg in str(_ex) for msg in ['correct access rights', 'cannot run ssh']): # If ssh is not setup to access this repository, see issue 694 - remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name) + remote_details = cast(str, self.repo.git.config('--get-all', 'remote.%s.url' % self.name)) for line in remote_details.split('\n'): yield line else: - raise ex + raise _ex else: raise ex @property - def refs(self): + def refs(self) -> IterableList: """ :return: IterableList of RemoteReference objects. It is prefixed, allowing @@ -589,7 +591,7 @@ def refs(self): return out_refs @property - def stale_refs(self): + def stale_refs(self) -> IterableList: """ :return: IterableList RemoteReference objects that do not have a corresponding @@ -623,7 +625,7 @@ def stale_refs(self): return out_refs @classmethod - def create(cls, repo, name, url, **kwargs): + def create(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote': """Create a new remote to the given repository :param repo: Repository instance that is to receive the new remote :param name: Desired name of the remote @@ -640,7 +642,7 @@ def create(cls, repo, name, url, **kwargs): add = create @classmethod - def remove(cls, repo, name): + def remove(cls, repo: 'Repo', name: str) -> str: """Remove the remote with the given name :return: the passed remote name to remove """ @@ -652,7 +654,7 @@ def remove(cls, repo, name): # alias rm = remove - def rename(self, new_name): + def rename(self, new_name: str) -> 'Remote': """Rename self to the given new_name :return: self """ if self.name == new_name: @@ -664,7 +666,7 @@ def rename(self, new_name): return self - def update(self, **kwargs): + def update(self, **kwargs: Any) -> 'Remote': """Fetch all changes for this remote, including new branches which will be forced in ( in case your local remote branch is not part the new remote branches ancestry anymore ). @@ -678,7 +680,8 @@ def update(self, **kwargs): self.repo.git.remote(scmd, self.name, **kwargs) return self - def _get_fetch_info_from_stderr(self, proc, progress): + def _get_fetch_info_from_stderr(self, proc: TBD, + progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: progress = to_progress_instance(progress) # skip first line as it is some remote info we are not interested in @@ -737,7 +740,8 @@ def _get_fetch_info_from_stderr(self, proc, progress): log.warning("Git informed while fetching: %s", err_line.strip()) return output - def _get_push_info(self, proc, progress): + def _get_push_info(self, proc: TBD, + progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: progress = to_progress_instance(progress) # read progress information from stderr @@ -745,9 +749,9 @@ def _get_push_info(self, proc, progress): # read the lines manually as it will use carriage returns between the messages # to override the previous one. This is why we read the bytes manually progress_handler = progress.new_message_handler() - output = [] + output = IterableList('push_infos') - def stdout_handler(line): + def stdout_handler(line: str) -> None: try: output.append(PushInfo._from_line(self, line)) except ValueError: @@ -766,7 +770,7 @@ def stdout_handler(line): return output - def _assert_refspec(self): + def _assert_refspec(self) -> None: """Turns out we can't deal with remotes if the refspec is missing""" config = self.config_reader unset = 'placeholder' @@ -779,7 +783,9 @@ def _assert_refspec(self): finally: config.release() - def fetch(self, refspec=None, progress=None, verbose=True, **kwargs): + def fetch(self, refspec: Union[str, List[str], None] = None, + progress: Union[Callable[..., Any], None] = None, + verbose: bool = True, **kwargs: Any) -> IterableList: """Fetch the latest changes for this remote :param refspec: @@ -810,9 +816,10 @@ def fetch(self, refspec=None, progress=None, verbose=True, **kwargs): if refspec is None: # No argument refspec, then ensure the repo's config has a fetch refspec. self._assert_refspec() + kwargs = add_progress(kwargs, self.repo.git, progress) if isinstance(refspec, list): - args = refspec + args = refspec # type: Sequence[Optional[str]] # should need this - check logic for passing None through else: args = [refspec] @@ -823,7 +830,9 @@ def fetch(self, refspec=None, progress=None, verbose=True, **kwargs): self.repo.odb.update_cache() return res - def pull(self, refspec=None, progress=None, **kwargs): + def pull(self, refspec: Union[str, List[str], None] = None, + progress: Union[Callable[..., Any], None] = None, + **kwargs: Any) -> IterableList: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. @@ -842,7 +851,9 @@ def pull(self, refspec=None, progress=None, **kwargs): self.repo.odb.update_cache() return res - def push(self, refspec=None, progress=None, **kwargs): + def push(self, refspec: Union[str, List[str], None] = None, + progress: Union[Callable[..., Any], None] = None, + **kwargs: Any) -> IterableList: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method @@ -873,14 +884,14 @@ def push(self, refspec=None, progress=None, **kwargs): return self._get_push_info(proc, progress) @property - def config_reader(self): + def config_reader(self) -> SectionConstraint: """ :return: GitConfigParser compatible object able to read options for only our remote. Hence you may simple type config.get("pushurl") to obtain the information""" return self._config_reader - def _clear_cache(self): + def _clear_cache(self) -> None: try: del(self._config_reader) except AttributeError: @@ -888,7 +899,7 @@ def _clear_cache(self): # END handle exception @property - def config_writer(self): + def config_writer(self) -> SectionConstraint: """ :return: GitConfigParser compatible object able to write options for this remote. :note: diff --git a/git/repo/base.py b/git/repo/base.py index ed0a810e4..94c6e30b0 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -34,8 +34,7 @@ # typing ------------------------------------------------------ -from git.compat.typing import Literal -from git.types import TBD, PathLike +from git.types import TBD, PathLike, Literal from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, TextIO, Tuple, Type, Union, @@ -434,7 +433,7 @@ def delete_tag(self, *tags: TBD) -> None: """Delete the given tag references""" return TagReference.delete(self, *tags) - def create_remote(self, name: str, url: PathLike, **kwargs: Any) -> Remote: + def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote: """Create a new remote. For more information, please see the documentation of the Remote.create @@ -443,7 +442,7 @@ def create_remote(self, name: str, url: PathLike, **kwargs: Any) -> Remote: :return: Remote reference""" return Remote.create(self, name, url, **kwargs) - def delete_remote(self, remote: 'Remote') -> Type['Remote']: + def delete_remote(self, remote: 'Remote') -> str: """Delete the given remote.""" return Remote.remove(self, remote) diff --git a/git/util.py b/git/util.py index af4990286..558be1e4d 100644 --- a/git/util.py +++ b/git/util.py @@ -21,8 +21,8 @@ # 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, Union, cast, TYPE_CHECKING) if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo @@ -996,7 +996,8 @@ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> 'IterableList': 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) -> Iterator[TBD]: + # 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") diff --git a/mypy.ini b/mypy.ini index d55d21647..8f86a6af7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ [mypy] # TODO: enable when we've fully annotated everything -disallow_untyped_defs = True +# disallow_untyped_defs = True # TODO: remove when 'gitdb' is fully annotated [mypy-gitdb.*] From efe68337c513c573dde8fbf58337bed2fa2ca39a Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 20:28:23 +0100 Subject: [PATCH 0045/1849] Add types to config.py CONFIG_LEVELS, MetaParserBuilder.__new__() .needs_values() .set_dirty_and_flush_changes() --- git/config.py | 16 ++++++++-------- git/repo/base.py | 3 +-- git/types.py | 6 ++++-- mypy.ini | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/git/config.py b/git/config.py index ea7302f4c..9f4d5ad9e 100644 --- a/git/config.py +++ b/git/config.py @@ -31,9 +31,9 @@ # typing------------------------------------------------------- -from typing import TYPE_CHECKING, Tuple +from typing import Any, Callable, Mapping, TYPE_CHECKING, Tuple -from git.types import Literal +from git.types import Literal, Lit_config_levels, TBD if TYPE_CHECKING: pass @@ -59,7 +59,7 @@ class MetaParserBuilder(abc.ABCMeta): """Utlity class wrapping base-class methods into decorators that assure read-only properties""" - def __new__(cls, name, bases, clsdict): + def __new__(cls, name: str, bases: TBD, clsdict: Mapping[str, Any]) -> TBD: """ Equip all base-class methods with a needs_values decorator, and all non-const methods with a set_dirty_and_flush_changes decorator in addition to that.""" @@ -85,23 +85,23 @@ def __new__(cls, name, bases, clsdict): return new_type -def needs_values(func): +def needs_values(func: Callable) -> Callable: """Returns method assuring we read values (on demand) before we try to access them""" @wraps(func) - def assure_data_present(self, *args, **kwargs): + def assure_data_present(self, *args: Any, **kwargs: Any) -> Any: self.read() return func(self, *args, **kwargs) # END wrapper method return assure_data_present -def set_dirty_and_flush_changes(non_const_func): +def set_dirty_and_flush_changes(non_const_func: Callable) -> Callable: """Return method that checks whether given non constant function may be called. If so, the instance will be set dirty. Additionally, we flush the changes right to disk""" - def flush_changes(self, *args, **kwargs): + def flush_changes(self, *args: Any, **kwargs: Any) -> Any: rval = non_const_func(self, *args, **kwargs) self._dirty = True self.write() @@ -206,7 +206,7 @@ def items_all(self): return [(k, self.getall(k)) for k in self] -def get_config_path(config_level: Literal['system', 'global', 'user', 'repository']) -> str: +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 diff --git a/git/repo/base.py b/git/repo/base.py index 94c6e30b0..ce5f6bd09 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -34,7 +34,7 @@ # typing ------------------------------------------------------ -from git.types import TBD, PathLike, Literal +from git.types import TBD, PathLike, Lit_config_levels from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, TextIO, Tuple, Type, Union, @@ -45,7 +45,6 @@ from git.refs.symbolic import SymbolicReference from git.objects import TagObject, Blob, Tree # NOQA: F401 -Lit_config_levels = Literal['system', 'global', 'user', 'repository'] # ----------------------------------------------------------- diff --git a/git/types.py b/git/types.py index 40d4f7885..6454bf0fa 100644 --- a/git/types.py +++ b/git/types.py @@ -12,8 +12,6 @@ from typing_extensions import Final, Literal # noqa: F401 -TBD = Any - if sys.version_info[:2] < (3, 6): # os.PathLike (PEP-519) only got introduced with Python 3.6 PathLike = str @@ -23,3 +21,7 @@ elif sys.version_info[:2] >= (3, 9): # os.PathLike only becomes subscriptable from Python 3.9 onwards PathLike = Union[str, os.PathLike[str]] + +TBD = Any + +Lit_config_levels = Literal['system', 'global', 'user', 'repository'] diff --git a/mypy.ini b/mypy.ini index 8f86a6af7..d55d21647 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ [mypy] # TODO: enable when we've fully annotated everything -# disallow_untyped_defs = True +disallow_untyped_defs = True # TODO: remove when 'gitdb' is fully annotated [mypy-gitdb.*] From 6e331a0b5e2acd1938bf4906aadf7276bc7f1b60 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 20:41:27 +0100 Subject: [PATCH 0046/1849] Add types to config.py class SectionConstraint --- git/config.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/git/config.py b/git/config.py index 9f4d5ad9e..b58870571 100644 --- a/git/config.py +++ b/git/config.py @@ -124,40 +124,40 @@ class SectionConstraint(object): _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", "remove_section", "remove_option", "options") - def __init__(self, config, section): + def __init__(self, config: cp.ConfigParser, section: str) -> None: self._config = config self._section_name = section - def __del__(self): + def __del__(self) -> None: # Yes, for some reason, we have to call it explicitly for it to work in PY3 ! # Apparently __del__ doesn't get call anymore if refcount becomes 0 # Ridiculous ... . self._config.release() - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: if attr in self._valid_attrs_: return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) return super(SectionConstraint, self).__getattribute__(attr) - def _call_config(self, method, *args, **kwargs): + def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: """Call the configuration at the given method which must take a section name as first argument""" return getattr(self._config, method)(self._section_name, *args, **kwargs) @property - def config(self): + def config(self) -> cp.ConfigParser: """return: Configparser instance we constrain""" return self._config - def release(self): + def release(self) -> None: """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" return self._config.release() - def __enter__(self): + def __enter__(self) -> 'SectionConstraint': self._config.__enter__() return self - def __exit__(self, exception_type, exception_value, traceback): + def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: self._config.__exit__(exception_type, exception_value, traceback) @@ -336,10 +336,10 @@ def __enter__(self): self._acquire_lock() return self - def __exit__(self, exception_type, exception_value, traceback): + def __exit__(self, exception_type, exception_value, traceback) -> None: self.release() - def release(self): + def release(self) -> None: """Flush changes and release the configuration write lock. This instance must not be used anymore afterwards. In Python 3, it's required to explicitly release locks and flush changes, as __del__ is not called deterministically anymore.""" From e21d96a76c223064a3b351fe062d5452da7670cd Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 20:50:18 +0100 Subject: [PATCH 0047/1849] Add types to config.py class _OMD --- git/config.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/git/config.py b/git/config.py index b58870571..68634d906 100644 --- a/git/config.py +++ b/git/config.py @@ -31,7 +31,7 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Mapping, TYPE_CHECKING, Tuple +from typing import Any, Callable, List, Mapping, TYPE_CHECKING, Tuple, Union, overload from git.types import Literal, Lit_config_levels, TBD @@ -164,26 +164,25 @@ def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> class _OMD(OrderedDict): """Ordered multi-dict.""" - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: super(_OMD, self).__setitem__(key, [value]) - def add(self, key, value): + def add(self, key: str, value: Any) -> None: if key not in self: super(_OMD, self).__setitem__(key, [value]) - return - + return None super(_OMD, self).__getitem__(key).append(value) - def setall(self, key, values): + def setall(self, key: str, values: Any) -> None: super(_OMD, self).__setitem__(key, values) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return super(_OMD, self).__getitem__(key)[-1] - def getlast(self, key): + def getlast(self, key: str) -> Any: return super(_OMD, self).__getitem__(key)[-1] - def setlast(self, key, value): + def setlast(self, key: str, value: Any) -> None: if key not in self: super(_OMD, self).__setitem__(key, [value]) return @@ -191,17 +190,25 @@ def setlast(self, key, value): prior = super(_OMD, self).__getitem__(key) prior[-1] = value - def get(self, key, default=None): + @overload + def get(self, key: str, default: None = ...) -> None: + ... + + @overload + def get(self, key: str, default: Any = ...) -> Any: + ... + + def get(self, key: str, default: Union[Any, None] = None) -> Union[Any, None]: return super(_OMD, self).get(key, [default])[-1] - def getall(self, key): + def getall(self, key: str) -> Any: return super(_OMD, self).__getitem__(key) - def items(self): + def items(self) -> List[Tuple[str, Any]]: """List of (key, last value for key).""" return [(k, self[k]) for k in self] - def items_all(self): + def items_all(self) -> List[Tuple[str, List[Any]]]: """List of (key, list of values for key).""" return [(k, self.getall(k)) for k in self] From efc259833ee184888fe21105d63b3c2aa3d51cfa Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 21:55:42 +0100 Subject: [PATCH 0048/1849] Add types to config.py GitConfigParser .__init__() .aquire_lock() --- git/config.py | 43 ++++++++++++++++++++++++++----------------- mypy.ini | 2 +- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/git/config.py b/git/config.py index 68634d906..7465cd5b3 100644 --- a/git/config.py +++ b/git/config.py @@ -29,14 +29,16 @@ import configparser as cp +from pathlib import Path + # typing------------------------------------------------------- -from typing import Any, Callable, List, Mapping, TYPE_CHECKING, Tuple, Union, overload +from typing import Any, Callable, IO, List, Dict, Sequence, TYPE_CHECKING, Tuple, Union, cast, overload -from git.types import Literal, Lit_config_levels, TBD +from git.types import Literal, Lit_config_levels, PathLike, TBD if TYPE_CHECKING: - pass + from git.repo.base import Repo # ------------------------------------------------------------- @@ -59,7 +61,7 @@ class MetaParserBuilder(abc.ABCMeta): """Utlity class wrapping base-class methods into decorators that assure read-only properties""" - def __new__(cls, name: str, bases: TBD, clsdict: Mapping[str, Any]) -> TBD: + def __new__(cls, name: str, bases: TBD, clsdict: Dict[str, Any]) -> TBD: """ Equip all base-class methods with a needs_values decorator, and all non-const methods with a set_dirty_and_flush_changes decorator in addition to that.""" @@ -124,7 +126,7 @@ class SectionConstraint(object): _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", "remove_section", "remove_option", "options") - def __init__(self, config: cp.ConfigParser, section: str) -> None: + def __init__(self, config: 'GitConfigParser', section: str) -> None: self._config = config self._section_name = section @@ -145,7 +147,7 @@ def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: return getattr(self._config, method)(self._section_name, *args, **kwargs) @property - def config(self) -> cp.ConfigParser: + def config(self) -> 'GitConfigParser': """return: Configparser instance we constrain""" return self._config @@ -204,7 +206,7 @@ def get(self, key: str, default: Union[Any, None] = None) -> Union[Any, None]: def getall(self, key: str) -> Any: return super(_OMD, self).__getitem__(key) - def items(self) -> List[Tuple[str, Any]]: + def items(self) -> List[Tuple[str, Any]]: # type: ignore ## mypy doesn't like overwriting supertype signitures """List of (key, last value for key).""" return [(k, self[k]) for k in self] @@ -271,7 +273,10 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje # list of RawConfigParser methods able to change the instance _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") - def __init__(self, file_or_files=None, read_only=True, merge_includes=True, config_level=None, repo=None): + def __init__(self, file_or_files: Union[None, PathLike, IO, Sequence[Union[PathLike, IO]]] = None, + read_only: bool = True, merge_includes: bool = True, + config_level: Union[Lit_config_levels, None] = None, + repo: Union['Repo', None] = None) -> None: """Initialize a configuration reader to read the given file_or_files and to possibly allow changes to it by setting read_only False @@ -297,11 +302,13 @@ def __init__(self, file_or_files=None, read_only=True, merge_includes=True, conf self._proxies = self._dict() if file_or_files is not None: - self._file_or_files = file_or_files + self._file_or_files = file_or_files # type: Union[PathLike, IO, Sequence[Union[PathLike, IO]]] else: if config_level is None: if read_only: - self._file_or_files = [get_config_path(f) for f in CONFIG_LEVELS if f != 'repository'] + self._file_or_files = [get_config_path(f) # type: ignore + for f in CONFIG_LEVELS # Can type f properly when 3.5 dropped + if f != 'repository'] else: raise ValueError("No configuration level or configuration files specified") else: @@ -312,20 +319,21 @@ def __init__(self, file_or_files=None, read_only=True, merge_includes=True, conf self._is_initialized = False self._merge_includes = merge_includes self._repo = repo - self._lock = None + self._lock = None # type: Union['LockFile', None] self._acquire_lock() - def _acquire_lock(self): + def _acquire_lock(self) -> None: if not self._read_only: if not self._lock: - if isinstance(self._file_or_files, (tuple, list)): + if isinstance(self._file_or_files, (tuple, list, Sequence)): raise ValueError( "Write-ConfigParsers can operate on a single file only, multiple files have been passed") # END single file check - file_or_files = self._file_or_files - if not isinstance(self._file_or_files, str): - file_or_files = self._file_or_files.name + if not isinstance(self._file_or_files, (str, Path)): # cannot narrow by os._pathlike until 3.5 dropped + file_or_files = cast(IO, self._file_or_files).name # type: PathLike + else: + file_or_files = self._file_or_files # END get filename from handle/stream # initialize lock base - we want to write self._lock = self.t_lock(file_or_files) @@ -366,7 +374,8 @@ def release(self) -> None: # Usually when shutting down the interpreter, don'y know how to fix this pass finally: - self._lock._release_lock() + if self._lock is not None: + self._lock._release_lock() def optionxform(self, optionstr): """Do not transform options in any way when writing""" diff --git a/mypy.ini b/mypy.ini index d55d21647..8f86a6af7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,7 +2,7 @@ [mypy] # TODO: enable when we've fully annotated everything -disallow_untyped_defs = True +# disallow_untyped_defs = True # TODO: remove when 'gitdb' is fully annotated [mypy-gitdb.*] From 94b7ece1794901feddf98fcac3a672f81aa6a6e1 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sat, 8 May 2021 22:13:31 +0100 Subject: [PATCH 0049/1849] Add types to config.py GitConfigParser .release() ._read() ._has_includes() ._included_paths() .__del__() .__exit__() .__enter__() ._optionform() --- git/config.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/git/config.py b/git/config.py index 7465cd5b3..31ef11fa8 100644 --- a/git/config.py +++ b/git/config.py @@ -330,10 +330,11 @@ def _acquire_lock(self) -> None: "Write-ConfigParsers can operate on a single file only, multiple files have been passed") # END single file check - if not isinstance(self._file_or_files, (str, Path)): # cannot narrow by os._pathlike until 3.5 dropped - file_or_files = cast(IO, self._file_or_files).name # type: PathLike - else: + if isinstance(self._file_or_files, (str, Path)): # cannot narrow by os._pathlike until 3.5 dropped file_or_files = self._file_or_files + else: + file_or_files = cast(IO, self._file_or_files).name + # END get filename from handle/stream # initialize lock base - we want to write self._lock = self.t_lock(file_or_files) @@ -342,12 +343,12 @@ def _acquire_lock(self) -> None: self._lock._obtain_lock() # END read-only check - def __del__(self): + def __del__(self) -> None: """Write pending changes if required and release locks""" # NOTE: only consistent in PY2 self.release() - def __enter__(self): + def __enter__(self) -> 'GitConfigParser': self._acquire_lock() return self @@ -377,11 +378,11 @@ def release(self) -> None: if self._lock is not None: self._lock._release_lock() - def optionxform(self, optionstr): + def optionxform(self, optionstr: str) -> str: """Do not transform options in any way when writing""" return optionstr - def _read(self, fp, fpname): + def _read(self, fp: IO[bytes], fpname: str) -> None: """A direct copy of the py2.4 version of the super class's _read method to assure it uses ordered dicts. Had to change one line to make it work. @@ -397,7 +398,7 @@ def _read(self, fp, fpname): is_multi_line = False e = None # None, or an exception - def string_decode(v): + def string_decode(v: str) -> str: if v[-1] == '\\': v = v[:-1] # end cut trailing escapes to prevent decode error @@ -479,11 +480,12 @@ def string_decode(v): if e: raise e - def _has_includes(self): + def _has_includes(self) -> Union[bool, int]: return self._merge_includes and len(self._included_paths()) - def _included_paths(self): - """Return all paths that must be included to configuration. + def _included_paths(self) -> List[Tuple[str, str]]: + """Return List all paths that must be included to configuration + as Tuples of (option, value). """ paths = [] @@ -516,9 +518,9 @@ def _included_paths(self): ), value ) - - if fnmatch.fnmatchcase(self._repo.git_dir, value): - paths += self.items(section) + if self._repo.git_dir: + if fnmatch.fnmatchcase(str(self._repo.git_dir), value): + paths += self.items(section) elif keyword == "onbranch": try: From c6e458c9f8682ab5091e15e637c66ad6836f23b4 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 9 May 2021 16:40:35 +0100 Subject: [PATCH 0050/1849] Add types to config.py GitConfigParser .read() --- git/config.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/git/config.py b/git/config.py index 31ef11fa8..11b20c6f0 100644 --- a/git/config.py +++ b/git/config.py @@ -9,7 +9,7 @@ import abc from functools import wraps import inspect -from io import IOBase +from io import BufferedReader, IOBase import logging import os import re @@ -325,7 +325,7 @@ def __init__(self, file_or_files: Union[None, PathLike, IO, Sequence[Union[PathL def _acquire_lock(self) -> None: if not self._read_only: if not self._lock: - if isinstance(self._file_or_files, (tuple, list, Sequence)): + if isinstance(self._file_or_files, (tuple, list)): raise ValueError( "Write-ConfigParsers can operate on a single file only, multiple files have been passed") # END single file check @@ -382,7 +382,7 @@ def optionxform(self, optionstr: str) -> str: """Do not transform options in any way when writing""" return optionstr - def _read(self, fp: IO[bytes], fpname: str) -> None: + def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None: """A direct copy of the py2.4 version of the super class's _read method to assure it uses ordered dicts. Had to change one line to make it work. @@ -534,33 +534,38 @@ def _included_paths(self) -> List[Tuple[str, str]]: return paths - def read(self): + def read(self) -> None: """Reads the data stored in the files we have been initialized with. It will ignore files that cannot be read, possibly leaving an empty configuration :return: Nothing :raise IOError: if a file cannot be handled""" if self._is_initialized: - return + return None self._is_initialized = True - if not isinstance(self._file_or_files, (tuple, list)): - files_to_read = [self._file_or_files] + files_to_read = [""] # type: List[Union[PathLike, IO]] ## just for types until 3.5 dropped + if isinstance(self._file_or_files, (str)): # replace with PathLike once 3.5 dropped + files_to_read = [self._file_or_files] # for str, as str is a type of Sequence + elif not isinstance(self._file_or_files, (tuple, list, Sequence)): + files_to_read = [self._file_or_files] # for IO or Path else: - files_to_read = list(self._file_or_files) + files_to_read = list(self._file_or_files) # for lists or tuples # end assure we have a copy of the paths to handle seen = set(files_to_read) num_read_include_files = 0 while files_to_read: file_path = files_to_read.pop(0) - fp = file_path file_ok = False - if hasattr(fp, "seek"): - self._read(fp, fp.name) + if hasattr(file_path, "seek"): + # must be a file objectfile-object + file_path = cast(IO[bytes], file_path) # replace with assert to narrow type, once sure + self._read(file_path, file_path.name) else: # assume a path if it is not a file-object + file_path = cast(PathLike, file_path) try: with open(file_path, 'rb') as fp: file_ok = True @@ -578,6 +583,7 @@ def read(self): if not file_ok: continue # end ignore relative paths if we don't know the configuration file path + file_path = cast(PathLike, file_path) assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work" include_path = osp.join(osp.dirname(file_path), include_path) # end make include path absolute From ab69b9a67520f18dd8efd338e6e599a77b46bb34 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 9 May 2021 16:53:08 +0100 Subject: [PATCH 0051/1849] Add types to config.py GitConfigParser .write() ._write() .items() .items_all() --- git/config.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/git/config.py b/git/config.py index 11b20c6f0..4fbe5d83f 100644 --- a/git/config.py +++ b/git/config.py @@ -604,7 +604,7 @@ def read(self) -> None: self._merge_includes = False # end - def _write(self, fp): + def _write(self, fp: IO) -> None: """Write an .ini-format representation of the configuration state in git compatible format""" def write_section(name, section_dict): @@ -623,11 +623,11 @@ def write_section(name, section_dict): for name, value in self._sections.items(): write_section(name, value) - def items(self, section_name): + def items(self, section_name: str) -> List[Tuple[str, str]]: """:return: list((option, value), ...) pairs of all items in the given section""" return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__'] - def items_all(self, section_name): + def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]: """:return: list((option, [values...]), ...) pairs of all items in the given section""" rv = _OMD(self._defaults) @@ -644,7 +644,7 @@ def items_all(self, section_name): return rv.items_all() @needs_values - def write(self): + def write(self) -> None: """Write changes to our file, if there are changes at all :raise IOError: if this is a read-only writer instance or if we could not obtain @@ -661,19 +661,21 @@ def write(self): if self._has_includes(): log.debug("Skipping write-back of configuration file as include files were merged in." + "Set merge_includes=False to prevent this.") - return + return None # end fp = self._file_or_files # we have a physical file on disk, so get a lock is_file_lock = isinstance(fp, (str, IOBase)) - if is_file_lock: + if is_file_lock and self._lock is not None: # else raise Error? self._lock._obtain_lock() if not hasattr(fp, "seek"): - with open(self._file_or_files, "wb") as fp: - self._write(fp) + self._file_or_files = cast(PathLike, self._file_or_files) + with open(self._file_or_files, "wb") as fp_open: + self._write(fp_open) else: + fp = cast(IO, fp) fp.seek(0) # make sure we do not overwrite into an existing file if hasattr(fp, 'truncate'): From c2f9f4e7fd8af09126167fd1dfa151be4fedcd71 Mon Sep 17 00:00:00 2001 From: yobmod Date: Sun, 9 May 2021 17:15:01 +0100 Subject: [PATCH 0052/1849] Add types to config.py GitConfigParser ._assure_writable .add_section .read_only .get_value .get_values ._string_to_value ._value_to_string .add_value .rename_section --- git/config.py | 32 +++++++++++++++++--------------- git/types.py | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/git/config.py b/git/config.py index 4fbe5d83f..cc6fcfa4f 100644 --- a/git/config.py +++ b/git/config.py @@ -667,12 +667,13 @@ def write(self) -> None: fp = self._file_or_files # we have a physical file on disk, so get a lock - is_file_lock = isinstance(fp, (str, IOBase)) + is_file_lock = isinstance(fp, (str, IOBase)) # can't use Pathlike until 3.5 dropped if is_file_lock and self._lock is not None: # else raise Error? self._lock._obtain_lock() + if not hasattr(fp, "seek"): - self._file_or_files = cast(PathLike, self._file_or_files) - with open(self._file_or_files, "wb") as fp_open: + fp = cast(PathLike, fp) + with open(fp, "wb") as fp_open: self._write(fp_open) else: fp = cast(IO, fp) @@ -682,20 +683,22 @@ def write(self) -> None: fp.truncate() self._write(fp) - def _assure_writable(self, method_name): + def _assure_writable(self, method_name: str) -> None: if self.read_only: raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) - def add_section(self, section): + def add_section(self, section: str) -> None: """Assures added options will stay in order""" return super(GitConfigParser, self).add_section(section) @property - def read_only(self): + def read_only(self) -> bool: """:return: True if this instance may change the configuration file""" return self._read_only - def get_value(self, section, option, default=None): + def get_value(self, section: str, option: str, default: Union[int, float, str, bool, None] = None + ) -> Union[int, float, str, bool]: + # can default or return type include bool? """Get an option's value. If multiple values are specified for this option in the section, the @@ -717,7 +720,8 @@ def get_value(self, section, option, default=None): return self._string_to_value(valuestr) - def get_values(self, section, option, default=None): + def get_values(self, section: str, option: str, default: Union[int, float, str, bool, None] = None + ) -> List[Union[int, float, str, bool]]: """Get an option's values. If multiple values are specified for this option in the section, all are @@ -739,16 +743,14 @@ def get_values(self, section, option, default=None): return [self._string_to_value(valuestr) for valuestr in lst] - def _string_to_value(self, valuestr): + def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]: types = (int, float) for numtype in types: try: val = numtype(valuestr) - # truncated value ? if val != float(valuestr): continue - return val except (ValueError, TypeError): continue @@ -768,14 +770,14 @@ def _string_to_value(self, valuestr): return valuestr - def _value_to_string(self, value): + def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str: if isinstance(value, (int, float, bool)): return str(value) return force_text(value) @needs_values @set_dirty_and_flush_changes - def set_value(self, section, option, value): + def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> 'GitConfigParser': """Sets the given option in section to the given value. It will create the section if required, and will not throw as opposed to the default ConfigParser 'set' method. @@ -793,7 +795,7 @@ def set_value(self, section, option, value): @needs_values @set_dirty_and_flush_changes - def add_value(self, section, option, value): + def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> 'GitConfigParser': """Adds a value for the given option in section. It will create the section if required, and will not throw as opposed to the default ConfigParser 'set' method. The value becomes the new value of the option as returned @@ -810,7 +812,7 @@ def add_value(self, section, option, value): self._sections[section].add(option, self._value_to_string(value)) return self - def rename_section(self, section, new_name): + def rename_section(self, section: str, new_name: str) -> 'GitConfigParser': """rename the given section to new_name :raise ValueError: if section doesn't exit :raise ValueError: if a section with new_name does already exist diff --git a/git/types.py b/git/types.py index 6454bf0fa..91d35b567 100644 --- a/git/types.py +++ b/git/types.py @@ -20,7 +20,7 @@ PathLike = Union[str, os.PathLike] elif sys.version_info[:2] >= (3, 9): # os.PathLike only becomes subscriptable from Python 3.9 onwards - PathLike = Union[str, os.PathLike[str]] + PathLike = Union[str, 'os.PathLike[str]'] # forward ref as pylance complains unless editing with py3.9+ TBD = Any From 3473060f4b356a6c8ed744ba17ad9aa26ef6aab7 Mon Sep 17 00:00:00 2001 From: yobmod Date: Wed, 12 May 2021 17:03:10 +0100 Subject: [PATCH 0053/1849] Add typing section to cmd.py --- git/cmd.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/git/cmd.py b/git/cmd.py index ac3ca2ec1..cafe999ab 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -40,6 +40,18 @@ stream_copy, ) +# typing --------------------------------------------------------------------------- + +from typing import TYPE_CHECKING + +from git.types import TBD + +if TYPE_CHECKING: + pass + + +# --------------------------------------------------------------------------------- + execute_kwargs = {'istream', 'with_extended_output', 'with_exceptions', 'as_process', 'stdout_as_string', 'output_stream', 'with_stdout', 'kill_after_timeout', From 887f249a2241d45765437b295b46bca1597d91a3 Mon Sep 17 00:00:00 2001 From: yobmod Date: Wed, 12 May 2021 17:50:51 +0100 Subject: [PATCH 0054/1849] Add types to cmd.py Git --- git/cmd.py | 40 +++++++++++++++++++++++----------------- git/util.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index cafe999ab..ac4cdf30b 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -42,12 +42,12 @@ # typing --------------------------------------------------------------------------- -from typing import TYPE_CHECKING +from typing import Any, BinaryIO, Callable, Dict, Mapping, Sequence, TYPE_CHECKING, Union -from git.types import TBD +from git.types import PathLike, TBD if TYPE_CHECKING: - pass + pass # --------------------------------------------------------------------------------- @@ -69,8 +69,11 @@ # Documentation ## @{ -def handle_process_output(process, stdout_handler, stderr_handler, - finalizer=None, decode_streams=True): +def handle_process_output(process: subprocess.Popen, + stdout_handler: Union[None, Callable[[str], None]], + stderr_handler: Union[None, Callable[[str], None]], + finalizer: Union[None, Callable[[subprocess.Popen], TBD]] = None, + decode_streams: bool = True) -> Union[None, TBD]: # TBD is whatever finalizer returns """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -87,13 +90,14 @@ def handle_process_output(process, stdout_handler, stderr_handler, or if decoding must happen later (i.e. for Diffs). """ # Use 2 "pump" threads and wait for both to finish. - def pump_stream(cmdline, name, stream, is_decode, handler): + def pump_stream(cmdline: str, name: str, stream: BinaryIO, is_decode: bool, + handler: Union[None, Callable[[str], None]]) -> None: try: for line in stream: if handler: if is_decode: - line = line.decode(defenc) - handler(line) + line_str = line.decode(defenc) + handler(line_str) except Exception as ex: log.error("Pumping %r of cmd(%s) failed due to: %r", name, remove_password_if_present(cmdline), ex) raise CommandError(['<%s-pump>' % name] + remove_password_if_present(cmdline), ex) from ex @@ -126,17 +130,20 @@ def pump_stream(cmdline, name, stream, is_decode, handler): if finalizer: return finalizer(process) + else: + return None -def dashify(string): +def dashify(string: str) -> str: return string.replace('_', '-') -def slots_to_dict(self, exclude=()): +def slots_to_dict(self, exclude: Sequence[str] = ()) -> Dict[str, Any]: + # annotate self.__slots__ as Tuple[str, ...] once 3.5 dropped return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} -def dict_to_slots_and__excluded_are_none(self, d, excluded=()): +def dict_to_slots_and__excluded_are_none(self, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None: for k, v in d.items(): setattr(self, k, v) for k in excluded: @@ -175,10 +182,10 @@ class Git(LazyMixin): _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info') - def __getstate__(self): + def __getstate__(self) -> Dict[str, Any]: return slots_to_dict(self, exclude=self._excluded_) - def __setstate__(self, d): + def __setstate__(self, d) -> None: dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_) # CONFIGURATION @@ -202,7 +209,7 @@ def __setstate__(self, d): # the top level __init__ @classmethod - def refresh(cls, path=None): + def refresh(cls, path: Union[None, PathLike] = None) -> bool: """This gets called by the refresh function (see the top level __init__). """ @@ -317,11 +324,11 @@ def refresh(cls, path=None): return has_git @classmethod - def is_cygwin(cls): + def is_cygwin(cls) -> bool: return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE) @classmethod - def polish_url(cls, url, is_cygwin=None): + def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: if is_cygwin is None: is_cygwin = cls.is_cygwin() @@ -338,7 +345,6 @@ def polish_url(cls, url, is_cygwin=None): if url.startswith('~'): url = os.path.expanduser(url) url = url.replace("\\\\", "\\").replace("\\", "/") - return url class AutoInterrupt(object): diff --git a/git/util.py b/git/util.py index 558be1e4d..76ac92f18 100644 --- a/git/util.py +++ b/git/util.py @@ -22,11 +22,13 @@ # typing --------------------------------------------------------- from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List, - Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING) + Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING, overload) + + if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo -from .types import PathLike, TBD +from .types import PathLike, TBD, Literal # --------------------------------------------------------------------- @@ -279,9 +281,20 @@ def _cygexpath(drive: Optional[str], path: PathLike) -> str: ) # type: Tuple[Tuple[Pattern[str], Callable, bool], ...] +@overload +def cygpath(path: str) -> str: + ... + + +@overload +def cygpath(path: PathLike) -> PathLike: + ... + + def cygpath(path: PathLike) -> PathLike: """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) @@ -314,10 +327,23 @@ def decygpath(path: PathLike) -> str: _is_cygwin_cache = {} # type: 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 + if git_executable is None: + return False # or raise error? + #from subprocess import check_output git_executable = str(git_executable) is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool] From f1ace258417deae329880754987851b1b8fc0a7a Mon Sep 17 00:00:00 2001 From: yobmod Date: Wed, 12 May 2021 18:10:37 +0100 Subject: [PATCH 0055/1849] Add types to cmd.py AutoInterrupt --- git/cmd.py | 37 ++++++++++++++++++++----------------- git/exc.py | 2 +- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index ac4cdf30b..74113ce89 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -357,11 +357,11 @@ class AutoInterrupt(object): __slots__ = ("proc", "args") - def __init__(self, proc, args): + def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.proc = proc self.args = args - def __del__(self): + def __del__(self) -> None: if self.proc is None: return @@ -377,13 +377,13 @@ def __del__(self): # did the process finish already so we have a return code ? try: if proc.poll() is not None: - return + return None except OSError as ex: log.info("Ignored error after process had died: %r", ex) # can be that nothing really exists anymore ... if os is None or getattr(os, 'kill', None) is None: - return + return None # try to kill it try: @@ -400,10 +400,11 @@ def __del__(self): call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) # END exception handling - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: return getattr(self.proc, attr) - def wait(self, stderr=b''): # TODO: Bad choice to mimic `proc.wait()` but with different args. + # TODO: Bad choice to mimic `proc.wait()` but with different args. + def wait(self, stderr: Union[None, bytes] = b'') -> int: """Wait for the process and return its status code. :param stderr: Previously read value of stderr, in case stderr is already closed. @@ -413,20 +414,22 @@ def wait(self, stderr=b''): # TODO: Bad choice to mimic `proc.wait()` but with stderr = b'' stderr = force_bytes(data=stderr, encoding='utf-8') - status = self.proc.wait() - - def read_all_from_possibly_closed_stream(stream): - try: - return stderr + force_bytes(stream.read()) - except ValueError: - return stderr or b'' + if self.proc is not None: + status = self.proc.wait() - if status != 0: - errstr = read_all_from_possibly_closed_stream(self.proc.stderr) - log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) - raise GitCommandError(remove_password_if_present(self.args), status, errstr) + def read_all_from_possibly_closed_stream(stream): + try: + return stderr + force_bytes(stream.read()) + except ValueError: + return stderr or b'' + + if status != 0: + errstr = read_all_from_possibly_closed_stream(self.proc.stderr) + log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) + raise GitCommandError(remove_password_if_present(self.args), status, errstr) # END status handling return status + # END auto interrupt class CatFileContentStream(object): diff --git a/git/exc.py b/git/exc.py index 6e646921c..bcf5aabbd 100644 --- a/git/exc.py +++ b/git/exc.py @@ -91,7 +91,7 @@ class GitCommandError(CommandError): """ Thrown if execution of the git command fails with non-zero status code. """ def __init__(self, command: Union[List[str], Tuple[str, ...], str], - status: Union[str, None, Exception] = None, + status: Union[str, int, None, Exception] = None, stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None, ) -> None: From 39eb0e607f86537929a372f3ef33c9721984565a Mon Sep 17 00:00:00 2001 From: yobmod Date: Wed, 12 May 2021 18:16:40 +0100 Subject: [PATCH 0056/1849] Add types to cmd.py CatFileContentStream --- git/cmd.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 74113ce89..4c8a87d44 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -42,7 +42,7 @@ # typing --------------------------------------------------------------------------- -from typing import Any, BinaryIO, Callable, Dict, Mapping, Sequence, TYPE_CHECKING, Union +from typing import Any, BinaryIO, Callable, Dict, List, Mapping, Sequence, TYPE_CHECKING, Union from git.types import PathLike, TBD @@ -443,7 +443,7 @@ class CatFileContentStream(object): __slots__ = ('_stream', '_nbr', '_size') - def __init__(self, size, stream): + def __init__(self, size: int, stream: BinaryIO) -> None: self._stream = stream self._size = size self._nbr = 0 # num bytes read @@ -454,7 +454,7 @@ def __init__(self, size, stream): stream.read(1) # END handle empty streams - def read(self, size=-1): + def read(self, size: int = -1) -> bytes: bytes_left = self._size - self._nbr if bytes_left == 0: return b'' @@ -474,7 +474,7 @@ def read(self, size=-1): # END finish reading return data - def readline(self, size=-1): + def readline(self, size: int = -1) -> bytes: if self._nbr == self._size: return b'' @@ -496,7 +496,7 @@ def readline(self, size=-1): return data - def readlines(self, size=-1): + def readlines(self, size: int = -1) -> List[bytes]: if self._nbr == self._size: return [] @@ -517,20 +517,20 @@ def readlines(self, size=-1): return out # skipcq: PYL-E0301 - def __iter__(self): + def __iter__(self) -> 'Git.CatFileContentStream': return self - def __next__(self): + def __next__(self) -> bytes: return self.next() - def next(self): + def next(self) -> bytes: line = self.readline() if not line: raise StopIteration return line - def __del__(self): + def __del__(self) -> None: bytes_left = self._size - self._nbr if bytes_left: # read and discard - seeking is impossible within a stream @@ -538,7 +538,7 @@ def __del__(self): self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir=None): + def __init__(self, working_dir: Union[None, PathLike]=None): """Initialize this instance with: :param working_dir: From f62c8d8bbb566edd9e7a40155c7380944cf65dfb Mon Sep 17 00:00:00 2001 From: yobmod Date: Thu, 13 May 2021 00:48:39 +0100 Subject: [PATCH 0057/1849] Add types to cmd.py Git --- git/cmd.py | 225 +++++++++++++++++++++++++++++++++++--------------- git/compat.py | 4 +- git/diff.py | 10 ++- git/exc.py | 13 +-- git/util.py | 25 ++++-- 5 files changed, 194 insertions(+), 83 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 4c8a87d44..7b4ebc178 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -42,12 +42,14 @@ # typing --------------------------------------------------------------------------- -from typing import Any, BinaryIO, Callable, Dict, List, Mapping, Sequence, TYPE_CHECKING, Union +from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, List, Mapping, + Sequence, TYPE_CHECKING, Tuple, Union, cast, overload) -from git.types import PathLike, TBD +from git.types import PathLike, Literal, TBD if TYPE_CHECKING: - pass + from git.repo.base import Repo + from git.diff import DiffIndex # --------------------------------------------------------------------------------- @@ -70,10 +72,16 @@ ## @{ def handle_process_output(process: subprocess.Popen, - stdout_handler: Union[None, Callable[[str], None]], - stderr_handler: Union[None, Callable[[str], None]], - finalizer: Union[None, Callable[[subprocess.Popen], TBD]] = None, - decode_streams: bool = True) -> Union[None, TBD]: # TBD is whatever finalizer returns + stdout_handler: Union[None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None], + Callable[[bytes, 'Repo', 'DiffIndex'], None]], + stderr_handler: Union[None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None]], + finalizer: Union[None, + Callable[[subprocess.Popen], None]] = None, + decode_streams: bool = True) -> None: """Registers for notifications to learn that process output is ready to read, and dispatches lines to the respective line handlers. This function returns once the finalizer returns @@ -327,8 +335,18 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: def is_cygwin(cls) -> bool: return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE) + @overload @classmethod def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: + ... + + @overload + @classmethod + def polish_url(cls, url: PathLike, is_cygwin: Union[None, bool] = None) -> PathLike: + ... + + @classmethod + def polish_url(cls, url: PathLike, is_cygwin: Union[None, bool] = None) -> PathLike: if is_cygwin is None: is_cygwin = cls.is_cygwin() @@ -443,7 +461,7 @@ class CatFileContentStream(object): __slots__ = ('_stream', '_nbr', '_size') - def __init__(self, size: int, stream: BinaryIO) -> None: + def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream self._size = size self._nbr = 0 # num bytes read @@ -538,7 +556,7 @@ def __del__(self) -> None: self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir: Union[None, PathLike]=None): + def __init__(self, working_dir: Union[None, PathLike] = None): """Initialize this instance with: :param working_dir: @@ -548,17 +566,17 @@ def __init__(self, working_dir: Union[None, PathLike]=None): .git directory in case of bare repositories.""" super(Git, self).__init__() self._working_dir = expand_path(working_dir) - self._git_options = () - self._persistent_git_options = [] + self._git_options = () # type: Union[List[str], Tuple[str, ...]] + self._persistent_git_options = [] # type: List[str] # Extra environment variables to pass to git commands - self._environment = {} + self._environment = {} # type: Dict[str, str] # cached command slots self.cat_file_header = None self.cat_file_all = None - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: """A convenience method as it allows to call the command as if it was an object. :return: Callable object that will execute call _call_process with your arguments.""" @@ -566,7 +584,7 @@ def __getattr__(self, name): return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - def set_persistent_git_options(self, **kwargs): + def set_persistent_git_options(self, **kwargs: Any) -> None: """Specify command line options to the git executable for subsequent subcommand calls @@ -580,43 +598,96 @@ def set_persistent_git_options(self, **kwargs): self._persistent_git_options = self.transform_kwargs( split_single_char_options=True, **kwargs) - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr == '_version_info': # We only use the first 4 numbers, as everything else could be strings in fact (on windows) - version_numbers = self._call_process('version').split(' ')[2] - self._version_info = tuple(int(n) for n in version_numbers.split('.')[:4] if n.isdigit()) + process_version = self._call_process('version') # should be as default *args and **kwargs used + version_numbers = process_version.split(' ')[2] + + self._version_info = tuple( + int(n) for n in version_numbers.split('.')[:4] if n.isdigit() + ) # type: Tuple[int, int, int, int] # type: ignore else: super(Git, self)._set_cache_(attr) # END handle version info @property - def working_dir(self): + def working_dir(self) -> Union[None, str]: """:return: Git directory we are working on""" return self._working_dir @property - def version_info(self): + def version_info(self) -> Tuple[int, int, int, int]: """ :return: tuple(int, int, int, int) tuple with integers representing the major, minor and additional version numbers as parsed from git version. This value is generated on demand and is cached""" return self._version_info - def execute(self, command, - istream=None, - with_extended_output=False, - with_exceptions=True, - as_process=False, - output_stream=None, - stdout_as_string=True, - kill_after_timeout=None, - with_stdout=True, - universal_newlines=False, - shell=None, - env=None, - max_chunk_size=io.DEFAULT_BUFFER_SIZE, - **subprocess_kwargs - ): + @overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[True], + ) -> AutoInterrupt: + ... + + @overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[True], + ) -> Union[str, Tuple[int, str, str]]: + ... + + @overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[False] = False, + ) -> Union[bytes, Tuple[int, bytes, str]]: + ... + + @overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[True], + + ) -> str: + ... + + @overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[False], + + ) -> bytes: + ... + + def execute(self, + command: Union[str, Sequence[Any]], + istream: Union[None, BinaryIO] = None, + with_extended_output: bool = False, + with_exceptions: bool = True, + as_process: bool = False, + output_stream: Union[None, BinaryIO] = None, + stdout_as_string: bool = True, + kill_after_timeout: Union[None, int] = None, + with_stdout: bool = True, + universal_newlines: bool = False, + shell: Union[None, bool] = None, + env: Union[None, Mapping[str, str]] = None, + max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + **subprocess_kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: """Handles executing the command on the shell and consumes and returns the returned information (stdout) @@ -758,22 +829,31 @@ def execute(self, command, creationflags=PROC_CREATIONFLAGS, **subprocess_kwargs ) + proc = cast(Popen[bytes], proc) + + proc.stdout = cast(BinaryIO, proc.stdout) except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err + else: + assert isinstance(proc.stdout, BinaryIO) + assert isinstance(proc.stderr, BinaryIO) + # proc.stdout = cast(BinaryIO, proc.stdout) + # proc.stderr = cast(BinaryIO, proc.stderr) if as_process: return self.AutoInterrupt(proc, command) - def _kill_process(pid): + def _kill_process(pid: int) -> None: """ Callback method to kill a process. """ p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE, creationflags=PROC_CREATIONFLAGS) child_pids = [] - 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)) + 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: # Windows does not have SIGKILL, so use SIGTERM instead sig = getattr(signal, 'SIGKILL', signal.SIGTERM) @@ -797,8 +877,8 @@ def _kill_process(pid): # Wait for the process to return status = 0 - stdout_value = b'' - stderr_value = b'' + stdout_value = b'' # type: Union[str, bytes] + stderr_value = b'' # type: Union[str, bytes] newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: @@ -807,16 +887,17 @@ def _kill_process(pid): stdout_value, stderr_value = proc.communicate() if kill_after_timeout: watchdog.cancel() - if kill_check.isSet(): + 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) # strip trailing "\n" - if stdout_value.endswith(newline): + if stdout_value.endswith(newline): # type: ignore stdout_value = stdout_value[:-1] - if stderr_value.endswith(newline): + if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] + status = proc.returncode else: max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE @@ -824,7 +905,7 @@ def _kill_process(pid): stdout_value = proc.stdout.read() stderr_value = proc.stderr.read() # strip trailing "\n" - if stderr_value.endswith(newline): + if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] status = proc.wait() # END stdout handling @@ -908,7 +989,7 @@ def custom_environment(self, **kwargs): finally: self.update_environment(**old_env) - def transform_kwarg(self, name, value, split_single_char_options): + def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]: if len(name) == 1: if value is True: return ["-%s" % name] @@ -924,7 +1005,7 @@ def transform_kwarg(self, name, value, split_single_char_options): return ["--%s=%s" % (dashify(name), value)] return [] - def transform_kwargs(self, split_single_char_options=True, **kwargs): + def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: """Transforms Python style kwargs into git command line options.""" # Python 3.6 preserves the order of kwargs and thus has a stable # order. For older versions sort the kwargs by the key to get a stable @@ -943,7 +1024,7 @@ def transform_kwargs(self, split_single_char_options=True, **kwargs): return args @classmethod - def __unpack_args(cls, arg_list): + def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]: if not isinstance(arg_list, (list, tuple)): return [str(arg_list)] @@ -957,7 +1038,7 @@ def __unpack_args(cls, arg_list): # END for each arg return outlist - def __call__(self, **kwargs): + def __call__(self, **kwargs: Any) -> 'Git': """Specify command line options to the git executable for a subcommand call @@ -973,7 +1054,18 @@ def __call__(self, **kwargs): split_single_char_options=True, **kwargs) return self - def _call_process(self, method, *args, **kwargs): + @overload + def _call_process(self, method: str, *args: None, **kwargs: None + ) -> str: + ... # if no args given, execute called with all defaults + + @overload + def _call_process(self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + ... + + def _call_process(self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: """Run the given git command with the specified arguments and return the result as a String @@ -1001,7 +1093,9 @@ def _call_process(self, method, *args, **kwargs): git rev-list max-count 10 --header master - :return: Same as ``execute``""" + :return: Same as ``execute`` + if no args given used execute default (esp. as_process = False, stdout_as_string = True) + and return str """ # Handle optional arguments prior to calling transform_kwargs # otherwise these'll end up in args, which is bad. exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} @@ -1010,11 +1104,12 @@ def _call_process(self, method, *args, **kwargs): insert_after_this_arg = opts_kwargs.pop('insert_kwargs_after', None) # Prepare the argument list + opt_args = self.transform_kwargs(**opts_kwargs) ext_args = self.__unpack_args([a for a in args if a is not None]) if insert_after_this_arg is None: - args = opt_args + ext_args + args_list = opt_args + ext_args else: try: index = ext_args.index(insert_after_this_arg) @@ -1022,7 +1117,7 @@ def _call_process(self, method, *args, **kwargs): raise ValueError("Couldn't find argument '%s' in args %s to insert cmd options after" % (insert_after_this_arg, str(ext_args))) from err # end handle error - args = ext_args[:index + 1] + opt_args + ext_args[index + 1:] + args_list = ext_args[:index + 1] + opt_args + ext_args[index + 1:] # end handle opts_kwargs call = [self.GIT_PYTHON_GIT_EXECUTABLE] @@ -1036,11 +1131,11 @@ def _call_process(self, method, *args, **kwargs): self._git_options = () call.append(dashify(method)) - call.extend(args) + call.extend(args_list) return self.execute(call, **exec_kwargs) - def _parse_object_header(self, header_line): + def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: """ :param header_line: type_string size_as_int @@ -1062,12 +1157,11 @@ def _parse_object_header(self, header_line): raise ValueError("Failed to parse header: %r" % header_line) return (tokens[0], tokens[1], int(tokens[2])) - def _prepare_ref(self, ref): + def _prepare_ref(self, ref: AnyStr) -> bytes: # required for command to separate refs on stdin, as bytes - refstr = ref if isinstance(ref, bytes): # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text - refstr = ref.decode('ascii') + refstr = ref.decode('ascii') # type: str elif not isinstance(ref, str): refstr = str(ref) # could be ref-object @@ -1075,7 +1169,8 @@ def _prepare_ref(self, ref): refstr += "\n" return refstr.encode(defenc) - def _get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs): + def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any + ) -> Union['Git.AutoInterrupt', TBD]: cur_val = getattr(self, attr_name) if cur_val is not None: return cur_val @@ -1087,12 +1182,12 @@ def _get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs): setattr(self, attr_name, cmd) return cmd - def __get_object_header(self, cmd, ref): + def __get_object_header(self, cmd, ref: AnyStr) -> Tuple[str, str, int]: cmd.stdin.write(self._prepare_ref(ref)) cmd.stdin.flush() return self._parse_object_header(cmd.stdout.readline()) - def get_object_header(self, ref): + def get_object_header(self, ref: AnyStr) -> Tuple[str, str, int]: """ Use this method to quickly examine the type and size of the object behind the given ref. @@ -1103,7 +1198,7 @@ def get_object_header(self, ref): cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) return self.__get_object_header(cmd, ref) - def get_object_data(self, ref): + def get_object_data(self, ref: AnyStr) -> Tuple[str, str, int, bytes]: """ As get_object_header, but returns object data as well :return: (hexsha, type_string, size_as_int,data_string) :note: not threadsafe""" @@ -1112,7 +1207,7 @@ def get_object_data(self, ref): del(stream) return (hexsha, typename, size, data) - def stream_object_data(self, ref): + def stream_object_data(self, ref: AnyStr) -> Tuple[str, str, int, 'Git.CatFileContentStream']: """ As get_object_header, but returns the data as a stream :return: (hexsha, type_string, size_as_int, stream) @@ -1121,7 +1216,7 @@ def stream_object_data(self, ref): hexsha, typename, size = self.__get_object_header(cmd, ref) return (hexsha, typename, size, self.CatFileContentStream(size, cmd.stdout)) - def clear_cache(self): + def clear_cache(self) -> 'Git': """Clear all kinds of internal caches to release resources. Currently persistent commands will be interrupted. diff --git a/git/compat.py b/git/compat.py index 4ecd19a9a..cbb39fa6f 100644 --- a/git/compat.py +++ b/git/compat.py @@ -44,9 +44,9 @@ def safe_decode(s: None) -> None: ... @overload -def safe_decode(s: Union[IO[str], AnyStr]) -> str: ... +def safe_decode(s: AnyStr) -> str: ... -def safe_decode(s: Union[IO[str], AnyStr, None]) -> Optional[str]: +def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): return s diff --git a/git/diff.py b/git/diff.py index 5a7b189fc..ca673b0ca 100644 --- a/git/diff.py +++ b/git/diff.py @@ -22,6 +22,8 @@ from .objects.tree import Tree from git.repo.base import Repo + from subprocess import Popen + Lit_change_type = Literal['A', 'D', 'M', 'R', 'T'] # ------------------------------------------------------------------------ @@ -490,7 +492,7 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: return index @staticmethod - def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: TBD) -> None: + def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: DiffIndex) -> None: lines = lines_bytes.decode(defenc) for line in lines.split(':')[1:]: @@ -542,14 +544,14 @@ def _handle_diff_line(lines_bytes: bytes, repo: 'Repo', index: TBD) -> None: index.append(diff) @classmethod - def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: + def _index_from_raw_format(cls, repo: 'Repo', proc: 'Popen') -> 'DiffIndex': """Create a new DiffIndex from the given stream which must be in raw format. :return: git.DiffIndex""" # handles # :100644 100644 687099101... 37c5e30c8... M .gitignore index = DiffIndex() - handle_process_output(proc, lambda bytes: cls._handle_diff_line( - bytes, repo, index), None, finalize_process, decode_streams=False) + handle_process_output(proc, lambda byt: cls._handle_diff_line(byt, repo, index), + None, finalize_process, decode_streams=False) return index diff --git a/git/exc.py b/git/exc.py index bcf5aabbd..1e0caf4ed 100644 --- a/git/exc.py +++ b/git/exc.py @@ -11,7 +11,7 @@ # typing ---------------------------------------------------- -from typing import IO, List, Optional, Tuple, Union, TYPE_CHECKING +from typing import List, Optional, Tuple, Union, TYPE_CHECKING from git.types import PathLike if TYPE_CHECKING: @@ -49,8 +49,9 @@ class CommandError(GitError): _msg = "Cmd('%s') failed%s" def __init__(self, command: Union[List[str], Tuple[str, ...], str], - status: Union[str, None, Exception] = None, - stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None) -> None: + status: Union[str, int, None, Exception] = None, + stderr: Union[bytes, str, None] = None, + stdout: Union[bytes, str, None] = None) -> None: if not isinstance(command, (tuple, list)): command = command.split() self.command = command @@ -92,8 +93,8 @@ class GitCommandError(CommandError): def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Union[str, int, None, Exception] = None, - stderr: Optional[IO[str]] = None, - stdout: Optional[IO[str]] = None, + stderr: Union[bytes, str, None] = None, + stdout: Union[bytes, str, None] = None, ) -> None: super(GitCommandError, self).__init__(command, status, stderr, stdout) @@ -139,7 +140,7 @@ class HookExecutionError(CommandError): via standard output""" def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Optional[str], - stderr: Optional[IO[str]] = None, stdout: Optional[IO[str]] = None) -> None: + stderr: Optional[str] = None, stdout: Optional[str] = None) -> None: super(HookExecutionError, self).__init__(command, status, stderr, stdout) self._msg = "Hook('%s') failed%s" diff --git a/git/util.py b/git/util.py index 76ac92f18..d1ea4c104 100644 --- a/git/util.py +++ b/git/util.py @@ -374,18 +374,31 @@ def get_user_id() -> str: return "%s@%s" % (getpass.getuser(), platform.node()) -def finalize_process(proc: TBD, **kwargs: Any) -> None: +def finalize_process(proc: subprocess.Popen, **kwargs: Any) -> None: """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" ## 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: + ... + + +def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[str]: try: - p = osp.expanduser(p) - if expand_vars: - p = osp.expandvars(p) - return osp.normpath(osp.abspath(p)) + if p is not None: + p_out = osp.expanduser(p) + if expand_vars: + p_out = osp.expandvars(p_out) + return osp.normpath(osp.abspath(p_out)) + else: + return None except Exception: return None From 96c43652c9f5b11b611e1aca0a6d67393e9e38c1 Mon Sep 17 00:00:00 2001 From: yobmod Date: Thu, 13 May 2021 01:27:08 +0100 Subject: [PATCH 0058/1849] flake8 and mypy fixes --- git/cmd.py | 40 ++++++++++++++++++++-------------------- git/util.py | 10 ---------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 7b4ebc178..d46ccef31 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -43,7 +43,7 @@ # typing --------------------------------------------------------------------------- from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, List, Mapping, - Sequence, TYPE_CHECKING, Tuple, Union, cast, overload) + Sequence, TYPE_CHECKING, TextIO, Tuple, Union, cast, overload) from git.types import PathLike, Literal, TBD @@ -98,14 +98,17 @@ def handle_process_output(process: subprocess.Popen, or if decoding must happen later (i.e. for Diffs). """ # Use 2 "pump" threads and wait for both to finish. - def pump_stream(cmdline: str, name: str, stream: BinaryIO, is_decode: bool, - handler: Union[None, Callable[[str], None]]) -> None: + def pump_stream(cmdline: str, name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, + handler: Union[None, Callable[[Union[bytes, str]], None]]) -> None: try: for line in stream: if handler: if is_decode: + assert isinstance(line, bytes) line_str = line.decode(defenc) - handler(line_str) + handler(line_str) + else: + handler(line) except Exception as ex: log.error("Pumping %r of cmd(%s) failed due to: %r", name, remove_password_if_present(cmdline), ex) raise CommandError(['<%s-pump>' % name] + remove_password_if_present(cmdline), ex) from ex @@ -337,12 +340,12 @@ def is_cygwin(cls) -> bool: @overload @classmethod - def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: + def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ... @overload @classmethod - def polish_url(cls, url: PathLike, is_cygwin: Union[None, bool] = None) -> PathLike: + def polish_url(cls, url: PathLike, is_cygwin: Union[None, bool] = None) -> str: ... @classmethod @@ -628,8 +631,8 @@ def version_info(self) -> Tuple[int, int, int, int]: def execute(self, command: Union[str, Sequence[Any]], *, - as_process: Literal[True], - ) -> AutoInterrupt: + as_process: Literal[True] + ) -> 'AutoInterrupt': ... @overload @@ -637,7 +640,7 @@ def execute(self, command: Union[str, Sequence[Any]], *, as_process: Literal[False] = False, - stdout_as_string: Literal[True], + stdout_as_string: Literal[True] ) -> Union[str, Tuple[int, str, str]]: ... @@ -646,7 +649,7 @@ def execute(self, command: Union[str, Sequence[Any]], *, as_process: Literal[False] = False, - stdout_as_string: Literal[False] = False, + stdout_as_string: Literal[False] = False ) -> Union[bytes, Tuple[int, bytes, str]]: ... @@ -656,8 +659,7 @@ def execute(self, *, with_extended_output: Literal[False], as_process: Literal[False], - stdout_as_string: Literal[True], - + stdout_as_string: Literal[True] ) -> str: ... @@ -667,8 +669,7 @@ def execute(self, *, with_extended_output: Literal[False], as_process: Literal[False], - stdout_as_string: Literal[False], - + stdout_as_string: Literal[False] ) -> bytes: ... @@ -829,16 +830,13 @@ def execute(self, creationflags=PROC_CREATIONFLAGS, **subprocess_kwargs ) - proc = cast(Popen[bytes], proc) - proc.stdout = cast(BinaryIO, proc.stdout) except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err else: - assert isinstance(proc.stdout, BinaryIO) - assert isinstance(proc.stderr, BinaryIO) - # proc.stdout = cast(BinaryIO, proc.stdout) - # proc.stderr = cast(BinaryIO, proc.stderr) + proc = cast(Popen, proc) + proc.stdout = cast(BinaryIO, proc.stdout) + proc.stderr = cast(BinaryIO, proc.stderr) if as_process: return self.AutoInterrupt(proc, command) @@ -1164,6 +1162,8 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: refstr = ref.decode('ascii') # type: str elif not isinstance(ref, str): refstr = str(ref) # could be ref-object + else: + refstr = ref if not refstr.endswith("\n"): refstr += "\n" diff --git a/git/util.py b/git/util.py index d1ea4c104..300183101 100644 --- a/git/util.py +++ b/git/util.py @@ -281,16 +281,6 @@ def _cygexpath(drive: Optional[str], path: PathLike) -> str: ) # type: Tuple[Tuple[Pattern[str], Callable, bool], ...] -@overload -def cygpath(path: str) -> str: - ... - - -@overload -def cygpath(path: PathLike) -> PathLike: - ... - - def cygpath(path: PathLike) -> PathLike: """Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment.""" path = str(path) # ensure is str and not AnyPath. From fed0cadffd20e48bed8e78fd51a245ad666c54f6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 13 May 2021 10:22:33 +0800 Subject: [PATCH 0059/1849] remove comments --- git/compat.py | 11 +++++++++-- git/util.py | 3 +-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/git/compat.py b/git/compat.py index cbb39fa6f..187618a2a 100644 --- a/git/compat.py +++ b/git/compat.py @@ -43,9 +43,11 @@ @overload def safe_decode(s: None) -> None: ... + @overload def safe_decode(s: AnyStr) -> str: ... + def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: """Safely decodes a binary string to unicode""" if isinstance(s, str): @@ -61,9 +63,11 @@ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: @overload def safe_encode(s: None) -> None: ... + @overload def safe_encode(s: AnyStr) -> bytes: ... + def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Safely encodes a binary string to unicode""" if isinstance(s, str): @@ -79,9 +83,11 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: @overload def win_encode(s: None) -> None: ... + @overload def win_encode(s: AnyStr) -> bytes: ... + def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: """Encode unicodes for process arguments on Windows.""" if isinstance(s, str): @@ -93,7 +99,8 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: return None -def with_metaclass(meta: Type[Any], *bases: Any) -> TBD: # type: ignore ## mypy cannot understand dynamic class creation +# type: ignore ## mypy cannot understand dynamic class creation +def with_metaclass(meta: Type[Any], *bases: Any) -> TBD: """copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15""" class metaclass(meta): # type: ignore @@ -105,4 +112,4 @@ def __new__(cls, name: str, nbases: Optional[Tuple[int, ...]], d: Dict[str, Any] return type.__new__(cls, name, (), d) return meta(name, bases, d) - return metaclass(meta.__name__ + 'Helper', None, {}) # type: ignore + return metaclass(meta.__name__ + 'Helper', None, {}) # type: ignore diff --git a/git/util.py b/git/util.py index 300183101..403e66a64 100644 --- a/git/util.py +++ b/git/util.py @@ -332,9 +332,8 @@ def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool: return False if git_executable is None: - return False # or raise error? + return False - #from subprocess import check_output git_executable = str(git_executable) is_cygwin = _is_cygwin_cache.get(git_executable) # type: Optional[bool] if is_cygwin is None: From b38725361f711ae638c048f93a7b6a12d165bd4e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 13 May 2021 11:37:08 +0800 Subject: [PATCH 0060/1849] Bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b5f785d2d..e6291b96f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.15 +3.1.16 From e76b5379cf55fcd31a2e8696fb97adf8c4df1a8d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 13 May 2021 11:43:15 +0800 Subject: [PATCH 0061/1849] update change log --- doc/source/changes.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 1b916f30f..0c34e385b 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,9 +2,18 @@ Changelog ========= -3.1.15 +3.1.16 ====== +* Fix issues from 3.1.15 (see https://github.com/gitpython-developers/GitPython/issues/1223) +* Add more static typing information + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 + +3.1.15 (YANKED) +================ + * add deprectation warning for python 3.5 See the following for details: From cfa37825b011af682bc12047b82d8cec0121fe4e Mon Sep 17 00:00:00 2001 From: yobmod Date: Thu, 13 May 2021 22:16:54 +0100 Subject: [PATCH 0062/1849] revert util.expand_path() due to regression --- git/util.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/git/util.py b/git/util.py index 300183101..ef8ea8d6c 100644 --- a/git/util.py +++ b/git/util.py @@ -382,13 +382,10 @@ def expand_path(p: PathLike, expand_vars: bool = ...) -> str: def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[str]: try: - if p is not None: - p_out = osp.expanduser(p) - if expand_vars: - p_out = osp.expandvars(p_out) - return osp.normpath(osp.abspath(p_out)) - else: - return None + p = osp.expanduser(p) # type: ignore + if expand_vars: + p = osp.expandvars(p) # type: ignore + return osp.normpath(osp.abspath(p)) # type: ignore except Exception: return None From 33346b25c3a4fb5ea37202d88d6a6c66379099c5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 14 May 2021 10:42:49 +0800 Subject: [PATCH 0063/1849] prepare new patch --- VERSION | 2 +- doc/source/changes.rst | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index e6291b96f..3797f3f9c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.16 +3.1.17 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 0c34e385b..68a94516c 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,17 +2,27 @@ Changelog ========= -3.1.16 +3.1.17 ====== +* Fix issues from 3.1.16 (see https://github.com/gitpython-developers/GitPython/issues/1238) +* Fix issues from 3.1.15 (see https://github.com/gitpython-developers/GitPython/issues/1223) +* Add more static typing information + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/49?closed=1 + +3.1.16 (YANKED) +=============== + * Fix issues from 3.1.15 (see https://github.com/gitpython-developers/GitPython/issues/1223) * Add more static typing information See the following for details: https://github.com/gitpython-developers/gitpython/milestone/48?closed=1 -3.1.15 (YANKED) -================ +3.1.15 (YANKED) +=============== * add deprectation warning for python 3.5 From 96364599258e7e036298dd5737918bde346ec195 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sat, 15 May 2021 22:53:20 +0100 Subject: [PATCH 0064/1849] Add initial types to IndexFile .init() to _to_relative_path() --- git/index/base.py | 122 +++++++++++++++++++++++++++------------------- git/index/fun.py | 11 +++-- git/repo/base.py | 6 +-- 3 files changed, 82 insertions(+), 57 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 5b3667ace..4dcfb30d7 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -63,6 +63,19 @@ git_working_dir ) +# typing ----------------------------------------------------------------------------- + +from typing import Any, Callable, Dict, IO, Iterator, List, Sequence, TYPE_CHECKING, Tuple, Union + +from git.types import PathLike, TBD + +if TYPE_CHECKING: + from subprocess import Popen + from git.repo import Repo + +StageType = int +Treeish = Union[Tree, Commit, bytes] + __all__ = ('IndexFile', 'CheckoutError') @@ -93,7 +106,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable): _VERSION = 2 # latest version we support S_IFGITLINK = S_IFGITLINK # a submodule - def __init__(self, repo, file_path=None): + def __init__(self, repo: 'Repo', file_path: PathLike = None) -> None: """Initialize this Index instance, optionally from the given ``file_path``. If no file_path is given, we will be created from the current index file. @@ -102,9 +115,9 @@ def __init__(self, repo, file_path=None): self.repo = repo self.version = self._VERSION self._extension_data = b'' - self._file_path = file_path or self._index_path() + self._file_path = file_path or self._index_path() # type: PathLike - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr == "entries": # read the current index # try memory map for speed @@ -115,8 +128,8 @@ def _set_cache_(self, attr): ok = True except OSError: # in new repositories, there may be no index, which means we are empty - self.entries = {} - return + self.entries = {} # type: Dict[Tuple[PathLike, StageType], IndexEntry] + return None finally: if not ok: lfd.rollback() @@ -133,15 +146,18 @@ def _set_cache_(self, attr): else: super(IndexFile, self)._set_cache_(attr) - def _index_path(self): - return join_path_native(self.repo.git_dir, "index") + def _index_path(self) -> PathLike: + if self.repo.git_dir: + return join_path_native(self.repo.git_dir, "index") + else: + raise GitCommandError("No git directory given to join index path") @property - def path(self): + def path(self) -> PathLike: """ :return: Path to the index file we are representing """ return self._file_path - def _delete_entries_cache(self): + def _delete_entries_cache(self) -> None: """Safely clear the entries cache so it can be recreated""" try: del(self.entries) @@ -152,18 +168,18 @@ def _delete_entries_cache(self): #{ Serializable Interface - def _deserialize(self, stream): + def _deserialize(self, stream: IO) -> 'IndexFile': """Initialize this instance with index values read from the given stream""" self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream) return self - def _entries_sorted(self): + def _entries_sorted(self) -> List[TBD]: """:return: list of entries, in a sorted fashion, first by path, then by stage""" return sorted(self.entries.values(), key=lambda e: (e.path, e.stage)) - def _serialize(self, stream, ignore_extension_data=False): + def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> 'IndexFile': entries = self._entries_sorted() - extension_data = self._extension_data + extension_data = self._extension_data # type: Union[None, bytes] if ignore_extension_data: extension_data = None write_cache(entries, stream, extension_data) @@ -171,7 +187,7 @@ def _serialize(self, stream, ignore_extension_data=False): #} END serializable interface - def write(self, file_path=None, ignore_extension_data=False): + def write(self, file_path: Union[None, PathLike] = None, ignore_extension_data: bool = False) -> None: """Write the current state to our file path or to the given one :param file_path: @@ -191,7 +207,7 @@ def write(self, file_path=None, ignore_extension_data=False): Alternatively, use IndexFile.write_tree() to handle this case automatically - :return: self""" + :return: self # does it? or returns None?""" # make sure we have our entries read before getting a write lock # else it would be done when streaming. This can happen # if one doesn't change the index, but writes it right away @@ -215,7 +231,7 @@ def write(self, file_path=None, ignore_extension_data=False): @post_clear_cache @default_index - def merge_tree(self, rhs, base=None): + def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> 'IndexFile': """Merge the given rhs treeish into the current index, possibly taking a common base treeish into account. @@ -242,7 +258,7 @@ def merge_tree(self, rhs, base=None): # -i : ignore working tree status # --aggressive : handle more merge cases # -m : do an actual merge - args = ["--aggressive", "-i", "-m"] + args = ["--aggressive", "-i", "-m"] # type: List[Union[Treeish, str]] if base is not None: args.append(base) args.append(rhs) @@ -251,7 +267,7 @@ def merge_tree(self, rhs, base=None): return self @classmethod - def new(cls, repo, *tree_sha): + def new(cls, repo: 'Repo', *tree_sha: bytes) -> 'IndexFile': """ Merge the given treeish revisions into a new index which is returned. This method behaves like git-read-tree --aggressive when doing the merge. @@ -275,7 +291,7 @@ def new(cls, repo, *tree_sha): return inst @classmethod - def from_tree(cls, repo, *treeish, **kwargs): + def from_tree(cls, repo: 'Repo', *treeish: Treeish, **kwargs: Any) -> 'IndexFile': """Merge the given treeish revisions into a new index which is returned. The original index will remain unaltered @@ -312,7 +328,7 @@ def from_tree(cls, repo, *treeish, **kwargs): if len(treeish) == 0 or len(treeish) > 3: raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish)) - arg_list = [] + arg_list = [] # type: List[Union[Treeish, str]] # ignore that working tree and index possibly are out of date if len(treeish) > 1: # drop unmerged entries when reading our index and merging @@ -331,7 +347,8 @@ def from_tree(cls, repo, *treeish, **kwargs): # as it considers existing entries. moving it essentially clears the index. # Unfortunately there is no 'soft' way to do it. # The TemporaryFileSwap assure the original file get put back - index_handler = TemporaryFileSwap(join_path_native(repo.git_dir, 'index')) + if repo.git_dir: + index_handler = TemporaryFileSwap(join_path_native(repo.git_dir, 'index')) try: repo.git.read_tree(*arg_list, **kwargs) index = cls(repo, tmp_index) @@ -346,7 +363,7 @@ def from_tree(cls, repo, *treeish, **kwargs): # UTILITIES @unbare_repo - def _iter_expand_paths(self, paths): + def _iter_expand_paths(self, paths: Sequence[PathLike]) -> Iterator[PathLike]: """Expand the directories in list of paths to the corresponding paths accordingly, Note: git will add items multiple times even if a glob overlapped @@ -354,10 +371,10 @@ def _iter_expand_paths(self, paths): times - we respect that and do not prune""" def raise_exc(e): raise e - r = self.repo.working_tree_dir + r = str(self.repo.working_tree_dir) rs = r + os.sep for path in paths: - abs_path = path + abs_path = str(path) if not osp.isabs(abs_path): abs_path = osp.join(r, path) # END make absolute path @@ -374,7 +391,7 @@ def raise_exc(e): # end check symlink # if the path is not already pointing to an existing file, resolve globs if possible - if not os.path.exists(path) and ('?' in path or '*' in path or '[' in path): + if not os.path.exists(abs_path) and ('?' in abs_path or '*' in abs_path or '[' in abs_path): resolved_paths = glob.glob(abs_path) # not abs_path in resolved_paths: # a glob() resolving to the same path we are feeding it with @@ -396,12 +413,12 @@ def raise_exc(e): # END for each subdirectory except OSError: # was a file or something that could not be iterated - yield path.replace(rs, '') + yield abs_path.replace(rs, '') # END path exception handling # END for each path - def _write_path_to_stdin(self, proc, filepath, item, fmakeexc, fprogress, - read_from_stdout=True): + def _write_path_to_stdin(self, proc: 'Popen', filepath: PathLike, item, fmakeexc, fprogress, + read_from_stdout: bool = True) -> Union[None, str]: """Write path to proc.stdin and make sure it processes the item, including progress. :return: stdout string @@ -417,20 +434,24 @@ def _write_path_to_stdin(self, proc, filepath, item, fmakeexc, fprogress, we will close stdin to break the pipe.""" fprogress(filepath, False, item) - rval = None - try: - proc.stdin.write(("%s\n" % filepath).encode(defenc)) - except IOError as e: - # pipe broke, usually because some error happened - raise fmakeexc() from e - # END write exception handling - proc.stdin.flush() - if read_from_stdout: + rval = None # type: Union[None, str] + + if proc.stdin is not None: + try: + proc.stdin.write(("%s\n" % filepath).encode(defenc)) + except IOError as e: + # pipe broke, usually because some error happened + raise fmakeexc() from e + # END write exception handling + proc.stdin.flush() + + if read_from_stdout and proc.stdout is not None: rval = proc.stdout.readline().strip() fprogress(filepath, True, item) return rval - def iter_blobs(self, predicate=lambda t: True): + def iter_blobs(self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True + ) -> Iterator[Tuple[StageType, Blob]]: """ :return: Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob) @@ -446,12 +467,13 @@ def iter_blobs(self, predicate=lambda t: True): yield output # END for each entry - def unmerged_blobs(self): + def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: """ :return: Iterator yielding dict(path : list( tuple( stage, Blob, ...))), being a dictionary associating a path in the index with a list containing sorted stage/blob pairs + ##### Does it return iterator? or just the Dict? :note: Blobs that have been removed in one side simply do not exist in the @@ -459,7 +481,7 @@ def unmerged_blobs(self): are at stage 3 will not have a stage 3 entry. """ is_unmerged_blob = lambda t: t[0] != 0 - path_map = {} + path_map = {} # type: Dict[PathLike, List[Tuple[TBD, Blob]]] for stage, blob in self.iter_blobs(is_unmerged_blob): path_map.setdefault(blob.path, []).append((stage, blob)) # END for each unmerged blob @@ -468,10 +490,10 @@ def unmerged_blobs(self): return path_map @classmethod - def entry_key(cls, *entry): - return entry_key(*entry) + def entry_key(cls, entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, StageType]]) -> Tuple[PathLike, StageType]: + return entry_key(entry) - def resolve_blobs(self, iter_blobs): + def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> 'IndexFile': """Resolve the blobs given in blob iterator. This will effectively remove the index entries of the respective path at all non-null stages and add the given blob as new stage null blob. @@ -489,9 +511,9 @@ def resolve_blobs(self, iter_blobs): for blob in iter_blobs: stage_null_key = (blob.path, 0) if stage_null_key in self.entries: - raise ValueError("Path %r already exists at stage 0" % blob.path) + raise ValueError("Path %r already exists at stage 0" % str(blob.path)) # END assert blob is not stage 0 already - + # delete all possible stages for stage in (1, 2, 3): try: @@ -506,7 +528,7 @@ def resolve_blobs(self, iter_blobs): return self - def update(self): + def update(self) -> 'IndexFile': """Reread the contents of our index file, discarding all cached information we might have. @@ -517,7 +539,7 @@ def update(self): # allows to lazily reread on demand return self - def write_tree(self): + def write_tree(self) -> Tree: """Writes this index to a corresponding Tree object into the repository's object database and return it. @@ -542,7 +564,7 @@ def write_tree(self): root_tree._cache = tree_items return root_tree - def _process_diff_args(self, args): + def _process_diff_args(self, args: Any) -> List[Any]: try: args.pop(args.index(self)) except IndexError: @@ -550,14 +572,14 @@ def _process_diff_args(self, args): # END remove self return args - def _to_relative_path(self, path): + def _to_relative_path(self, path: PathLike) -> PathLike: """:return: Version of path relative to our git directory or raise ValueError if it is not within our git direcotory""" if not osp.isabs(path): return path if self.repo.bare: raise InvalidGitRepositoryError("require non-bare repository") - if not path.startswith(self.repo.working_tree_dir): + if not str(path).startswith(str(self.repo.working_tree_dir)): raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir)) return os.path.relpath(path, self.repo.working_tree_dir) diff --git a/git/index/fun.py b/git/index/fun.py index e92e8e381..ea7a404b7 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -1,6 +1,7 @@ # Contains standalone functions to accompany the index implementation and make it # more versatile # NOTE: Autodoc hates it if this is a docstring +from git.types import PathLike, TBD from io import BytesIO import os from stat import ( @@ -12,6 +13,7 @@ S_IFREG, ) import subprocess +from typing import List, Tuple, Union, cast from git.cmd import PROC_CREATIONFLAGS, handle_process_output from git.compat import ( @@ -166,11 +168,12 @@ def read_header(stream): return version, num_entries -def entry_key(*entry): +def entry_key(entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, TBD]]): """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" if len(entry) == 1: - return (entry[0].path, entry[0].stage) + entry_first = cast(BaseIndexEntry, entry[0]) # type: BaseIndexEntry + return (entry_first.path, entry_first.stage) return tuple(entry) # END handle entry @@ -283,7 +286,7 @@ def _tree_entry_to_baseindexentry(tree_entry, stage): return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2])) -def aggressive_tree_merge(odb, tree_shas): +def aggressive_tree_merge(odb, tree_shas) -> List[BaseIndexEntry]: """ :return: list of BaseIndexEntries representing the aggressive merge of the given trees. All valid entries are on stage 0, whereas the conflicting ones are left @@ -292,7 +295,7 @@ def aggressive_tree_merge(odb, tree_shas): :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas If 1 or two, the entries will effectively correspond to the last given tree If 3 are given, a 3 way merge is performed""" - out = [] + out = [] # type: List[BaseIndexEntry] out_append = out.append # one and two way is the same for us, as we don't have to handle an existing diff --git a/git/repo/base.py b/git/repo/base.py index ce5f6bd09..607eb8685 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -80,8 +80,8 @@ class Repo(object): git = cast('Git', None) # Must exist, or __del__ will fail in case we raise on `__init__()` working_dir = None # type: Optional[PathLike] _working_tree_dir = None # type: Optional[PathLike] - git_dir = None # type: Optional[PathLike] - _common_dir = None # type: Optional[PathLike] + git_dir = "" # type: PathLike + _common_dir = "" # type: PathLike # precompiled regex re_whitespace = re.compile(r'\s+') @@ -208,7 +208,7 @@ def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = common_dir = open(osp.join(self.git_dir, 'commondir'), 'rt').readlines()[0].strip() self._common_dir = osp.join(self.git_dir, common_dir) except OSError: - self._common_dir = None + self._common_dir = "" # adjust the wd in case we are actually bare - we didn't know that # in the first place From 4dff2004308a7a1e5b9afc7a5b3b9cb515e12514 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sat, 15 May 2021 22:58:47 +0100 Subject: [PATCH 0065/1849] Add initial types to IndexFile .init() to _to_relative_path() --- git/index/base.py | 4 ++-- git/index/fun.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 4dcfb30d7..6a4bacc4a 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -490,8 +490,8 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: return path_map @classmethod - def entry_key(cls, entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, StageType]]) -> Tuple[PathLike, StageType]: - return entry_key(entry) + def entry_key(cls, *entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, StageType]]) -> Tuple[PathLike, StageType]: + return entry_key(*entry) def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> 'IndexFile': """Resolve the blobs given in blob iterator. This will effectively remove the diff --git a/git/index/fun.py b/git/index/fun.py index ea7a404b7..85b85ed5c 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -1,7 +1,7 @@ # Contains standalone functions to accompany the index implementation and make it # more versatile # NOTE: Autodoc hates it if this is a docstring -from git.types import PathLike, TBD +from git.types import PathLike from io import BytesIO import os from stat import ( @@ -168,13 +168,13 @@ def read_header(stream): return version, num_entries -def entry_key(entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, TBD]]): +def entry_key(*entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, int]]): """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" - if len(entry) == 1: + if len(*entry) == 1: entry_first = cast(BaseIndexEntry, entry[0]) # type: BaseIndexEntry return (entry_first.path, entry_first.stage) - return tuple(entry) + return tuple(*entry) # END handle entry From c3f91bd1c0e8aef1b416ae6b1f55e7bd93a4f281 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sat, 15 May 2021 23:12:30 +0100 Subject: [PATCH 0066/1849] Add initial types to IndexFile .init() to _to_relative_path() --- git/index/fun.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index 85b85ed5c..466d323cc 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -13,7 +13,7 @@ S_IFREG, ) import subprocess -from typing import List, Tuple, Union, cast +from typing import List, Tuple, cast from git.cmd import PROC_CREATIONFLAGS, handle_process_output from git.compat import ( @@ -168,13 +168,15 @@ def read_header(stream): return version, num_entries -def entry_key(*entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, int]]): +def entry_key(*entry) -> Tuple[PathLike, int]: """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" - if len(*entry) == 1: + if len(entry) == 1: entry_first = cast(BaseIndexEntry, entry[0]) # type: BaseIndexEntry return (entry_first.path, entry_first.stage) - return tuple(*entry) + else: + entry = cast(Tuple[PathLike, int], tuple(entry)) + return entry # END handle entry From 78d12aa7c922551dddd7168498e29eae32c9d109 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 18:04:30 +0100 Subject: [PATCH 0067/1849] Add remaining types to IndexFile ._preprocess_add_items() to .diff() --- git/diff.py | 4 +-- git/exc.py | 4 +-- git/index/base.py | 91 ++++++++++++++++++++++++++++------------------- git/index/fun.py | 4 +-- git/index/typ.py | 6 ++-- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/git/diff.py b/git/diff.py index ca673b0ca..a40fc244e 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,7 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING -from git.types import TBD, Final, Literal +from git.types import PathLike, TBD, Final, Literal if TYPE_CHECKING: from .objects.tree import Tree @@ -84,7 +84,7 @@ def _process_diff_args(self, args: List[Union[str, 'Diffable', object]]) -> List return args def diff(self, other: Union[Type[Index], Type['Tree'], object, None, str] = Index, - paths: Union[str, List[str], Tuple[str, ...], None] = None, + paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None, create_patch: bool = False, **kwargs: Any) -> 'DiffIndex': """Creates diffs between two items being trees, trees and index or an index and the working tree. It will detect renames automatically. diff --git a/git/exc.py b/git/exc.py index 1e0caf4ed..54a1d51bf 100644 --- a/git/exc.py +++ b/git/exc.py @@ -11,7 +11,7 @@ # typing ---------------------------------------------------- -from typing import List, Optional, Tuple, Union, TYPE_CHECKING +from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING from git.types import PathLike if TYPE_CHECKING: @@ -113,7 +113,7 @@ class CheckoutError(GitError): were checked out successfully and hence match the version stored in the index""" - def __init__(self, message: str, failed_files: List[PathLike], valid_files: List[PathLike], + def __init__(self, message: str, failed_files: Sequence[PathLike], valid_files: List[PathLike], failed_reasons: List[str]) -> None: Exception.__init__(self, message) diff --git a/git/index/base.py b/git/index/base.py index 6a4bacc4a..f2ba71e02 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -3,6 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +from git.refs.reference import Reference import glob from io import BytesIO import os @@ -65,7 +66,8 @@ # typing ----------------------------------------------------------------------------- -from typing import Any, Callable, Dict, IO, Iterator, List, Sequence, TYPE_CHECKING, Tuple, Union +from typing import (Any, Callable, Dict, IO, Iterable, Iterator, List, + Sequence, TYPE_CHECKING, Tuple, Union) from git.types import PathLike, TBD @@ -73,8 +75,9 @@ from subprocess import Popen from git.repo import Repo + StageType = int -Treeish = Union[Tree, Commit, bytes] +Treeish = Union[Tree, Commit, str, bytes] __all__ = ('IndexFile', 'CheckoutError') @@ -490,7 +493,7 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: return path_map @classmethod - def entry_key(cls, *entry: Union[Tuple[BaseIndexEntry], Tuple[PathLike, StageType]]) -> Tuple[PathLike, StageType]: + def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]: return entry_key(*entry) def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> 'IndexFile': @@ -513,7 +516,7 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> 'IndexFile': if stage_null_key in self.entries: raise ValueError("Path %r already exists at stage 0" % str(blob.path)) # END assert blob is not stage 0 already - + # delete all possible stages for stage in (1, 2, 3): try: @@ -650,8 +653,9 @@ def _entries_for_paths(self, paths, path_rewriter, fprogress, entries): # END path handling return entries_added - def add(self, items, force=True, fprogress=lambda *args: None, path_rewriter=None, - write=True, write_extension_data=False): + def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, Submodule]], force: bool = True, + fprogress: Callable = lambda *args: None, path_rewriter: Callable = None, + write: bool = True, write_extension_data: bool = False) -> List[BaseIndexEntry]: """Add files from the working tree, specific blobs or BaseIndexEntries to the index. @@ -838,7 +842,8 @@ def _items_to_rela_paths(self, items): @post_clear_cache @default_index - def remove(self, items, working_tree=False, **kwargs): + def remove(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, Submodule]], working_tree: bool = False, + **kwargs: Any) -> List[str]: """Remove the given items from the index and optionally from the working tree as well. @@ -889,7 +894,8 @@ def remove(self, items, working_tree=False, **kwargs): @post_clear_cache @default_index - def move(self, items, skip_errors=False, **kwargs): + def move(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, Submodule]], skip_errors: bool = False, + **kwargs: Any) -> List[Tuple[str, str]]: """Rename/move the items, whereas the last item is considered the destination of the move operation. If the destination is a file, the first item ( of two ) must be a file as well. If the destination is a directory, it may be preceded @@ -951,9 +957,9 @@ def move(self, items, skip_errors=False, **kwargs): return out - def commit(self, message, parent_commits=None, head=True, author=None, - committer=None, author_date=None, commit_date=None, - skip_hooks=False): + def commit(self, message: str, parent_commits=None, head: bool = True, author: str = None, + committer: str = None, author_date: str = None, commit_date: str = None, + skip_hooks: bool = False) -> Commit: """Commit the current default index file, creating a commit object. For more information on the arguments, see tree.commit. @@ -977,33 +983,39 @@ def commit(self, message, parent_commits=None, head=True, author=None, run_commit_hook('post-commit', self) return rval - def _write_commit_editmsg(self, message): + def _write_commit_editmsg(self, message: str) -> None: with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file: commit_editmsg_file.write(message.encode(defenc)) - def _remove_commit_editmsg(self): + def _remove_commit_editmsg(self) -> None: os.remove(self._commit_editmsg_filepath()) - def _read_commit_editmsg(self): + def _read_commit_editmsg(self) -> str: with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file: return commit_editmsg_file.read().decode(defenc) - def _commit_editmsg_filepath(self): + def _commit_editmsg_filepath(self) -> str: return osp.join(self.repo.common_dir, "COMMIT_EDITMSG") - @classmethod - def _flush_stdin_and_wait(cls, proc, ignore_stdout=False): - proc.stdin.flush() - proc.stdin.close() - stdout = '' - if not ignore_stdout: + def _flush_stdin_and_wait(cls, proc: 'Popen[bytes]', ignore_stdout: bool = False) -> bytes: + stdin_IO = proc.stdin + if stdin_IO: + stdin_IO.flush() + stdin_IO.close() + + stdout = b'' + if not ignore_stdout and proc.stdout: stdout = proc.stdout.read() - proc.stdout.close() - proc.wait() + + if proc.stdout: + proc.stdout.close() + proc.wait() return stdout @default_index - def checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwargs): + def checkout(self, paths: Union[None, Iterable[PathLike]] = None, force: bool = False, + fprogress: Callable = lambda *args: None, **kwargs: Any + ) -> Union[None, Iterator[PathLike], List[PathLike]]: """Checkout the given paths or all files from the version known to the index into the working tree. @@ -1054,12 +1066,15 @@ def checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwar failed_reasons = [] unknown_lines = [] - def handle_stderr(proc, iter_checked_out_files): - stderr = proc.stderr.read() - if not stderr: - return + def handle_stderr(proc: 'Popen[bytes]', iter_checked_out_files: Iterable[PathLike]) -> None: + + stderr_IO = proc.stderr + if not stderr_IO: + return None # return early if stderr empty + else: + stderr_bytes = stderr_IO.read() # line contents: - stderr = stderr.decode(defenc) + stderr = stderr_bytes.decode(defenc) # git-checkout-index: this already exists endings = (' already exists', ' is not in the cache', ' does not exist at stage', ' is unmerged') for line in stderr.splitlines(): @@ -1123,7 +1138,7 @@ def handle_stderr(proc, iter_checked_out_files): proc = self.repo.git.checkout_index(args, **kwargs) # FIXME: Reading from GIL! make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read()) - checked_out_files = [] + checked_out_files = [] # type: List[PathLike] for path in paths: co_path = to_native_path_linux(self._to_relative_path(path)) @@ -1133,11 +1148,11 @@ def handle_stderr(proc, iter_checked_out_files): try: self.entries[(co_path, 0)] except KeyError: - folder = co_path + folder = str(co_path) if not folder.endswith('/'): folder += '/' for entry in self.entries.values(): - if entry.path.startswith(folder): + if str(entry.path).startswith(folder): p = entry.path self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False) @@ -1167,7 +1182,9 @@ def handle_stderr(proc, iter_checked_out_files): assert "Should not reach this point" @default_index - def reset(self, commit='HEAD', working_tree=False, paths=None, head=False, **kwargs): + def reset(self, commit: Union[Commit, Reference, str] = 'HEAD', working_tree: bool = False, + paths: Union[None, Iterable[PathLike]] = None, + head: bool = False, **kwargs: Any) -> 'IndexFile': """Reset the index to reflect the tree at the given commit. This will not adjust our HEAD reference as opposed to HEAD.reset by default. @@ -1235,10 +1252,12 @@ def reset(self, commit='HEAD', working_tree=False, paths=None, head=False, **kwa return self @default_index - def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs): + def diff(self, other: Union[diff.Diffable.Index, 'IndexFile.Index', Treeish, None, object] = diff.Diffable.Index, + paths: Union[str, List[PathLike], Tuple[PathLike, ...]] = None, create_patch: bool = False, **kwargs: Any + ) -> diff.DiffIndex: """Diff this index against the working copy or a Tree or Commit object - For a documentation of the parameters and return values, see + For a documentation of the parameters and return values, see, Diffable.diff :note: @@ -1256,7 +1275,7 @@ def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwar other = self.repo.rev_parse(other) # END object conversion - if isinstance(other, Object): + if isinstance(other, Object): # for Tree or Commit # invert the existing R flag cur_val = kwargs.get('R', False) kwargs['R'] = not cur_val diff --git a/git/index/fun.py b/git/index/fun.py index 466d323cc..cc43f0a46 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -13,7 +13,7 @@ S_IFREG, ) import subprocess -from typing import List, Tuple, cast +from typing import List, Tuple, Union, cast from git.cmd import PROC_CREATIONFLAGS, handle_process_output from git.compat import ( @@ -168,7 +168,7 @@ def read_header(stream): return version, num_entries -def entry_key(*entry) -> Tuple[PathLike, int]: +def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]: """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" if len(entry) == 1: diff --git a/git/index/typ.py b/git/index/typ.py index 2a7dd7990..4d3077771 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -8,6 +8,8 @@ ) from git.objects import Blob +from git.types import PathLike + __all__ = ('BlobFilter', 'BaseIndexEntry', 'IndexEntry') @@ -79,7 +81,7 @@ def hexsha(self): return b2a_hex(self[1]).decode('ascii') @property - def stage(self): + def stage(self) -> int: """Stage of the entry, either: * 0 = default stage @@ -92,7 +94,7 @@ def stage(self): return (self[2] & CE_STAGEMASK) >> CE_STAGESHIFT @property - def path(self): + def path(self) -> PathLike: """:return: our path relative to the repository working tree root""" return self[3] From 11d91e245194cd9a2e44b81b2b3c62514596c578 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 18:10:05 +0100 Subject: [PATCH 0068/1849] Add remaining types to IndexFile ._preprocess_add_items() to .diff() --- git/index/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/index/base.py b/git/index/base.py index f2ba71e02..cf7fafef0 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -586,7 +586,8 @@ def _to_relative_path(self, path: PathLike) -> PathLike: raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir)) return os.path.relpath(path, self.repo.working_tree_dir) - def _preprocess_add_items(self, items): + def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, Submodule]] + ) -> Tuple[List[PathLike], List[BaseIndexEntry]]: """ Split the items into two lists of path strings and BaseEntries. """ paths = [] entries = [] From 7c6c8dcc01b08748c552228e00070b0c94affa94 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 18:22:53 +0100 Subject: [PATCH 0069/1849] Add remaining types to IndexFile ._store_items() ._entries_for_paths() --- git/index/base.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index cf7fafef0..d939e1af5 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -66,7 +66,7 @@ # typing ----------------------------------------------------------------------------- -from typing import (Any, Callable, Dict, IO, Iterable, Iterator, List, +from typing import (Any, BinaryIO, Callable, Dict, IO, Iterable, Iterator, List, Sequence, TYPE_CHECKING, Tuple, Union) from git.types import PathLike, TBD @@ -567,7 +567,8 @@ def write_tree(self) -> Tree: root_tree._cache = tree_items return root_tree - def _process_diff_args(self, args: Any) -> List[Any]: + def _process_diff_args(self, args: List[Union[str, diff.Diffable, object]] + ) -> List[Union[str, diff.Diffable, object]]: try: args.pop(args.index(self)) except IndexError: @@ -607,13 +608,14 @@ def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexE # END for each item return paths, entries - def _store_path(self, filepath, fprogress): + def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry: """Store file at filepath in the database and return the base index entry Needs the git_working_dir decorator active ! This must be assured in the calling code""" st = os.lstat(filepath) # handles non-symlinks as well if S_ISLNK(st.st_mode): # in PY3, readlink is string, but we need bytes. In PY2, it's just OS encoded bytes, we assume UTF-8 - open_stream = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc)) + open_stream = lambda: BytesIO(force_bytes(os.readlink(filepath), + encoding=defenc)) # type: Callable[[], BinaryIO] else: open_stream = lambda: open(filepath, 'rb') with open_stream() as stream: @@ -625,16 +627,18 @@ def _store_path(self, filepath, fprogress): @unbare_repo @git_working_dir - def _entries_for_paths(self, paths, path_rewriter, fprogress, entries): - entries_added = [] + def _entries_for_paths(self, paths: List[str], path_rewriter: Callable, fprogress: Callable, + entries: List[BaseIndexEntry]) -> List[BaseIndexEntry]: + entries_added = [] # type: List[BaseIndexEntry] if path_rewriter: for path in paths: if osp.isabs(path): abspath = path - gitrelative_path = path[len(self.repo.working_tree_dir) + 1:] + gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1:] else: gitrelative_path = path - abspath = osp.join(self.repo.working_tree_dir, gitrelative_path) + if self.repo.working_tree_dir: + abspath = osp.join(self.repo.working_tree_dir, gitrelative_path) # end obtain relative and absolute paths blob = Blob(self.repo, Blob.NULL_BIN_SHA, From 158b3c75d9c621820e3f34b8567acb7898dccce4 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 18:48:32 +0100 Subject: [PATCH 0070/1849] Add types to index.typ.py --- git/index/base.py | 7 +++++-- git/index/typ.py | 51 ++++++++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index d939e1af5..54f736174 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -79,6 +79,8 @@ StageType = int Treeish = Union[Tree, Commit, str, bytes] +# ------------------------------------------------------------------------------------ + __all__ = ('IndexFile', 'CheckoutError') @@ -287,8 +289,9 @@ def new(cls, repo: 'Repo', *tree_sha: bytes) -> 'IndexFile': inst = cls(repo) # convert to entries dict - entries = dict(zip(((e.path, e.stage) for e in base_entries), - (IndexEntry.from_base(e) for e in base_entries))) + entries = dict(zip( + ((e.path, e.stage) for e in base_entries), + (IndexEntry.from_base(e) for e in base_entries))) # type: Dict[Tuple[PathLike, int], IndexEntry] inst.entries = entries return inst diff --git a/git/index/typ.py b/git/index/typ.py index 4d3077771..bb1a03845 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -8,8 +8,17 @@ ) from git.objects import Blob + +# typing ---------------------------------------------------------------------- + +from typing import (List, Sequence, TYPE_CHECKING, Tuple, cast) + from git.types import PathLike +if TYPE_CHECKING: + from git.repo import Repo + +# --------------------------------------------------------------------------------- __all__ = ('BlobFilter', 'BaseIndexEntry', 'IndexEntry') @@ -33,7 +42,7 @@ class BlobFilter(object): """ __slots__ = 'paths' - def __init__(self, paths): + def __init__(self, paths: Sequence[PathLike]) -> None: """ :param paths: tuple or list of paths which are either pointing to directories or @@ -41,7 +50,7 @@ def __init__(self, paths): """ self.paths = paths - def __call__(self, stage_blob): + def __call__(self, stage_blob: Blob) -> bool: path = stage_blob[1].path for p in self.paths: if path.startswith(p): @@ -59,24 +68,24 @@ class BaseIndexEntry(tuple): expecting a BaseIndexEntry can also handle full IndexEntries even if they use numeric indices for performance reasons. """ - def __str__(self): + def __str__(self) -> str: return "%o %s %i\t%s" % (self.mode, self.hexsha, self.stage, self.path) - def __repr__(self): + def __repr__(self) -> str: return "(%o, %s, %i, %s)" % (self.mode, self.hexsha, self.stage, self.path) @property - def mode(self): + def mode(self) -> int: """ File Mode, compatible to stat module constants """ return self[0] @property - def binsha(self): + def binsha(self) -> bytes: """binary sha of the blob """ return self[1] @property - def hexsha(self): + def hexsha(self) -> str: """hex version of our sha""" return b2a_hex(self[1]).decode('ascii') @@ -94,21 +103,21 @@ def stage(self) -> int: return (self[2] & CE_STAGEMASK) >> CE_STAGESHIFT @property - def path(self) -> PathLike: + def path(self) -> str: """:return: our path relative to the repository working tree root""" return self[3] @property - def flags(self): + def flags(self) -> List[str]: """:return: flags stored with this entry""" return self[2] @classmethod - def from_blob(cls, blob, stage=0): + def from_blob(cls, blob: Blob, stage: int = 0) -> 'BaseIndexEntry': """:return: Fully equipped BaseIndexEntry at the given stage""" return cls((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path)) - def to_blob(self, repo): + def to_blob(self, repo: 'Repo') -> Blob: """:return: Blob using the information of this index entry""" return Blob(repo, self.binsha, self.mode, self.path) @@ -122,40 +131,40 @@ class IndexEntry(BaseIndexEntry): See the properties for a mapping between names and tuple indices. """ @property - def ctime(self): + def ctime(self) -> Tuple[int, int]: """ :return: Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the file's creation time""" - return unpack(">LL", self[4]) + return cast(Tuple[int, int], unpack(">LL", self[4])) @property - def mtime(self): + def mtime(self) -> Tuple[int, int]: """See ctime property, but returns modification time """ - return unpack(">LL", self[5]) + return cast(Tuple[int, int], unpack(">LL", self[5])) @property - def dev(self): + def dev(self) -> int: """ Device ID """ return self[6] @property - def inode(self): + def inode(self) -> int: """ Inode ID """ return self[7] @property - def uid(self): + def uid(self) -> int: """ User ID """ return self[8] @property - def gid(self): + def gid(self) -> int: """ Group ID """ return self[9] @property - def size(self): + def size(self) -> int: """:return: Uncompressed size of the blob """ return self[10] @@ -171,7 +180,7 @@ def from_base(cls, base): return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0)) @classmethod - def from_blob(cls, blob, stage=0): + def from_blob(cls, blob: Blob, stage: int = 0) -> 'IndexEntry': """:return: Minimal entry resembling the given blob object""" time = pack(">LL", 0, 0) return IndexEntry((blob.mode, blob.binsha, stage << CE_STAGESHIFT, blob.path, From f58702b0c3a0bb58d49b995a7e5479a7b24933e4 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 18:58:12 +0100 Subject: [PATCH 0071/1849] Add types to index.util.py --- git/index/util.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/git/index/util.py b/git/index/util.py index 02742a5df..ccdc5c1c0 100644 --- a/git/index/util.py +++ b/git/index/util.py @@ -9,6 +9,18 @@ import os.path as osp +# typing ---------------------------------------------------------------------- + +from typing import (Any, Callable, List, Sequence, TYPE_CHECKING, Tuple, cast) + +from git.types import PathLike + +if TYPE_CHECKING: + from git.repo import Repo + +# --------------------------------------------------------------------------------- + + __all__ = ('TemporaryFileSwap', 'post_clear_cache', 'default_index', 'git_working_dir') #{ Aliases @@ -24,16 +36,16 @@ class TemporaryFileSwap(object): and moving it back on to where on object deletion.""" __slots__ = ("file_path", "tmp_file_path") - def __init__(self, file_path): + def __init__(self, file_path: PathLike) -> None: self.file_path = file_path - self.tmp_file_path = self.file_path + tempfile.mktemp('', '', '') + self.tmp_file_path = str(self.file_path) + tempfile.mktemp('', '', '') # it may be that the source does not exist try: os.rename(self.file_path, self.tmp_file_path) except OSError: pass - def __del__(self): + def __del__(self) -> None: if osp.isfile(self.tmp_file_path): if is_win and osp.exists(self.file_path): os.remove(self.file_path) @@ -43,7 +55,7 @@ def __del__(self): #{ Decorators -def post_clear_cache(func): +def post_clear_cache(func: Callable[..., Any]) -> Callable[..., Any]: """Decorator for functions that alter the index using the git command. This would invalidate our possibly existing entries dictionary which is why it must be deleted to allow it to be lazily reread later. @@ -54,7 +66,7 @@ def post_clear_cache(func): """ @wraps(func) - def post_clear_cache_if_not_raised(self, *args, **kwargs): + def post_clear_cache_if_not_raised(self, *args: Any, **kwargs: Any) -> Any: rval = func(self, *args, **kwargs) self._delete_entries_cache() return rval @@ -63,13 +75,13 @@ def post_clear_cache_if_not_raised(self, *args, **kwargs): return post_clear_cache_if_not_raised -def default_index(func): +def default_index(func: Callable[..., Any]) -> Callable[..., Any]: """Decorator assuring the wrapped method may only run if we are the default repository index. This is as we rely on git commands that operate on that index only. """ @wraps(func) - def check_default_index(self, *args, **kwargs): + def check_default_index(self, *args: Any, **kwargs: Any) -> Any: if self._file_path != self._index_path(): raise AssertionError( "Cannot call %r on indices that do not represent the default git index" % func.__name__) @@ -79,12 +91,12 @@ def check_default_index(self, *args, **kwargs): return check_default_index -def git_working_dir(func): +def git_working_dir(func: Callable[..., Any]) -> Callable[..., None]: """Decorator which changes the current working dir to the one of the git repository in order to assure relative paths are handled correctly""" @wraps(func) - def set_git_working_dir(self, *args, **kwargs): + def set_git_working_dir(self, *args: Any, **kwargs: Any) -> None: cur_wd = os.getcwd() os.chdir(self.repo.working_tree_dir) try: From 595181da70978ed44983a6c0ca4cb6d982ba0e8b Mon Sep 17 00:00:00 2001 From: Yobmod Date: Sun, 16 May 2021 21:21:44 +0100 Subject: [PATCH 0072/1849] flake8 and mypy fixes --- git/cmd.py | 8 ++++---- git/db.py | 12 ++++++------ git/index/base.py | 2 +- git/index/util.py | 5 +---- git/refs/log.py | 4 ++-- git/repo/fun.py | 4 ++-- git/util.py | 5 ++++- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index d46ccef31..d8b82352d 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -615,7 +615,7 @@ def _set_cache_(self, attr: str) -> None: # END handle version info @property - def working_dir(self) -> Union[None, str]: + def working_dir(self) -> Union[None, PathLike]: """:return: Git directory we are working on""" return self._working_dir @@ -1187,7 +1187,7 @@ def __get_object_header(self, cmd, ref: AnyStr) -> Tuple[str, str, int]: cmd.stdin.flush() return self._parse_object_header(cmd.stdout.readline()) - def get_object_header(self, ref: AnyStr) -> Tuple[str, str, int]: + def get_object_header(self, ref: str) -> Tuple[str, str, int]: """ Use this method to quickly examine the type and size of the object behind the given ref. @@ -1198,7 +1198,7 @@ def get_object_header(self, ref: AnyStr) -> Tuple[str, str, int]: cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) return self.__get_object_header(cmd, ref) - def get_object_data(self, ref: AnyStr) -> Tuple[str, str, int, bytes]: + def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: """ As get_object_header, but returns object data as well :return: (hexsha, type_string, size_as_int,data_string) :note: not threadsafe""" @@ -1207,7 +1207,7 @@ def get_object_data(self, ref: AnyStr) -> Tuple[str, str, int, bytes]: del(stream) return (hexsha, typename, size, data) - def stream_object_data(self, ref: AnyStr) -> Tuple[str, str, int, 'Git.CatFileContentStream']: + def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileContentStream']: """ As get_object_header, but returns the data as a stream :return: (hexsha, type_string, size_as_int, stream) diff --git a/git/db.py b/git/db.py index dc60c5552..47cccda8d 100644 --- a/git/db.py +++ b/git/db.py @@ -12,7 +12,7 @@ # typing------------------------------------------------- -from typing import TYPE_CHECKING, AnyStr +from typing import TYPE_CHECKING from git.types import PathLike if TYPE_CHECKING: @@ -39,18 +39,18 @@ def __init__(self, root_path: PathLike, git: 'Git') -> None: super(GitCmdObjectDB, self).__init__(root_path) self._git = git - def info(self, sha: bytes) -> OInfo: - hexsha, typename, size = self._git.get_object_header(bin_to_hex(sha)) + def info(self, binsha: bytes) -> OInfo: + hexsha, typename, size = self._git.get_object_header(bin_to_hex(binsha)) return OInfo(hex_to_bin(hexsha), typename, size) - def stream(self, sha: bytes) -> OStream: + def stream(self, binsha: bytes) -> OStream: """For now, all lookup is done by git itself""" - hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(sha)) + hexsha, typename, size, stream = self._git.stream_object_data(bin_to_hex(binsha)) return OStream(hex_to_bin(hexsha), typename, size, stream) # { Interface - def partial_to_complete_sha_hex(self, partial_hexsha: AnyStr) -> bytes: + def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes: """:return: Full binary 20 byte sha from the given partial hexsha :raise AmbiguousObjectName: :raise BadObject: diff --git a/git/index/base.py b/git/index/base.py index 54f736174..2bb62f32e 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -272,7 +272,7 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> 'IndexF return self @classmethod - def new(cls, repo: 'Repo', *tree_sha: bytes) -> 'IndexFile': + def new(cls, repo: 'Repo', *tree_sha: Union[str, Tree]) -> 'IndexFile': """ Merge the given treeish revisions into a new index which is returned. This method behaves like git-read-tree --aggressive when doing the merge. diff --git a/git/index/util.py b/git/index/util.py index ccdc5c1c0..471e9262f 100644 --- a/git/index/util.py +++ b/git/index/util.py @@ -11,13 +11,10 @@ # typing ---------------------------------------------------------------------- -from typing import (Any, Callable, List, Sequence, TYPE_CHECKING, Tuple, cast) +from typing import (Any, Callable) from git.types import PathLike -if TYPE_CHECKING: - from git.repo import Repo - # --------------------------------------------------------------------------------- diff --git a/git/refs/log.py b/git/refs/log.py index fcd2c23cf..363c3c5d5 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -97,8 +97,8 @@ def from_line(cls, line): " Got %s" % repr(line)) # END handle first split - oldhexsha = info[:40] - newhexsha = info[41:81] + oldhexsha = info[:40] # type: str + newhexsha = info[41:81] # type: str for hexsha in (oldhexsha, newhexsha): if not cls._re_hexsha_only.match(hexsha): raise ValueError("Invalid hexsha: %r" % (hexsha,)) diff --git a/git/repo/fun.py b/git/repo/fun.py index 703940819..e96b62e0f 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -18,7 +18,7 @@ # Typing ---------------------------------------------------------------------- -from typing import AnyStr, Union, Optional, cast, TYPE_CHECKING +from typing import Union, Optional, cast, TYPE_CHECKING from git.types import PathLike if TYPE_CHECKING: from .base import Repo @@ -103,7 +103,7 @@ def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: return None -def short_to_long(odb: 'GitCmdObjectDB', hexsha: AnyStr) -> Optional[bytes]: +def short_to_long(odb: 'GitCmdObjectDB', hexsha: str) -> Optional[bytes]: """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha or None if no candidate could be found. :param hexsha: hexsha with less than 40 byte""" diff --git a/git/util.py b/git/util.py index 220901a49..581bf877f 100644 --- a/git/util.py +++ b/git/util.py @@ -24,6 +24,7 @@ from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List, Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING, overload) +import pathlib if TYPE_CHECKING: from git.remote import Remote @@ -379,7 +380,9 @@ def expand_path(p: PathLike, expand_vars: bool = ...) -> str: ... -def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[str]: +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) # type: ignore if expand_vars: From 025fe17da390c410e5bae4d6db0832afbfa26442 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 17 May 2021 13:11:57 +0100 Subject: [PATCH 0073/1849] add types to index.fun.py --- git/exc.py | 11 ++++--- git/index/base.py | 5 +-- git/index/fun.py | 77 ++++++++++++++++++++++++++++------------------- git/repo/base.py | 4 +-- git/util.py | 1 + 5 files changed, 59 insertions(+), 39 deletions(-) diff --git a/git/exc.py b/git/exc.py index 54a1d51bf..e8ff784c7 100644 --- a/git/exc.py +++ b/git/exc.py @@ -11,7 +11,7 @@ # typing ---------------------------------------------------- -from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING +from typing import List, Sequence, Tuple, Union, TYPE_CHECKING from git.types import PathLike if TYPE_CHECKING: @@ -113,7 +113,7 @@ class CheckoutError(GitError): were checked out successfully and hence match the version stored in the index""" - def __init__(self, message: str, failed_files: Sequence[PathLike], valid_files: List[PathLike], + def __init__(self, message: str, failed_files: Sequence[PathLike], valid_files: Sequence[PathLike], failed_reasons: List[str]) -> None: Exception.__init__(self, message) @@ -139,8 +139,11 @@ class HookExecutionError(CommandError): """Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned via standard output""" - def __init__(self, command: Union[List[str], Tuple[str, ...], str], status: Optional[str], - stderr: Optional[str] = None, stdout: Optional[str] = None) -> None: + def __init__(self, command: Union[List[str], Tuple[str, ...], str], + status: Union[str, int, None, Exception], + stderr: Union[bytes, str, None] = None, + stdout: Union[bytes, str, None] = None) -> None: + super(HookExecutionError, self).__init__(command, status, stderr, stdout) self._msg = "Hook('%s') failed%s" diff --git a/git/index/base.py b/git/index/base.py index 2bb62f32e..5c4947ca8 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -285,7 +285,8 @@ def new(cls, repo: 'Repo', *tree_sha: Union[str, Tree]) -> 'IndexFile': New IndexFile instance. Its path will be undefined. If you intend to write such a merged Index, supply an alternate file_path to its 'write' method.""" - base_entries = aggressive_tree_merge(repo.odb, [to_bin_sha(str(t)) for t in tree_sha]) + tree_sha_bytes = [to_bin_sha(str(t)) for t in tree_sha] # List[bytes] + base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) inst = cls(repo) # convert to entries dict @@ -1023,7 +1024,7 @@ def _flush_stdin_and_wait(cls, proc: 'Popen[bytes]', ignore_stdout: bool = False @default_index def checkout(self, paths: Union[None, Iterable[PathLike]] = None, force: bool = False, fprogress: Callable = lambda *args: None, **kwargs: Any - ) -> Union[None, Iterator[PathLike], List[PathLike]]: + ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]: """Checkout the given paths or all files from the version known to the index into the working tree. diff --git a/git/index/fun.py b/git/index/fun.py index cc43f0a46..95dc3d565 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -1,7 +1,7 @@ # Contains standalone functions to accompany the index implementation and make it # more versatile # NOTE: Autodoc hates it if this is a docstring -from git.types import PathLike + from io import BytesIO import os from stat import ( @@ -13,7 +13,6 @@ S_IFREG, ) import subprocess -from typing import List, Tuple, Union, cast from git.cmd import PROC_CREATIONFLAGS, handle_process_output from git.compat import ( @@ -49,6 +48,17 @@ unpack ) +# typing ----------------------------------------------------------------------------- + +from typing import (Dict, IO, List, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast) + +from git.types import PathLike + +if TYPE_CHECKING: + from .base import IndexFile + +# ------------------------------------------------------------------------------------ + S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule CE_NAMEMASK_INV = ~CE_NAMEMASK @@ -57,12 +67,12 @@ 'stat_mode_to_index_mode', 'S_IFGITLINK', 'run_commit_hook', 'hook_path') -def hook_path(name, git_dir): +def hook_path(name: str, git_dir: PathLike) -> str: """:return: path to the given named hook in the given git repository directory""" return osp.join(git_dir, 'hooks', name) -def run_commit_hook(name, index, *args): +def run_commit_hook(name: str, index: IndexFile, *args: str) -> None: """Run the commit hook of the given name. Silently ignores hooks that do not exist. :param name: name of hook, like 'pre-commit' :param index: IndexFile instance @@ -70,10 +80,10 @@ def run_commit_hook(name, index, *args): :raises HookExecutionError: """ hp = hook_path(name, index.repo.git_dir) if not os.access(hp, os.X_OK): - return + return None env = os.environ.copy() - env['GIT_INDEX_FILE'] = safe_decode(index.path) + env['GIT_INDEX_FILE'] = safe_decode(str(index.path)) env['GIT_EDITOR'] = ':' try: cmd = subprocess.Popen([hp] + list(args), @@ -86,14 +96,14 @@ def run_commit_hook(name, index, *args): except Exception as ex: raise HookExecutionError(hp, ex) from ex else: - stdout = [] - stderr = [] - handle_process_output(cmd, stdout.append, stderr.append, finalize_process) - stdout = ''.join(stdout) - stderr = ''.join(stderr) + stdout_list = [] # type: List[str] + stderr_list = [] # type: List[str] + handle_process_output(cmd, stdout_list.append, stderr_list.append, finalize_process) + stdout_str = ''.join(stderr_list) + stderr_str = ''.join(stderr_list) if cmd.returncode != 0: - stdout = force_text(stdout, defenc) - stderr = force_text(stderr, defenc) + stdout = force_text(stdout_str, defenc) + stderr = force_text(stderr_str, defenc) raise HookExecutionError(hp, cmd.returncode, stderr, stdout) # end handle return code @@ -108,7 +118,9 @@ def stat_mode_to_index_mode(mode): return S_IFREG | 0o644 | (mode & 0o111) # blobs with or without executable bit -def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1Writer): +def write_cache(entries: Sequence[Union[BaseIndexEntry, 'IndexEntry']], stream: IO[bytes], + extension_data: Union[None, bytes] = None, + ShaStreamCls: Type[IndexFileSHA1Writer] = IndexFileSHA1Writer) -> None: """Write the cache represented by entries to a stream :param entries: **sorted** list of entries @@ -121,10 +133,10 @@ def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1 :param extension_data: any kind of data to write as a trailer, it must begin a 4 byte identifier, followed by its size ( 4 bytes )""" # wrap the stream into a compatible writer - stream = ShaStreamCls(stream) + stream_sha = ShaStreamCls(stream) - tell = stream.tell - write = stream.write + tell = stream_sha.tell + write = stream_sha.write # header version = 2 @@ -136,8 +148,8 @@ def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1 beginoffset = tell() write(entry[4]) # ctime write(entry[5]) # mtime - path = entry[3] - path = force_bytes(path, encoding=defenc) + path_str = entry[3] # type: str + path = force_bytes(path_str, encoding=defenc) plen = len(path) & CE_NAMEMASK # path length assert plen == len(path), "Path %s too long to fit into index" % entry[3] flags = plen | (entry[2] & CE_NAMEMASK_INV) # clear possible previous values @@ -150,18 +162,19 @@ def write_cache(entries, stream, extension_data=None, ShaStreamCls=IndexFileSHA1 # write previously cached extensions data if extension_data is not None: - stream.write(extension_data) + stream_sha.write(extension_data) # write the sha over the content - stream.write_sha() + stream_sha.write_sha() -def read_header(stream): +def read_header(stream: IO[bytes]) -> Tuple[int, int]: """Return tuple(version_long, num_entries) from the given stream""" type_id = stream.read(4) if type_id != b"DIRC": raise AssertionError("Invalid index file header: %r" % type_id) - version, num_entries = unpack(">LL", stream.read(4 * 2)) + unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2))) + version, num_entries = unpacked # TODO: handle version 3: extended data, see read-cache.c assert version in (1, 2) @@ -180,7 +193,7 @@ def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, i # END handle entry -def read_cache(stream): +def read_cache(stream: IO[bytes]) -> Tuple[int, Dict[Tuple[PathLike, int], 'IndexEntry'], bytes, bytes]: """Read a cache file from the given stream :return: tuple(version, entries_dict, extension_data, content_sha) * version is the integer version number @@ -189,7 +202,7 @@ def read_cache(stream): * content_sha is a 20 byte sha on all cache file contents""" version, num_entries = read_header(stream) count = 0 - entries = {} + entries = {} # type: Dict[Tuple[PathLike, int], 'IndexEntry'] read = stream.read tell = stream.tell @@ -228,7 +241,8 @@ def read_cache(stream): return (version, entries, extension_data, content_sha) -def write_tree_from_cache(entries, odb, sl, si=0): +def write_tree_from_cache(entries: List[IndexEntry], odb, sl: slice, si: int = 0 + ) -> Tuple[bytes, List[Tuple[str, int, str]]]: """Create a tree from the given sorted list of entries and put the respective trees into the given object database @@ -238,7 +252,7 @@ def write_tree_from_cache(entries, odb, sl, si=0): :param sl: slice indicating the range we should process on the entries list :return: tuple(binsha, list(tree_entry, ...)) a tuple of a sha and a list of tree entries being a tuple of hexsha, mode, name""" - tree_items = [] + tree_items = [] # type: List[Tuple[Union[bytes, str], int, str]] tree_items_append = tree_items.append ci = sl.start end = sl.stop @@ -277,18 +291,19 @@ def write_tree_from_cache(entries, odb, sl, si=0): # finally create the tree sio = BytesIO() - tree_to_stream(tree_items, sio.write) + tree_to_stream(tree_items, sio.write) # converts bytes of each item[0] to str + tree_items_stringified = cast(List[Tuple[str, int, str]], tree_items) # type: List[Tuple[str, int, str]] sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) - return (istream.binsha, tree_items) + return (istream.binsha, tree_items_stringified) -def _tree_entry_to_baseindexentry(tree_entry, stage): +def _tree_entry_to_baseindexentry(tree_entry: Tuple[str, int, str], stage: int) -> BaseIndexEntry: return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2])) -def aggressive_tree_merge(odb, tree_shas) -> List[BaseIndexEntry]: +def aggressive_tree_merge(odb, tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]: """ :return: list of BaseIndexEntries representing the aggressive merge of the given trees. All valid entries are on stage 0, whereas the conflicting ones are left diff --git a/git/repo/base.py b/git/repo/base.py index 607eb8685..e23ebb1ac 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -36,7 +36,7 @@ from git.types import TBD, PathLike, Lit_config_levels from typing import (Any, BinaryIO, Callable, Dict, - Iterator, List, Mapping, Optional, + Iterator, List, Mapping, Optional, Sequence, TextIO, Tuple, Type, Union, NamedTuple, cast, TYPE_CHECKING) @@ -536,7 +536,7 @@ def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree': return self.head.commit.tree return self.rev_parse(str(rev) + "^{tree}") - def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, List[PathLike]] = '', + def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, Sequence[PathLike]] = '', **kwargs: Any) -> Iterator[Commit]: """A list of Commit objects representing the history of a given ref/commit diff --git a/git/util.py b/git/util.py index 581bf877f..76aaee497 100644 --- a/git/util.py +++ b/git/util.py @@ -377,6 +377,7 @@ 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 ... From 473fc3a348cd09b4ffca319daff32464d10d8ef9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 17 May 2021 13:15:48 +0100 Subject: [PATCH 0074/1849] forward reference for IndexFile --- git/index/fun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/index/fun.py b/git/index/fun.py index 95dc3d565..d9fe41085 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -72,7 +72,7 @@ def hook_path(name: str, git_dir: PathLike) -> str: return osp.join(git_dir, 'hooks', name) -def run_commit_hook(name: str, index: IndexFile, *args: str) -> None: +def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: """Run the commit hook of the given name. Silently ignores hooks that do not exist. :param name: name of hook, like 'pre-commit' :param index: IndexFile instance From f08d3067310e0251e6d5a33dc5bc65f1b76a2d49 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 17 May 2021 18:06:13 +0100 Subject: [PATCH 0075/1849] forward reference for IndexFile --- git/index/fun.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index d9fe41085..c8a7617ea 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -96,16 +96,17 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: except Exception as ex: raise HookExecutionError(hp, ex) from ex else: - stdout_list = [] # type: List[str] - stderr_list = [] # type: List[str] + stdout_list = [] # type: List[str] + stderr_list = [] # type: List[str] handle_process_output(cmd, stdout_list.append, stderr_list.append, finalize_process) - stdout_str = ''.join(stderr_list) - stderr_str = ''.join(stderr_list) + stdout = ''.join(stdout_list) + stderr = ''.join(stderr_list) if cmd.returncode != 0: - stdout = force_text(stdout_str, defenc) - stderr = force_text(stderr_str, defenc) + stdout = force_text(stdout, defenc) + stderr = force_text(stderr, defenc) raise HookExecutionError(hp, cmd.returncode, stderr, stdout) # end handle return code + # end handle return code def stat_mode_to_index_mode(mode): From 969185b76df038603a90518f35789f28e4cfe5b9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 17 May 2021 18:10:13 +0100 Subject: [PATCH 0076/1849] index.base unmerged_blobs() doc string --- git/index/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 5c4947ca8..044240602 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -477,10 +477,10 @@ def iter_blobs(self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambd def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: """ :return: - Iterator yielding dict(path : list( tuple( stage, Blob, ...))), being + Dict(path : list( tuple( stage, Blob, ...))), being a dictionary associating a path in the index with a list containing sorted stage/blob pairs - ##### Does it return iterator? or just the Dict? + :note: Blobs that have been removed in one side simply do not exist in the From c30bf3ba7548a0e996907b9a097ec322760eb43a Mon Sep 17 00:00:00 2001 From: Yobmod Date: Mon, 17 May 2021 18:13:20 +0100 Subject: [PATCH 0077/1849] Tidy up some comments --- git/index/fun.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/index/fun.py b/git/index/fun.py index c8a7617ea..f40928c33 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -106,7 +106,6 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: stderr = force_text(stderr, defenc) raise HookExecutionError(hp, cmd.returncode, stderr, stdout) # end handle return code - # end handle return code def stat_mode_to_index_mode(mode): From 11837f61aa4b5c286c6ee9870e23a7ee342858c5 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Tue, 18 May 2021 13:11:25 +0100 Subject: [PATCH 0078/1849] Add types to objects.base.py --- git/diff.py | 4 +-- git/index/fun.py | 2 +- git/objects/base.py | 63 +++++++++++++++++++++++++++++++-------------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/git/diff.py b/git/diff.py index a40fc244e..346a2ca7b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,7 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING -from git.types import PathLike, TBD, Final, Literal +from git.types import PathLike, TBD, Literal if TYPE_CHECKING: from .objects.tree import Tree @@ -31,7 +31,7 @@ __all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE') # Special object to compare against the empty tree in diffs -NULL_TREE = object() # type: Final[object] +NULL_TREE = object() _octal_byte_re = re.compile(b'\\\\([0-9]{3})') diff --git a/git/index/fun.py b/git/index/fun.py index f40928c33..96d9b4755 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -108,7 +108,7 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: # end handle return code -def stat_mode_to_index_mode(mode): +def stat_mode_to_index_mode(mode: int) -> int: """Convert the given mode from a stat call to the corresponding index mode and return it""" if S_ISLNK(mode): # symlinks diff --git a/git/objects/base.py b/git/objects/base.py index 59f0e8368..e50387468 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -3,16 +3,34 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php + +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 typing import Optional # noqa: F401 unused import from .util import get_object_type_by_name -_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutal git object type %r" +# typing ------------------------------------------------------------------ + +from typing import Any, TYPE_CHECKING, Optional, Union + +from git.types import PathLike + +if TYPE_CHECKING: + from git.repo import Repo + from gitdb.base import OStream + from .tree import Tree + from .blob import Blob + from .tag import TagObject + from .commit import Commit + +# -------------------------------------------------------------------------- + + +_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutual git object type %r" __all__ = ("Object", "IndexObject") @@ -27,7 +45,7 @@ class Object(LazyMixin): __slots__ = ("repo", "binsha", "size") type = None # type: Optional[str] # to be set by subclass - def __init__(self, repo, binsha): + def __init__(self, repo: 'Repo', binsha: bytes): """Initialize an object by identifying it by its binary sha. All keyword arguments will be set on demand if None. @@ -40,7 +58,7 @@ def __init__(self, repo, binsha): assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (binsha, len(binsha)) @classmethod - def new(cls, repo, id): # @ReservedAssignment + def new(cls, repo: 'Repo', id): # @ReservedAssignment """ :return: New Object instance of a type appropriate to the object type behind id. The id of the newly created object will be a binsha even though @@ -53,7 +71,7 @@ def new(cls, repo, id): # @ReservedAssignment return repo.rev_parse(str(id)) @classmethod - def new_from_sha(cls, repo, sha1): + def new_from_sha(cls, repo: 'Repo', sha1: bytes) -> Union['Commit', 'TagObject', 'Tree', 'Blob']: """ :return: new object instance of a type appropriate to represent the given binary sha1 @@ -67,7 +85,7 @@ def new_from_sha(cls, repo, sha1): inst.size = oinfo.size return inst - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: """Retrieve object information""" if attr == "size": oinfo = self.repo.odb.info(self.binsha) @@ -76,43 +94,43 @@ def _set_cache_(self, attr): else: super(Object, self)._set_cache_(attr) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """:return: True if the objects have the same SHA1""" if not hasattr(other, 'binsha'): return False return self.binsha == other.binsha - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: """:return: True if the objects do not have the same SHA1 """ if not hasattr(other, 'binsha'): return True return self.binsha != other.binsha - def __hash__(self): + def __hash__(self) -> int: """:return: Hash of our id allowing objects to be used in dicts and sets""" return hash(self.binsha) - def __str__(self): + def __str__(self) -> str: """:return: string of our SHA1 as understood by all git commands""" return self.hexsha - def __repr__(self): + def __repr__(self) -> str: """:return: string with pythonic representation of our object""" return '' % (self.__class__.__name__, self.hexsha) @property - def hexsha(self): + def hexsha(self) -> str: """:return: 40 byte hex version of our 20 byte binary sha""" # b2a_hex produces bytes return bin_to_hex(self.binsha).decode('ascii') @property - def data_stream(self): + def data_stream(self) -> 'OStream': """ :return: File Object compatible stream to the uncompressed raw data of the object :note: returned streams must be read in order""" return self.repo.odb.stream(self.binsha) - def stream_data(self, ostream): + def stream_data(self, ostream: 'OStream') -> 'Object': """Writes our data directly to the given output stream :param ostream: File object compatible stream object. :return: self""" @@ -130,7 +148,9 @@ class IndexObject(Object): # for compatibility with iterable lists _id_attribute_ = 'path' - def __init__(self, repo, binsha, mode=None, path=None): + def __init__(self, + repo: 'Repo', binsha: bytes, mode: Union[None, int] = None, path: Union[None, PathLike] = None + ) -> None: """Initialize a newly instanced IndexObject :param repo: is the Repo we are located in @@ -150,14 +170,14 @@ def __init__(self, repo, binsha, mode=None, path=None): if path is not None: self.path = path - def __hash__(self): + def __hash__(self) -> int: """ :return: Hash of our path as index items are uniquely identifiable by path, not by their data !""" return hash(self.path) - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr in IndexObject.__slots__: # they cannot be retrieved lateron ( not without searching for them ) raise AttributeError( @@ -168,16 +188,19 @@ def _set_cache_(self, attr): # END handle slot attribute @property - def name(self): + def name(self) -> str: """:return: Name portion of the path, effectively being the basename""" return osp.basename(self.path) @property - def abspath(self): + def abspath(self) -> PathLike: """ :return: Absolute path to this index object in the file system ( as opposed to the .path field which is a path relative to the git repository ). The returned path will be native to the system and contains '\' on windows. """ - return join_path_native(self.repo.working_tree_dir, self.path) + if self.repo.working_tree_dir is not None: + return join_path_native(self.repo.working_tree_dir, self.path) + else: + raise WorkTreeRepositoryUnsupported From 434306e7d09300b62763b7ebd797d08e7b99ea77 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Tue, 18 May 2021 14:14:11 +0100 Subject: [PATCH 0079/1849] Add types to objects.blob.py --- git/objects/base.py | 2 +- git/objects/blob.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/git/objects/base.py b/git/objects/base.py index e50387468..34c595eee 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -203,4 +203,4 @@ def abspath(self) -> PathLike: if self.repo.working_tree_dir is not None: return join_path_native(self.repo.working_tree_dir, self.path) else: - raise WorkTreeRepositoryUnsupported + raise WorkTreeRepositoryUnsupported("Working_tree_dir was None or empty") diff --git a/git/objects/blob.py b/git/objects/blob.py index 897f892bf..b027adabd 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -4,6 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php from mimetypes import guess_type +from typing import Tuple, Union from . import base __all__ = ('Blob', ) @@ -23,7 +24,7 @@ class Blob(base.IndexObject): __slots__ = () @property - def mime_type(self): + def mime_type(self) -> Union[None, Tuple[Union[None, str], Union[None, str]]]: """ :return: String describing the mime type of this file (based on the filename) :note: Defaults to 'text/plain' in case the actual file type is unknown. """ From ecb12c27b6dc56387594df26a205161a1e75c1b9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Tue, 18 May 2021 14:21:47 +0100 Subject: [PATCH 0080/1849] Add types to objects.tag.py --- git/objects/tag.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/git/objects/tag.py b/git/objects/tag.py index b9bc6c248..84d65d3fb 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -9,6 +9,12 @@ from ..util import hex_to_bin from ..compat import defenc +from typing import List, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from git.repo import Repo + from git.util import Actor + __all__ = ("TagObject", ) @@ -18,8 +24,10 @@ class TagObject(base.Object): type = "tag" __slots__ = ("object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message") - def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment - tagger=None, tagged_date=None, tagger_tz_offset=None, message=None): + def __init__(self, repo: 'Repo', binsha: bytes, object: Union[None, base.Object] = None, + tag: Union[None, str] = None, tagger: Union[None, Actor] = None, tagged_date: Union[int, None] = None, + tagger_tz_offset: Union[int, None] = None, message: Union[str, None] = None + ) -> None: # @ReservedAssignment """Initialize a tag object with additional data :param repo: repository this object is located in @@ -46,11 +54,11 @@ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment if message is not None: self.message = message - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: """Cache all our attributes at once""" if attr in TagObject.__slots__: ostream = self.repo.odb.stream(self.binsha) - lines = ostream.read().decode(defenc, 'replace').splitlines() + lines = ostream.read().decode(defenc, 'replace').splitlines() # type: List[str] _obj, hexsha = lines[0].split(" ") _type_token, type_name = lines[1].split(" ") From 01c8d59e426ae097e486a0bffa5b21d2118a48c3 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Tue, 18 May 2021 14:39:47 +0100 Subject: [PATCH 0081/1849] Add initial types to objects.util.py --- git/objects/blob.py | 2 +- git/objects/util.py | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/git/objects/blob.py b/git/objects/blob.py index b027adabd..a6a5b2241 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -24,7 +24,7 @@ class Blob(base.IndexObject): __slots__ = () @property - def mime_type(self) -> Union[None, Tuple[Union[None, str], Union[None, str]]]: + def mime_type(self) -> Union[None, str, Tuple[Union[None, str], Union[None, str]]]: """ :return: String describing the mime type of this file (based on the filename) :note: Defaults to 'text/plain' in case the actual file type is unknown. """ diff --git a/git/objects/util.py b/git/objects/util.py index d15d83c35..e823d39aa 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -17,6 +17,15 @@ import calendar from datetime import datetime, timedelta, tzinfo + +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from .commit import Commit + from .blob import Blob + from .tag import TagObject + from .tree import Tree + __all__ = ('get_object_type_by_name', 'parse_date', 'parse_actor_and_date', 'ProcessStreamAdapter', 'Traversable', 'altz_to_utctz_str', 'utctz_to_altz', 'verify_utctz', 'Actor', 'tzoffset', 'utc') @@ -26,7 +35,7 @@ #{ Functions -def mode_str_to_int(modestr): +def mode_str_to_int(modestr: str) -> int: """ :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used :return: @@ -41,7 +50,7 @@ def mode_str_to_int(modestr): return mode -def get_object_type_by_name(object_type_name): +def get_object_type_by_name(object_type_name: str) -> Union['Commit', 'TagObject', 'Tree', 'Blob']: """ :return: type suitable to handle the given object type name. Use the type to create new instances. @@ -65,7 +74,7 @@ def get_object_type_by_name(object_type_name): raise ValueError("Cannot handle unknown object type: %s" % object_type_name) -def utctz_to_altz(utctz): +def utctz_to_altz(utctz: str) -> int: """we convert utctz to the timezone in seconds, it is the format time.altzone returns. Git stores it as UTC timezone which has the opposite sign as well, which explains the -1 * ( that was made explicit here ) @@ -73,7 +82,7 @@ def utctz_to_altz(utctz): return -1 * int(float(utctz) / 100 * 3600) -def altz_to_utctz_str(altz): +def altz_to_utctz_str(altz: int) -> str: """As above, but inverses the operation, returning a string that can be used in commit objects""" utci = -1 * int((float(altz) / 3600) * 100) @@ -83,7 +92,7 @@ def altz_to_utctz_str(altz): return prefix + utcs -def verify_utctz(offset): +def verify_utctz(offset: str) -> str: """:raise ValueError: if offset is incorrect :return: offset""" fmt_exc = ValueError("Invalid timezone offset format: %s" % offset) @@ -101,6 +110,7 @@ def verify_utctz(offset): class tzoffset(tzinfo): + def __init__(self, secs_west_of_utc, name=None): self._offset = timedelta(seconds=-secs_west_of_utc) self._name = name or 'fixed' From 9c3255387fe2ce9b156cc06714148436ad2490d9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 13:47:11 +0100 Subject: [PATCH 0082/1849] Add types to objects.util.py tzoffset parse_actor_and_date() --- git/objects/util.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index e823d39aa..ebfb37585 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -17,14 +17,15 @@ import calendar from datetime import datetime, timedelta, tzinfo - -from typing import TYPE_CHECKING, Union +# typing ------------------------------------------------------------ +from typing import Literal, TYPE_CHECKING, Tuple, Union if TYPE_CHECKING: from .commit import Commit from .blob import Blob from .tag import TagObject from .tree import Tree + from subprocess import Popen __all__ = ('get_object_type_by_name', 'parse_date', 'parse_actor_and_date', 'ProcessStreamAdapter', 'Traversable', 'altz_to_utctz_str', 'utctz_to_altz', @@ -111,27 +112,27 @@ def verify_utctz(offset: str) -> str: class tzoffset(tzinfo): - def __init__(self, secs_west_of_utc, name=None): + def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None: self._offset = timedelta(seconds=-secs_west_of_utc) self._name = name or 'fixed' - def __reduce__(self): + def __reduce__(self) -> Tuple['tzoffset', Tuple[float, str]]: return tzoffset, (-self._offset.total_seconds(), self._name) - def utcoffset(self, dt): + def utcoffset(self, dt) -> timedelta: return self._offset - def tzname(self, dt): + def tzname(self, dt) -> str: return self._name - def dst(self, dt): + def dst(self, dt) -> timedelta: return ZERO utc = tzoffset(0, 'UTC') -def from_timestamp(timestamp, tz_offset): +def from_timestamp(timestamp, tz_offset: float) -> datetime: """Converts a timestamp + tz_offset into an aware datetime instance.""" utc_dt = datetime.fromtimestamp(timestamp, utc) try: @@ -141,7 +142,7 @@ def from_timestamp(timestamp, tz_offset): return utc_dt -def parse_date(string_date): +def parse_date(string_date: str) -> Tuple[int, int]: """ Parse the given date as one of the following @@ -228,7 +229,7 @@ def parse_date(string_date): _re_only_actor = re.compile(r'^.+? (.*)$') -def parse_actor_and_date(line): +def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: """Parse out the actor (author or committer) info from a line like:: author Tom Preston-Werner 1191999972 -0700 @@ -257,7 +258,7 @@ class ProcessStreamAdapter(object): it if the instance goes out of scope.""" __slots__ = ("_proc", "_stream") - def __init__(self, process, stream_name): + def __init__(self, process: Popen, stream_name: str): self._proc = process self._stream = getattr(process, stream_name) From f875ddea28b09f2b78496266c80502d5dc2b7411 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 14:27:15 +0100 Subject: [PATCH 0083/1849] Mypy fixes --- git/objects/base.py | 2 +- git/objects/tag.py | 8 ++++++-- git/objects/util.py | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/git/objects/base.py b/git/objects/base.py index 34c595eee..884f96515 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -89,7 +89,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 + self.size = oinfo.size # type: int # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type) else: super(Object, self)._set_cache_(attr) diff --git a/git/objects/tag.py b/git/objects/tag.py index 84d65d3fb..abcc75345 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -14,6 +14,9 @@ if TYPE_CHECKING: from git.repo import Repo from git.util import Actor + from .commit import Commit + from .blob import Blob + from .tree import Tree __all__ = ("TagObject", ) @@ -42,7 +45,7 @@ def __init__(self, repo: 'Repo', binsha: bytes, object: Union[None, base.Object] authored_date is in, in a format similar to time.altzone""" super(TagObject, self).__init__(repo, binsha) if object is not None: - self.object = object + self.object = object # type: Union['Commit', 'Blob', 'Tree', 'TagObject'] if tag is not None: self.tag = tag if tagger is not None: @@ -62,8 +65,9 @@ def _set_cache_(self, attr: str) -> None: _obj, hexsha = lines[0].split(" ") _type_token, type_name = lines[1].split(" ") + object_type = get_object_type_by_name(type_name.encode('ascii')) self.object = \ - get_object_type_by_name(type_name.encode('ascii'))(self.repo, hex_to_bin(hexsha)) + object_type(self.repo, hex_to_bin(hexsha)) self.tag = lines[2][4:] # tag diff --git a/git/objects/util.py b/git/objects/util.py index ebfb37585..6bc1b7093 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import Literal, TYPE_CHECKING, Tuple, Union +from typing import Literal, TYPE_CHECKING, Tuple, Type, Union, cast if TYPE_CHECKING: from .commit import Commit @@ -36,7 +36,7 @@ #{ Functions -def mode_str_to_int(modestr: str) -> int: +def mode_str_to_int(modestr: Union[bytes, str]) -> int: """ :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used :return: @@ -46,12 +46,14 @@ def mode_str_to_int(modestr: str) -> int: for example.""" mode = 0 for iteration, char in enumerate(reversed(modestr[-6:])): + char = cast(Union[str, int], char) mode += int(char) << iteration * 3 # END for each char return mode -def get_object_type_by_name(object_type_name: str) -> Union['Commit', 'TagObject', 'Tree', 'Blob']: +def get_object_type_by_name(object_type_name: bytes + ) -> Union[Type['Commit'], Type['TagObject'], Type['Tree'], Type['Blob']]: """ :return: type suitable to handle the given object type name. Use the type to create new instances. @@ -72,7 +74,7 @@ def get_object_type_by_name(object_type_name: str) -> Union['Commit', 'TagObject from . import tree return tree.Tree else: - raise ValueError("Cannot handle unknown object type: %s" % object_type_name) + raise ValueError("Cannot handle unknown object type: %s" % object_type_name.decode()) def utctz_to_altz(utctz: str) -> int: @@ -116,7 +118,7 @@ def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> No self._offset = timedelta(seconds=-secs_west_of_utc) self._name = name or 'fixed' - def __reduce__(self) -> Tuple['tzoffset', Tuple[float, str]]: + def __reduce__(self) -> Tuple[Type['tzoffset'], Tuple[float, str]]: return tzoffset, (-self._offset.total_seconds(), self._name) def utcoffset(self, dt) -> timedelta: @@ -163,18 +165,18 @@ def parse_date(string_date: str) -> Tuple[int, int]: # git time try: if string_date.count(' ') == 1 and string_date.rfind(':') == -1: - timestamp, offset = string_date.split() + timestamp, offset_str = string_date.split() if timestamp.startswith('@'): timestamp = timestamp[1:] - timestamp = int(timestamp) - return timestamp, utctz_to_altz(verify_utctz(offset)) + timestamp_int = int(timestamp) + return timestamp_int, utctz_to_altz(verify_utctz(offset_str)) else: - offset = "+0000" # local time by default + offset_str = "+0000" # local time by default if string_date[-5] in '-+': - offset = verify_utctz(string_date[-5:]) + offset_str = verify_utctz(string_date[-5:]) string_date = string_date[:-6] # skip space as well # END split timezone info - offset = utctz_to_altz(offset) + offset = utctz_to_altz(offset_str) # now figure out the date and time portion - split time date_formats = [] @@ -235,7 +237,7 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: author Tom Preston-Werner 1191999972 -0700 :return: [Actor, int_seconds_since_epoch, int_timezone_offset]""" - actor, epoch, offset = '', 0, 0 + actor, epoch, offset = '', '0', '0' m = _re_actor_epoch.search(line) if m: actor, epoch, offset = m.groups() From 82b03c2eb07f08dd5d6174a04e4288d41f49920f Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 14:29:11 +0100 Subject: [PATCH 0084/1849] flake8 fixes --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 6bc1b7093..b30733815 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import Literal, TYPE_CHECKING, Tuple, Type, Union, cast +from typing import TYPE_CHECKING, Tuple, Type, Union, cast if TYPE_CHECKING: from .commit import Commit From c5c69071fd6c730d29c31759caddb0ba8b8e92c3 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 14:34:57 +0100 Subject: [PATCH 0085/1849] Mypy fixes --- git/objects/blob.py | 2 +- git/objects/util.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/git/objects/blob.py b/git/objects/blob.py index a6a5b2241..013eaf8c8 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -30,5 +30,5 @@ def mime_type(self) -> Union[None, str, Tuple[Union[None, str], Union[None, str] :note: Defaults to 'text/plain' in case the actual file type is unknown. """ guesses = None if self.path: - guesses = guess_type(self.path) + guesses = guess_type(str(self.path)) return guesses and guesses[0] or self.DEFAULT_MIME_TYPE diff --git a/git/objects/util.py b/git/objects/util.py index b30733815..c123b02a8 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -27,6 +27,8 @@ from .tree import Tree from subprocess import Popen +# -------------------------------------------------------------------- + __all__ = ('get_object_type_by_name', 'parse_date', 'parse_actor_and_date', 'ProcessStreamAdapter', 'Traversable', 'altz_to_utctz_str', 'utctz_to_altz', 'verify_utctz', 'Actor', 'tzoffset', 'utc') From 82b60ab31cfa2ca146069df8dbc21ebfc917db0f Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 14:37:25 +0100 Subject: [PATCH 0086/1849] Change Popen to forwardref --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index c123b02a8..012f9f235 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -262,7 +262,7 @@ class ProcessStreamAdapter(object): it if the instance goes out of scope.""" __slots__ = ("_proc", "_stream") - def __init__(self, process: Popen, stream_name: str): + def __init__(self, process: 'Popen', stream_name: str): self._proc = process self._stream = getattr(process, stream_name) From da88d360d040cfde4c2bdb6c2f38218481b9676b Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 17:29:43 +0100 Subject: [PATCH 0087/1849] Change Actor to forwardref --- git/objects/tag.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/git/objects/tag.py b/git/objects/tag.py index abcc75345..cb6efbe9b 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -27,9 +27,13 @@ class TagObject(base.Object): type = "tag" __slots__ = ("object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message") - def __init__(self, repo: 'Repo', binsha: bytes, object: Union[None, base.Object] = None, - tag: Union[None, str] = None, tagger: Union[None, Actor] = None, tagged_date: Union[int, None] = None, - tagger_tz_offset: Union[int, None] = None, message: Union[str, None] = None + def __init__(self, repo: 'Repo', binsha: bytes, + object: Union[None, base.Object] = None, + tag: Union[None, str] = None, + tagger: Union[None, 'Actor'] = None, + tagged_date: Union[int, None] = None, + tagger_tz_offset: Union[int, None] = None, + message: Union[str, None] = None ) -> None: # @ReservedAssignment """Initialize a tag object with additional data From c242b55d7c64ee43405f8b335c762bcf92189d38 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 17:34:26 +0100 Subject: [PATCH 0088/1849] Add types to objects.util.py ProcessStreamAdapter --- git/objects/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index 012f9f235..fdc1406b7 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import TYPE_CHECKING, Tuple, Type, Union, cast +from typing import Any, IO, TYPE_CHECKING, Tuple, Type, Union, cast if TYPE_CHECKING: from .commit import Commit @@ -262,11 +262,11 @@ class ProcessStreamAdapter(object): it if the instance goes out of scope.""" __slots__ = ("_proc", "_stream") - def __init__(self, process: 'Popen', stream_name: str): + def __init__(self, process: 'Popen', stream_name: str) -> None: self._proc = process - self._stream = getattr(process, stream_name) + self._stream = getattr(process, stream_name) # type: IO[str] ## guess - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: return getattr(self._stream, attr) From 5402a166a4971512f9d513bf36159dead9672ae9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 20:44:53 +0100 Subject: [PATCH 0089/1849] Add types to objects _get_intermediate_items() --- git/objects/commit.py | 12 ++++--- git/objects/submodule/base.py | 6 ++-- git/objects/tree.py | 8 +++-- git/objects/util.py | 64 ++++++++++++++++++++++++++++------- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 45e6d772c..6d3f0bac0 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,6 +4,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +from typing import Tuple, Union from gitdb import IStream from git.util import ( hex_to_bin, @@ -70,7 +71,8 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None, committer=None, committed_date=None, committer_tz_offset=None, - message=None, parents=None, encoding=None, gpgsig=None): + message=None, parents: Union[Tuple['Commit', ...], None] = None, + encoding=None, gpgsig=None): """Instantiate a new Commit. All keyword arguments taking None as default will be implicitly set on first query. @@ -133,7 +135,7 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut self.gpgsig = gpgsig @classmethod - def _get_intermediate_items(cls, commit): + def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: # type: ignore return commit.parents @classmethod @@ -477,7 +479,7 @@ def _deserialize(self, stream): readline = stream.readline self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, '') - self.parents = [] + self.parents_list = [] # List['Commit'] next_line = None while True: parent_line = readline() @@ -485,9 +487,9 @@ def _deserialize(self, stream): next_line = parent_line break # END abort reading parents - self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode('ascii')))) + self.parents_list.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode('ascii')))) # END for each parent line - self.parents = tuple(self.parents) + self.parents = tuple(self.parents_list) # type: Tuple['Commit', ...] # we don't know actual author encoding before we have parsed it, so keep the lines around author_line = next_line diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index e3be1a728..b03fa22a5 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,6 +3,7 @@ import logging import os import stat +from typing import List from unittest import SkipTest import uuid @@ -134,10 +135,11 @@ def _set_cache_(self, attr): super(Submodule, self)._set_cache_(attr) # END handle attribute name - def _get_intermediate_items(self, item): + @classmethod + def _get_intermediate_items(cls, item: 'Submodule') -> List['Submodule']: # type: ignore """:return: all the submodules of our module repository""" try: - return type(self).list_items(item.module()) + return cls.list_items(item.module()) except InvalidGitRepositoryError: return [] # END handle intermediate items diff --git a/git/objects/tree.py b/git/objects/tree.py index 68e98329b..65c9be4c7 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -3,6 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +from typing import Iterable, Iterator, Tuple, Union, cast from git.util import join_path import git.diff as diff from git.util import to_bin_sha @@ -182,8 +183,10 @@ def __init__(self, repo, binsha, mode=tree_id << 12, path=None): super(Tree, self).__init__(repo, binsha, mode, path) @classmethod - def _get_intermediate_items(cls, index_object): + def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore + ) -> Tuple['Tree', ...]: if index_object.type == "tree": + index_object = cast('Tree', index_object) return tuple(index_object._iter_convert_to_object(index_object._cache)) return () @@ -196,7 +199,8 @@ def _set_cache_(self, attr): super(Tree, self)._set_cache_(attr) # END handle attribute - def _iter_convert_to_object(self, iterable): + def _iter_convert_to_object(self, iterable: Iterable[Tuple[bytes, int, str]] + ) -> Iterator[Union[Blob, 'Tree', Submodule]]: """Iterable yields tuples of (binsha, mode, name), which will be converted to the respective object representation""" for binsha, mode, name in iterable: diff --git a/git/objects/util.py b/git/objects/util.py index fdc1406b7..88183567c 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -4,6 +4,8 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php """Module for general utility functions""" + + from git.util import ( IterableList, Actor @@ -18,9 +20,10 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import Any, IO, TYPE_CHECKING, Tuple, Type, Union, cast +from typing import Any, Callable, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload if TYPE_CHECKING: + from .submodule.base import Submodule from .commit import Commit from .blob import Blob from .tag import TagObject @@ -115,7 +118,7 @@ def verify_utctz(offset: str) -> str: class tzoffset(tzinfo): - + def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None: self._offset = timedelta(seconds=-secs_west_of_utc) self._name = name or 'fixed' @@ -275,29 +278,61 @@ class Traversable(object): """Simple interface to perform depth-first or breadth-first traversals into one direction. Subclasses only need to implement one function. - Instances of the Subclass must be hashable""" + Instances of the Subclass must be hashable + + Defined subclasses = [Commit, Tree, SubModule] + """ __slots__ = () + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]: + ... + + @overload @classmethod - def _get_intermediate_items(cls, item): + def _get_intermediate_items(cls, item: 'Submodule') -> Tuple['Submodule', ...]: + ... + + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]: + ... + + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]: + ... + + @classmethod + def _get_intermediate_items(cls, item: 'Traversable' + ) -> Sequence['Traversable']: """ Returns: - List of items connected to the given item. + Tuple of items connected to the given item. Must be implemented in subclass + + class Commit:: (cls, Commit) -> Tuple[Commit, ...] + class Submodule:: (cls, Submodule) -> Iterablelist[Submodule] + class Tree:: (cls, Tree) -> Tuple[Tree, ...] """ raise NotImplementedError("To be implemented in subclass") - def list_traverse(self, *args, **kwargs): + def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList: """ :return: IterableList with the results of the traversal as produced by traverse()""" - out = IterableList(self._id_attribute_) + out = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses out.extend(self.traverse(*args, **kwargs)) return out - def traverse(self, predicate=lambda i, d: True, - prune=lambda i, d: False, depth=-1, branch_first=True, - visit_once=True, ignore_self=1, as_edge=False): + def traverse(self, + predicate: Callable[[object, int], bool] = lambda i, d: True, + prune: Callable[[object, int], bool] = lambda i, d: False, + depth: int = -1, + branch_first: bool = True, + visit_once: bool = True, ignore_self: int = 1, as_edge: bool = False + ) -> Union[Iterator['Traversable'], Iterator[Tuple['Traversable', 'Traversable']]]: """:return: iterator yielding of items found when traversing self :param predicate: f(i,d) returns False if item i at depth d should not be included in the result @@ -329,13 +364,16 @@ def traverse(self, predicate=lambda i, d: True, destination, i.e. tuple(src, dest) with the edge spanning from source to destination""" visited = set() - stack = Deque() + stack = Deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]] stack.append((0, self, None)) # self is always depth level 0 - def addToStack(stack, item, branch_first, depth): + def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None]]], + item: 'Traversable', + branch_first: bool, + depth) -> None: lst = self._get_intermediate_items(item) if not lst: - return + return None if branch_first: stack.extendleft((depth, i, item) for i in lst) else: From 6503ef72d90164840c06f168ab08f0426fb612bf Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 20:55:17 +0100 Subject: [PATCH 0090/1849] Add types to objects.util.py change deque to typing.Deque --- git/objects/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index 88183567c..106bab0e3 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -12,7 +12,7 @@ ) import re -from collections import deque as Deque +from collections import deque from string import digits import time @@ -20,7 +20,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import Any, Callable, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload +from typing import Any, Callable, Deque, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload if TYPE_CHECKING: from .submodule.base import Submodule @@ -364,7 +364,7 @@ def traverse(self, destination, i.e. tuple(src, dest) with the edge spanning from source to destination""" visited = set() - stack = Deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]] + stack = deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]] stack.append((0, self, None)) # self is always depth level 0 def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None]]], From ce8cc4a6123a3ea11fc4e35416d93a8bd68cfd65 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 21:06:09 +0100 Subject: [PATCH 0091/1849] Add types to commit.py spit parents into list and tuple types --- git/objects/commit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/git/objects/commit.py b/git/objects/commit.py index 6d3f0bac0..b95f4c657 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -129,6 +129,7 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut self.message = message if parents is not None: self.parents = parents + self.parents_list = list(parents) if encoding is not None: self.encoding = encoding if gpgsig is not None: From 76bcd7081265f1d72fcc3101bfda62c67d8a7f32 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 21:10:33 +0100 Subject: [PATCH 0092/1849] Add types to commit.py undo --- git/objects/commit.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index b95f4c657..228e897ea 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -129,7 +129,6 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut self.message = message if parents is not None: self.parents = parents - self.parents_list = list(parents) if encoding is not None: self.encoding = encoding if gpgsig is not None: @@ -480,7 +479,7 @@ def _deserialize(self, stream): readline = stream.readline self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, '') - self.parents_list = [] # List['Commit'] + self.parents = [] next_line = None while True: parent_line = readline() @@ -488,9 +487,9 @@ def _deserialize(self, stream): next_line = parent_line break # END abort reading parents - self.parents_list.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode('ascii')))) + self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode('ascii')))) # END for each parent line - self.parents = tuple(self.parents_list) # type: Tuple['Commit', ...] + self.parents = tuple(self.parents) # we don't know actual author encoding before we have parsed it, so keep the lines around author_line = next_line From c51f93823d46f0882b49822ce6f9e668228e5b8d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 21:34:33 +0100 Subject: [PATCH 0093/1849] Add types to objects _serialize() and _deserialize() --- git/objects/commit.py | 20 ++++++++++++-------- git/objects/tree.py | 16 +++++++++++++--- git/objects/util.py | 8 ++++---- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 228e897ea..26db6e36d 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -4,7 +4,6 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -from typing import Tuple, Union from gitdb import IStream from git.util import ( hex_to_bin, @@ -37,6 +36,11 @@ from io import BytesIO import logging +from typing import List, Tuple, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from git.repo import Repo + log = logging.getLogger('git.objects.commit') log.addHandler(logging.NullHandler()) @@ -71,7 +75,7 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None, committer=None, committed_date=None, committer_tz_offset=None, - message=None, parents: Union[Tuple['Commit', ...], None] = None, + message=None, parents: Union[Tuple['Commit', ...], List['Commit'], None] = None, encoding=None, gpgsig=None): """Instantiate a new Commit. All keyword arguments taking None as default will be implicitly set on first query. @@ -135,11 +139,11 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut self.gpgsig = gpgsig @classmethod - def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: # type: ignore - return commit.parents + def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: # type: ignore ## cos overriding super + return tuple(commit.parents) @classmethod - def _calculate_sha_(cls, repo, commit): + def _calculate_sha_(cls, repo: 'Repo', commit: 'Commit') -> bytes: '''Calculate the sha of a commit. :param repo: Repo object the commit should be part of @@ -432,7 +436,7 @@ def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, #{ Serializable Implementation - def _serialize(self, stream): + def _serialize(self, stream: BytesIO) -> 'Commit': write = stream.write write(("tree %s\n" % self.tree).encode('ascii')) for p in self.parents: @@ -473,7 +477,7 @@ def _serialize(self, stream): # END handle encoding return self - def _deserialize(self, stream): + def _deserialize(self, stream: BytesIO) -> 'Commit': """:param from_rev_list: if true, the stream format is coming from the rev-list command Otherwise it is assumed to be a plain data stream from our object""" readline = stream.readline @@ -513,7 +517,7 @@ def _deserialize(self, stream): buf = enc.strip() while buf: if buf[0:10] == b"encoding ": - self.encoding = buf[buf.find(' ') + 1:].decode( + self.encoding = buf[buf.find(b' ') + 1:].decode( self.encoding, 'ignore') elif buf[0:7] == b"gpgsig ": sig = buf[buf.find(b' ') + 1:] + b"\n" diff --git a/git/objects/tree.py b/git/objects/tree.py index 65c9be4c7..29b2a6846 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -3,7 +3,6 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -from typing import Iterable, Iterator, Tuple, Union, cast from git.util import join_path import git.diff as diff from git.util import to_bin_sha @@ -18,6 +17,17 @@ tree_to_stream ) + +# typing ------------------------------------------------- + +from typing import Iterable, Iterator, Tuple, Union, cast, TYPE_CHECKING + +if TYPE_CHECKING: + from io import BytesIO + +#-------------------------------------------------------- + + cmp = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") @@ -321,7 +331,7 @@ def __contains__(self, item): def __reversed__(self): return reversed(self._iter_convert_to_object(self._cache)) - def _serialize(self, stream): + def _serialize(self, stream: 'BytesIO') -> 'Tree': """Serialize this tree into the stream. Please note that we will assume our tree data to be in a sorted state. If this is not the case, serialization will not generate a correct tree representation as these are assumed to be sorted @@ -329,7 +339,7 @@ def _serialize(self, stream): tree_to_stream(self._cache, stream.write) return self - def _deserialize(self, stream): + def _deserialize(self, stream: 'BytesIO') -> 'Tree': self._cache = tree_entries_from_data(stream.read()) return self diff --git a/git/objects/util.py b/git/objects/util.py index 106bab0e3..b94e9f122 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -5,7 +5,6 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php """Module for general utility functions""" - from git.util import ( IterableList, Actor @@ -20,9 +19,10 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import Any, Callable, Deque, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload +from typing import (Any, Callable, Deque, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) if TYPE_CHECKING: + from io import BytesIO from .submodule.base import Submodule from .commit import Commit from .blob import Blob @@ -412,14 +412,14 @@ class Serializable(object): """Defines methods to serialize and deserialize objects from and into a data stream""" __slots__ = () - def _serialize(self, stream): + def _serialize(self, stream: 'BytesIO') -> 'Serializable': """Serialize the data of this object into the given data stream :note: a serialized object would ``_deserialize`` into the same object :param stream: a file-like object :return: self""" raise NotImplementedError("To be implemented in subclass") - def _deserialize(self, stream): + def _deserialize(self, stream: 'BytesIO') -> 'Serializable': """Deserialize all information regarding this object from the stream :param stream: a file-like object :return: self""" From 90f0fb8f449b6d3e4f12c28d8699ee79a6763b80 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 20 May 2021 21:51:56 +0100 Subject: [PATCH 0094/1849] change IO[str] to stringIO --- git/objects/blob.py | 3 +-- git/objects/util.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/git/objects/blob.py b/git/objects/blob.py index 013eaf8c8..017178f05 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -4,7 +4,6 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php from mimetypes import guess_type -from typing import Tuple, Union from . import base __all__ = ('Blob', ) @@ -24,7 +23,7 @@ class Blob(base.IndexObject): __slots__ = () @property - def mime_type(self) -> Union[None, str, Tuple[Union[None, str], Union[None, str]]]: + def mime_type(self) -> str: """ :return: String describing the mime type of this file (based on the filename) :note: Defaults to 'text/plain' in case the actual file type is unknown. """ diff --git a/git/objects/util.py b/git/objects/util.py index b94e9f122..087f0166b 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,10 +19,10 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, IO, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) +from typing import (Any, Callable, Deque, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) if TYPE_CHECKING: - from io import BytesIO + from io import BytesIO, StringIO from .submodule.base import Submodule from .commit import Commit from .blob import Blob @@ -267,7 +267,7 @@ class ProcessStreamAdapter(object): def __init__(self, process: 'Popen', stream_name: str) -> None: self._proc = process - self._stream = getattr(process, stream_name) # type: IO[str] ## guess + self._stream = getattr(process, stream_name) # type: StringIO ## guess def __getattr__(self, attr: str) -> Any: return getattr(self._stream, attr) From 47f35d1ba2b9b75a9078592cf4c41728ac088793 Mon Sep 17 00:00:00 2001 From: mxrch Date: Fri, 21 May 2021 14:17:07 +0200 Subject: [PATCH 0095/1849] fixed case where progress was no longer shown if a single error occured --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index 76aaee497..edbd5f1e7 100644 --- a/git/util.py +++ b/git/util.py @@ -470,7 +470,7 @@ def _parse_progress_line(self, line: AnyStr) -> None: 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 From 1a04c15b1f77f908b1dd3983a27ee49c41b3a3e5 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Mon, 24 May 2021 17:34:42 -0400 Subject: [PATCH 0096/1849] improve index mode for files with executable bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix for #430 in bebc4f56 (Use correct mode for executable files, 2016-05-19) is incomplete. It fails (in most cases) when files have modes which are not exactly 0644 or 0755. Git only cares whether the executable bit is set (or not). Ensure the mode we set for the index is either 100644 or 100755 based on whether the executable bit is set for the file owner. Do this similarly to how upstream git does it in cache.h¹. Add a test covering various file modes to help catch regressions. Fixes #1253 ¹ https://github.com/git/git/blob/v2.31.1/cache.h#L247 --- git/index/fun.py | 3 ++- test/test_fun.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index f40928c33..1012f4801 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -11,6 +11,7 @@ S_ISDIR, S_IFMT, S_IFREG, + S_IXUSR, ) import subprocess @@ -115,7 +116,7 @@ def stat_mode_to_index_mode(mode): return S_IFLNK if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK: # submodules return S_IFGITLINK - return S_IFREG | 0o644 | (mode & 0o111) # blobs with or without executable bit + return S_IFREG | (mode & S_IXUSR and 0o755 or 0o644) # blobs with or without executable bit def write_cache(entries: Sequence[Union[BaseIndexEntry, 'IndexEntry']], stream: IO[bytes], diff --git a/test/test_fun.py b/test/test_fun.py index a7fb8f8bc..e3d07194b 100644 --- a/test/test_fun.py +++ b/test/test_fun.py @@ -1,5 +1,5 @@ from io import BytesIO -from stat import S_IFDIR, S_IFREG, S_IFLNK +from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR from os import stat import os.path as osp from unittest import SkipTest @@ -7,7 +7,8 @@ from git import Git from git.index import IndexFile from git.index.fun import ( - aggressive_tree_merge + aggressive_tree_merge, + stat_mode_to_index_mode, ) from git.objects.fun import ( traverse_tree_recursive, @@ -206,6 +207,16 @@ def assert_entries(entries, num_entries, has_conflict=False): assert_entries(aggressive_tree_merge(odb, trees), 2, True) # END handle ours, theirs + def test_stat_mode_to_index_mode(self): + modes = ( + 0o600, 0o611, 0o640, 0o641, 0o644, 0o650, 0o651, + 0o700, 0o711, 0o740, 0o744, 0o750, 0o751, 0o755, + ) + for mode in modes: + expected_mode = S_IFREG | (mode & S_IXUSR and 0o755 or 0o644) + assert stat_mode_to_index_mode(mode) == expected_mode + # END for each mode + def _assert_tree_entries(self, entries, num_trees): for entry in entries: assert len(entry) == num_trees From 4fbf0ef97d6f59d2eb0f37b29716ba0de95c4457 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 3 Jun 2021 09:11:18 +0800 Subject: [PATCH 0097/1849] Don't raise on unknown line when parsing stale refs (#1262) --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index e17f7bb8c..6ea4b2a1a 100644 --- a/git/remote.py +++ b/git/remote.py @@ -612,7 +612,7 @@ def stale_refs(self) -> IterableList: # * [would prune] origin/new_branch token = " * [would prune] " if not line.startswith(token): - raise ValueError("Could not parse git-remote prune result: %r" % line) + continue ref_name = line.replace(token, "") # sometimes, paths start with a full ref name, like refs/tags/foo, see #260 if ref_name.startswith(Reference._common_path_default + '/'): From 2dbc2be846d1d00e907efbf8171c35b889ab0155 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 09:45:09 +0200 Subject: [PATCH 0098/1849] Adds failing test for repo.tag() method --- test/test_repo.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/test_repo.py b/test/test_repo.py index 8dc178337..9261f1cf7 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -414,6 +414,16 @@ def test_index(self): def test_tag(self): assert self.rorepo.tag('refs/tags/0.1.5').commit + def test_tag_to_full_tag_path(self): + tags = ['0.1.5', 'tags/0.1.5', 'refs/tags/0.1.5'] + value_errors = [] + for tag in tags: + try: + self.rorepo.tag(tag) + except ValueError as valueError: + value_errors.append(valueError.args[0]) + raise ValueError('. '.join(value_errors)) + def test_archive(self): tmpfile = tempfile.mktemp(suffix='archive-test') with open(tmpfile, 'wb') as stream: @@ -445,7 +455,7 @@ def test_should_display_blame_information(self, git): tlist = b[0][1] self.assertTrue(tlist) self.assertTrue(isinstance(tlist[0], str)) - self.assertTrue(len(tlist) < sum(len(t) for t in tlist)) # test for single-char bug + self.assertTrue(len(tlist) < sum(len(t) for t in tlist)) # test for single-char bug # BINARY BLAME git.return_value = fixture('blame_binary') @@ -454,7 +464,7 @@ def test_should_display_blame_information(self, git): def test_blame_real(self): c = 0 - nml = 0 # amount of multi-lines per blame + nml = 0 # amount of multi-lines per blame for item in self.rorepo.head.commit.tree.traverse( predicate=lambda i, d: i.type == 'blob' and i.path.endswith('.py')): c += 1 @@ -486,7 +496,8 @@ def test_blame_incremental(self, git): # Original line numbers orig_ranges = flatten([entry.orig_linenos for entry in blame_output]) - self.assertEqual(orig_ranges, flatten([range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)])) # noqa E501 + self.assertEqual(orig_ranges, flatten( + [range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)])) # noqa E501 @mock.patch.object(Git, '_call_process') def test_blame_complex_revision(self, git): @@ -530,9 +541,9 @@ def test_untracked_files(self, rwrepo): # end for each run def test_config_reader(self): - reader = self.rorepo.config_reader() # all config files + reader = self.rorepo.config_reader() # all config files assert reader.read_only - reader = self.rorepo.config_reader("repository") # single config file + reader = self.rorepo.config_reader("repository") # single config file assert reader.read_only def test_config_writer(self): @@ -729,7 +740,7 @@ def _assert_rev_parse(self, name): def test_rw_rev_parse(self, rwrepo): # verify it does not confuse branches with hexsha ids ahead = rwrepo.create_head('aaaaaaaa') - assert(rwrepo.rev_parse(str(ahead)) == ahead.commit) + assert (rwrepo.rev_parse(str(ahead)) == ahead.commit) def test_rev_parse(self): rev_parse = self.rorepo.rev_parse @@ -1041,7 +1052,7 @@ def test_git_work_tree_env(self, rw_dir): def test_rebasing(self, rw_dir): r = Repo.init(rw_dir) fp = osp.join(rw_dir, 'hello.txt') - r.git.commit("--allow-empty", message="init",) + r.git.commit("--allow-empty", message="init", ) with open(fp, 'w') as fs: fs.write("hello world") r.git.add(Git.polish_url(fp)) From a625d08801eacd94f373074d2c771103823954d0 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 10:12:30 +0200 Subject: [PATCH 0099/1849] Adds _common_default to build _common_path_default --- git/refs/tag.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/refs/tag.py b/git/refs/tag.py index 8f88c5225..4d84239e7 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -18,7 +18,8 @@ class TagReference(Reference): print(tagref.tag.message)""" __slots__ = () - _common_path_default = "refs/tags" + _common_default = "tags" + _common_path_default = Reference._common_path_default + "/" + _common_default @property def commit(self): From 057514e85bc99754e08d45385bf316920963adf9 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 10:18:46 +0200 Subject: [PATCH 0100/1849] Fixes test to not throw false negative results --- test/test_repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_repo.py b/test/test_repo.py index 9261f1cf7..453ec5c31 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -422,7 +422,8 @@ def test_tag_to_full_tag_path(self): self.rorepo.tag(tag) except ValueError as valueError: value_errors.append(valueError.args[0]) - raise ValueError('. '.join(value_errors)) + if value_errors: + raise ValueError('. '.join(value_errors)) def test_archive(self): tmpfile = tempfile.mktemp(suffix='archive-test') From abf9373865c319d2f1aaf188feef900bb8ebf933 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 10:21:50 +0200 Subject: [PATCH 0101/1849] Fixes resolving of tag parameter for repo.tag I accessed private variables instead of adding getters, because other parts of the code do the same and I didn't know if there was a reason for it. E.g.: remote.py line 409: (...) RemoteReference._common_path_default (...) --- git/repo/base.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index e23ebb1ac..540a5fe33 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -402,7 +402,18 @@ def tags(self) -> 'IterableList': def tag(self, path: PathLike) -> TagReference: """:return: TagReference Object, reference pointing to a Commit or Tag :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5 """ - return TagReference(self, path) + full_path = self._to_full_tag_path(path) + return TagReference(self, full_path) + + @staticmethod + def _to_full_tag_path(path: PathLike): + if path.startswith(TagReference._common_path_default + '/'): + return path + if path.startswith(TagReference._common_default + '/'): + return Reference._common_path_default + '/' + path + else: + return TagReference._common_path_default + '/' + path + def create_head(self, path: PathLike, commit: str = 'HEAD', force: bool = False, logmsg: Optional[str] = None From 79e24f78fa35136216130a10d163c91f9a6d4970 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 10:52:59 +0200 Subject: [PATCH 0102/1849] Reverts auto format introduced with 2dbc2be8 --- test/test_repo.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/test_repo.py b/test/test_repo.py index 453ec5c31..0311653a2 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -422,8 +422,7 @@ def test_tag_to_full_tag_path(self): self.rorepo.tag(tag) except ValueError as valueError: value_errors.append(valueError.args[0]) - if value_errors: - raise ValueError('. '.join(value_errors)) + raise ValueError('. '.join(value_errors)) def test_archive(self): tmpfile = tempfile.mktemp(suffix='archive-test') @@ -456,7 +455,7 @@ def test_should_display_blame_information(self, git): tlist = b[0][1] self.assertTrue(tlist) self.assertTrue(isinstance(tlist[0], str)) - self.assertTrue(len(tlist) < sum(len(t) for t in tlist)) # test for single-char bug + self.assertTrue(len(tlist) < sum(len(t) for t in tlist)) # test for single-char bug # BINARY BLAME git.return_value = fixture('blame_binary') @@ -465,7 +464,7 @@ def test_should_display_blame_information(self, git): def test_blame_real(self): c = 0 - nml = 0 # amount of multi-lines per blame + nml = 0 # amount of multi-lines per blame for item in self.rorepo.head.commit.tree.traverse( predicate=lambda i, d: i.type == 'blob' and i.path.endswith('.py')): c += 1 @@ -497,8 +496,7 @@ def test_blame_incremental(self, git): # Original line numbers orig_ranges = flatten([entry.orig_linenos for entry in blame_output]) - self.assertEqual(orig_ranges, flatten( - [range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)])) # noqa E501 + self.assertEqual(orig_ranges, flatten([range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)])) # noqa E501 @mock.patch.object(Git, '_call_process') def test_blame_complex_revision(self, git): @@ -542,9 +540,9 @@ def test_untracked_files(self, rwrepo): # end for each run def test_config_reader(self): - reader = self.rorepo.config_reader() # all config files + reader = self.rorepo.config_reader() # all config files assert reader.read_only - reader = self.rorepo.config_reader("repository") # single config file + reader = self.rorepo.config_reader("repository") # single config file assert reader.read_only def test_config_writer(self): @@ -741,7 +739,7 @@ def _assert_rev_parse(self, name): def test_rw_rev_parse(self, rwrepo): # verify it does not confuse branches with hexsha ids ahead = rwrepo.create_head('aaaaaaaa') - assert (rwrepo.rev_parse(str(ahead)) == ahead.commit) + assert(rwrepo.rev_parse(str(ahead)) == ahead.commit) def test_rev_parse(self): rev_parse = self.rorepo.rev_parse @@ -1053,7 +1051,7 @@ def test_git_work_tree_env(self, rw_dir): def test_rebasing(self, rw_dir): r = Repo.init(rw_dir) fp = osp.join(rw_dir, 'hello.txt') - r.git.commit("--allow-empty", message="init", ) + r.git.commit("--allow-empty", message="init",) with open(fp, 'w') as fs: fs.write("hello world") r.git.add(Git.polish_url(fp)) From 5a61a63ed4bb866b2817acbb04e045f8460e040e Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 11:10:39 +0200 Subject: [PATCH 0103/1849] Adds name to AUTHORS file --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 7b21b2b26..606796d98 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,4 +43,5 @@ Contributors are: -Liam Beguin -Ram Rachum -Alba Mendez +-Robert Westman Portions derived from other open source works and are clearly marked. From 702bdf105205ca845a50b16d6703828d18e93003 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 3 Jun 2021 21:06:30 +0800 Subject: [PATCH 0104/1849] Fix flake8 --- git/repo/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 540a5fe33..53698592b 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -414,7 +414,6 @@ def _to_full_tag_path(path: PathLike): else: return TagReference._common_path_default + '/' + path - def create_head(self, path: PathLike, commit: str = 'HEAD', force: bool = False, logmsg: Optional[str] = None ) -> 'SymbolicReference': From 7ca97dcef3131a11dd5ef41d674bb6bd36608608 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Thu, 3 Jun 2021 16:45:03 +0200 Subject: [PATCH 0105/1849] Removes PathLike type requirement for full_tag creation --- git/repo/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 53698592b..55682411a 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -406,7 +406,7 @@ def tag(self, path: PathLike) -> TagReference: return TagReference(self, full_path) @staticmethod - def _to_full_tag_path(path: PathLike): + def _to_full_tag_path(path): if path.startswith(TagReference._common_path_default + '/'): return path if path.startswith(TagReference._common_default + '/'): From 01a96b92f7d873cbd531d142813c2be7ab88d5a5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 4 Jun 2021 10:35:22 +0800 Subject: [PATCH 0106/1849] Conditionally throw an error --- test/test_repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_repo.py b/test/test_repo.py index 0311653a2..04102b013 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -422,7 +422,7 @@ def test_tag_to_full_tag_path(self): self.rorepo.tag(tag) except ValueError as valueError: value_errors.append(valueError.args[0]) - raise ValueError('. '.join(value_errors)) + self.assertEqual(value_errors, []) def test_archive(self): tmpfile = tempfile.mktemp(suffix='archive-test') From 385a8c6c1a72dc34f69c5273c1b4c1285cc1d3c5 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Sat, 5 Jun 2021 12:15:38 +0200 Subject: [PATCH 0107/1849] Adds repo.is_valid_object check --- git/repo/base.py | 21 ++++++++++++++++++++- test/test_repo.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index 55682411a..e7b1274b1 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -3,12 +3,14 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - +import binascii import logging import os import re import warnings +from gitdb.exc import BadObject + from git.cmd import ( Git, handle_process_output @@ -618,6 +620,23 @@ def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: raise return True + def is_valid_object(self, sha: str, object_type: Union['blob', 'commit', 'tree', 'tag'] = None) -> bool: + try: + complete_sha = self.odb.partial_to_complete_sha_hex(sha) + object_info = self.odb.info(complete_sha) + if object_type: + if object_info.type == object_type.encode(): + return True + else: + log.debug(f"Commit hash points to an object of type '{object_info.type.decode()}'. " + f"Requested were objects of type '{object_type}'") + return False + else: + return True + except BadObject as e: + log.debug("Commit hash is invalid.") + return False + def _get_daemon_export(self) -> bool: if self.git_dir: filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) diff --git a/test/test_repo.py b/test/test_repo.py index 04102b013..8aced94d4 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -989,6 +989,34 @@ def test_is_ancestor(self): for i, j in itertools.permutations([c1, 'ffffff', ''], r=2): self.assertRaises(GitCommandError, repo.is_ancestor, i, j) + def test_is_valid_object(self): + repo = self.rorepo + commit_sha = 'f6aa8d1' + blob_sha = '1fbe3e4375' + tree_sha = '960b40fe36' + tag_sha = '42c2f60c43' + + # Check for valid objects + self.assertTrue(repo.is_valid_object(commit_sha)) + self.assertTrue(repo.is_valid_object(blob_sha)) + self.assertTrue(repo.is_valid_object(tree_sha)) + self.assertTrue(repo.is_valid_object(tag_sha)) + + # Check for valid objects of specific type + self.assertTrue(repo.is_valid_object(commit_sha, 'commit')) + self.assertTrue(repo.is_valid_object(blob_sha, 'blob')) + self.assertTrue(repo.is_valid_object(tree_sha, 'tree')) + self.assertTrue(repo.is_valid_object(tag_sha, 'tag')) + + # Check for invalid objects + self.assertFalse(repo.is_valid_object(b'1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a', 'blob')) + + # Check for invalid objects of specific type + self.assertFalse(repo.is_valid_object(commit_sha, 'blob')) + self.assertFalse(repo.is_valid_object(blob_sha, 'commit')) + self.assertFalse(repo.is_valid_object(tree_sha, 'commit')) + self.assertFalse(repo.is_valid_object(tag_sha, 'commit')) + @with_rw_directory def test_git_work_tree_dotgit(self, rw_dir): """Check that we find .git as a worktree file and find the worktree From ac4fe6efbccc2ad5c2044bf36e34019363018630 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Sat, 5 Jun 2021 12:22:24 +0200 Subject: [PATCH 0108/1849] Fixes type check for is_valid_object --- git/repo/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index e7b1274b1..b19503ee2 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -620,7 +620,7 @@ def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: raise return True - def is_valid_object(self, sha: str, object_type: Union['blob', 'commit', 'tree', 'tag'] = None) -> bool: + def is_valid_object(self, sha: str, object_type: str = None) -> bool: try: complete_sha = self.odb.partial_to_complete_sha_hex(sha) object_info = self.odb.info(complete_sha) From 4832aa6bf82e4853f8f426fc06350540e2c8a9e7 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Sat, 5 Jun 2021 12:34:24 +0200 Subject: [PATCH 0109/1849] Removes local variable 'e' that is assigned to but never used --- git/repo/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index b19503ee2..d38cf756d 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -633,7 +633,7 @@ def is_valid_object(self, sha: str, object_type: str = None) -> bool: return False else: return True - except BadObject as e: + except BadObject: log.debug("Commit hash is invalid.") return False From fb2461d84f97a72641ef1e878450aeab7cd17241 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Sat, 5 Jun 2021 12:35:41 +0200 Subject: [PATCH 0110/1849] Removes unused import --- git/repo/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/git/repo/base.py b/git/repo/base.py index d38cf756d..0db0bd0cd 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -3,7 +3,6 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -import binascii import logging import os import re From 464504ce0069758fdb88b348e4a626a265fb3fe3 Mon Sep 17 00:00:00 2001 From: Robert Westman Date: Sat, 5 Jun 2021 12:39:44 +0200 Subject: [PATCH 0111/1849] Removes f-string syntax for p35 compatibility --- git/repo/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/repo/base.py b/git/repo/base.py index 0db0bd0cd..6cc560310 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -627,8 +627,8 @@ def is_valid_object(self, sha: str, object_type: str = None) -> bool: if object_info.type == object_type.encode(): return True else: - log.debug(f"Commit hash points to an object of type '{object_info.type.decode()}'. " - f"Requested were objects of type '{object_type}'") + log.debug("Commit hash points to an object of type '%s'. Requested were objects of type '%s'", + object_info.type.decode(), object_type) return False else: return True From 4d86d883714072b6e3bbc56a2127c06e9d6a6582 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 16 Jun 2021 11:03:13 +0800 Subject: [PATCH 0112/1849] prepare patch level 3.1.18 --- VERSION | 2 +- doc/source/changes.rst | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3797f3f9c..5762a6ffe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.17 +3.1.18 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 68a94516c..aabef8023 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ========= +3.1.18 +====== + +* drop support for python 3.5 to reduce maintenance burden on typing. Lower patch levels of python 3.5 would break, too. + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/50?closed=1 + 3.1.17 ====== From 820d3cc9ceda3e5690d627677883b7f9d349b326 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 16 Jun 2021 11:22:27 +0800 Subject: [PATCH 0113/1849] Revert "Remove support for Python 3.5" - fix CI for now. This reverts commit 45d1cd59d39227ee6841042eab85116a59a26d22. See #1201 which will hopefully help to get a proper fix soon. --- .appveyor.yml | 29 +++++++++++++++++++++++++++-- .github/workflows/pythonpackage.yml | 4 ++-- .travis.yml | 4 +++- README.md | 2 +- doc/source/intro.rst | 2 +- git/cmd.py | 9 +++++++++ setup.py | 4 ++-- 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 833f5c7b9..0a86c1a75 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,12 +6,29 @@ environment: CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%" matrix: + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4" + GIT_PATH: "%GIT_DAEMON_PATH%" + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5" + GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" GIT_PATH: "%GIT_DAEMON_PATH%" + - PYTHON: "C:\\Miniconda35-x64" + PYTHON_VERSION: "3.5" + IS_CONDA: "yes" + MAYFAIL: "yes" + GIT_PATH: "%GIT_DAEMON_PATH%" + ## Cygwin + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5" + IS_CYGWIN: "yes" + MAYFAIL: "yes" + GIT_PATH: "%CYGWIN64_GIT_PATH%" matrix: allow_failures: @@ -59,10 +76,18 @@ install: build: false test_script: - - nosetests -v + - IF "%IS_CYGWIN%" == "yes" ( + nosetests -v + ) ELSE ( + IF "%PYTHON_VERSION%" == "3.5" ( + nosetests -v --with-coverage + ) ELSE ( + nosetests -v + ) + ) on_success: - - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov) + - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov) # Enable this to be able to login to the build worker. You can use the # `remmina` program in Ubuntu, use the login information that the line below diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 53da76149..65d5e6cd4 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -61,4 +61,4 @@ jobs: run: | set -x pip install -r doc/requirements.txt - make -C doc html + make -C doc html \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 570beaad6..1fbb1ddb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ # UNUSED, only for reference. If adjustments are needed, please see github actions language: python python: + - "3.4" + - "3.5" - "3.6" - "3.7" - "3.8" @@ -36,7 +38,7 @@ script: - ulimit -n - coverage run --omit="test/*" -m unittest --buffer - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi + - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi after_success: - codecov diff --git a/README.md b/README.md index 4725d3aeb..0d0edeb43 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. * Git (1.7.x or newer) -* Python >= 3.6 +* Python >= 3.5 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 956a36073..7168c91b1 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.6 +* `Python`_ >= 3.5 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/git/cmd.py b/git/cmd.py index 4f58b3146..d8b82352d 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,7 +17,9 @@ import subprocess import sys import threading +from collections import OrderedDict from textwrap import dedent +import warnings from git.compat import ( defenc, @@ -1003,6 +1005,13 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: """Transforms Python style kwargs into git command line options.""" + # Python 3.6 preserves the order of kwargs and thus has a stable + # order. For older versions sort the kwargs by the key to get a stable + # order. + if sys.version_info[:2] < (3, 6): + kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) + warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" + + "It does not preserve the order for key-word arguments and enforce lexical sorting instead.") args = [] for k, v in kwargs.items(): if isinstance(v, (list, tuple)): diff --git a/setup.py b/setup.py index 850d680d4..f8829c386 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): include_package_data=True, py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={'git': 'git'}, - python_requires='>=3.6', + python_requires='>=3.5', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -127,6 +127,6 @@ def build_py_modules(basedir, excludes=[]): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9" ] ) From b0f79c58ad919e90261d1e332df79a4ad0bc40de Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 16 Jun 2021 11:27:07 +0800 Subject: [PATCH 0114/1849] Revert "Revert "Remove support for Python 3.5" - fix CI for now." This reverts commit 820d3cc9ceda3e5690d627677883b7f9d349b326. --- .appveyor.yml | 29 ++--------------------------- .github/workflows/pythonpackage.yml | 4 ++-- .travis.yml | 4 +--- README.md | 2 +- doc/source/intro.rst | 2 +- git/cmd.py | 9 --------- setup.py | 4 ++-- 7 files changed, 9 insertions(+), 45 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0a86c1a75..833f5c7b9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,29 +6,12 @@ environment: CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%" matrix: - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Miniconda35-x64" - PYTHON_VERSION: "3.5" - IS_CONDA: "yes" - MAYFAIL: "yes" - GIT_PATH: "%GIT_DAEMON_PATH%" - ## Cygwin - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - IS_CYGWIN: "yes" - MAYFAIL: "yes" - GIT_PATH: "%CYGWIN64_GIT_PATH%" matrix: allow_failures: @@ -76,18 +59,10 @@ install: build: false test_script: - - IF "%IS_CYGWIN%" == "yes" ( - nosetests -v - ) ELSE ( - IF "%PYTHON_VERSION%" == "3.5" ( - nosetests -v --with-coverage - ) ELSE ( - nosetests -v - ) - ) + - nosetests -v on_success: - - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov) + - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov) # Enable this to be able to login to the build worker. You can use the # `remmina` program in Ubuntu, use the login information that the line below diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 65d5e6cd4..53da76149 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -61,4 +61,4 @@ jobs: run: | set -x pip install -r doc/requirements.txt - make -C doc html \ No newline at end of file + make -C doc html diff --git a/.travis.yml b/.travis.yml index 1fbb1ddb8..570beaad6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ # UNUSED, only for reference. If adjustments are needed, please see github actions language: python python: - - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" @@ -38,7 +36,7 @@ script: - ulimit -n - coverage run --omit="test/*" -m unittest --buffer - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi + - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi after_success: - codecov diff --git a/README.md b/README.md index 0d0edeb43..4725d3aeb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. * Git (1.7.x or newer) -* Python >= 3.5 +* Python >= 3.6 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 7168c91b1..956a36073 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.5 +* `Python`_ >= 3.6 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/git/cmd.py b/git/cmd.py index d8b82352d..4f58b3146 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,9 +17,7 @@ import subprocess import sys import threading -from collections import OrderedDict from textwrap import dedent -import warnings from git.compat import ( defenc, @@ -1005,13 +1003,6 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: """Transforms Python style kwargs into git command line options.""" - # Python 3.6 preserves the order of kwargs and thus has a stable - # order. For older versions sort the kwargs by the key to get a stable - # order. - if sys.version_info[:2] < (3, 6): - kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) - warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" + - "It does not preserve the order for key-word arguments and enforce lexical sorting instead.") args = [] for k, v in kwargs.items(): if isinstance(v, (list, tuple)): diff --git a/setup.py b/setup.py index f8829c386..850d680d4 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): include_package_data=True, py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={'git': 'git'}, - python_requires='>=3.5', + python_requires='>=3.6', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -127,6 +127,6 @@ def build_py_modules(basedir, excludes=[]): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9" ] ) From 567c892322776756e8d0095e89f39b25b9b01bc2 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 17 Jun 2021 17:22:31 +0100 Subject: [PATCH 0115/1849] rebase with dropped 3.5 --- .appveyor.yml | 29 ++--------------------------- .github/workflows/pythonpackage.yml | 2 +- .travis.yml | 3 +-- README.md | 2 +- doc/source/intro.rst | 2 +- git/cmd.py | 3 +-- git/repo/base.py | 2 +- setup.py | 3 +-- 8 files changed, 9 insertions(+), 37 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0a86c1a75..833f5c7b9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,29 +6,12 @@ environment: CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%" matrix: - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Miniconda35-x64" - PYTHON_VERSION: "3.5" - IS_CONDA: "yes" - MAYFAIL: "yes" - GIT_PATH: "%GIT_DAEMON_PATH%" - ## Cygwin - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - IS_CYGWIN: "yes" - MAYFAIL: "yes" - GIT_PATH: "%CYGWIN64_GIT_PATH%" matrix: allow_failures: @@ -76,18 +59,10 @@ install: build: false test_script: - - IF "%IS_CYGWIN%" == "yes" ( - nosetests -v - ) ELSE ( - IF "%PYTHON_VERSION%" == "3.5" ( - nosetests -v --with-coverage - ) ELSE ( - nosetests -v - ) - ) + - nosetests -v on_success: - - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov) + - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov) # Enable this to be able to login to the build worker. You can use the # `remmina` program in Ubuntu, use the login information that the line below diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3c7215cbe..53da76149 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/.travis.yml b/.travis.yml index 1fbb1ddb8..8a171b4fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" @@ -38,7 +37,7 @@ script: - ulimit -n - coverage run --omit="test/*" -m unittest --buffer - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi + - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi after_success: - codecov diff --git a/README.md b/README.md index 0d0edeb43..4725d3aeb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. * Git (1.7.x or newer) -* Python >= 3.5 +* Python >= 3.6 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 7168c91b1..956a36073 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.5 +* `Python`_ >= 3.6 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/git/cmd.py b/git/cmd.py index d8b82352d..d15b97ca5 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -150,7 +150,6 @@ def dashify(string: str) -> str: def slots_to_dict(self, exclude: Sequence[str] = ()) -> Dict[str, Any]: - # annotate self.__slots__ as Tuple[str, ...] once 3.5 dropped return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} @@ -462,7 +461,7 @@ class CatFileContentStream(object): If not all data is read to the end of the objects's lifetime, we read the rest to assure the underlying stream continues to work""" - __slots__ = ('_stream', '_nbr', '_size') + __slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size') def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream diff --git a/git/repo/base.py b/git/repo/base.py index e23ebb1ac..2d2e915cf 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -519,7 +519,7 @@ def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator['Tree']: :note: Takes all arguments known to iter_commits method""" return (c.tree for c in self.iter_commits(*args, **kwargs)) - def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree': + def tree(self, rev: Union['Commit', 'Tree', str, None] = None) -> 'Tree': """The Tree object for the given treeish revision Examples:: diff --git a/setup.py b/setup.py index f8829c386..3fbcbbad1 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): include_package_data=True, py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={'git': 'git'}, - python_requires='>=3.5', + python_requires='>=3.6', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -123,7 +123,6 @@ def build_py_modules(basedir, excludes=[]): "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From df39446bb7b90ab9436fa3a76f6d4182c2a47da2 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 17 Jun 2021 17:35:46 +0100 Subject: [PATCH 0116/1849] del travis --- .travis.yml | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8a171b4fd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -# UNUSED, only for reference. If adjustments are needed, please see github actions -language: python -python: - - "3.4" - - "3.6" - - "3.7" - - "3.8" - - "nightly" - # - "pypy" - won't work as smmap doesn't work (see gitdb/.travis.yml for details) -matrix: - allow_failures: - - python: "nightly" -git: - # a higher depth is needed for most of the tests - must be high enough to not actually be shallow - # as we clone our own repository in the process - depth: 99999 -install: - - python --version; git --version - - git submodule update --init --recursive - - git fetch --tags - - pip install -r test-requirements.txt - - pip install -r doc/requirements.txt - - pip install codecov - - # generate some reflog as git-python tests need it (in master) - - ./init-tests-after-clone.sh - - # as commits are performed with the default user, it needs to be set for travis too - - git config --global user.email "travis@ci.com" - - git config --global user.name "Travis Runner" - # If we rewrite the user's config by accident, we will mess it up - # and cause subsequent tests to fail - - cat git/test/fixtures/.gitconfig >> ~/.gitconfig -script: - # Make sure we limit open handles to see if we are leaking them - - ulimit -n 128 - - ulimit -n - - coverage run --omit="test/*" -m unittest --buffer - - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi -after_success: - - codecov From f8ec952343583324c4f5dbefa4fb846f395ea6e4 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 17 Jun 2021 17:49:48 +0100 Subject: [PATCH 0117/1849] fix issue with mypy update to 0.9 --- git/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/util.py b/git/util.py index edbd5f1e7..05eeb4ad3 100644 --- a/git/util.py +++ b/git/util.py @@ -971,7 +971,7 @@ def __getattr__(self, attr: str) -> Any: # END for each item return list.__getattribute__(self, attr) - def __getitem__(self, index: Union[int, slice, str]) -> Any: + def __getitem__(self, index: Union[int, slice, str]) -> Any: # type: ignore if isinstance(index, int): return list.__getitem__(self, index) elif isinstance(index, slice): @@ -983,7 +983,7 @@ def __getitem__(self, index: Union[int, slice, str]) -> Any: 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[int, str, slice]) -> None: # type: ignore delindex = cast(int, index) if not isinstance(index, int): From 18b6aa55309adfa8aa99bdaf9e8f80337befe74e Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 17 Jun 2021 18:10:46 +0100 Subject: [PATCH 0118/1849] add SupportsIndex to IterableList, with version import guards --- git/types.py | 9 +++------ git/util.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/git/types.py b/git/types.py index 91d35b567..a410cb366 100644 --- a/git/types.py +++ b/git/types.py @@ -7,15 +7,12 @@ from typing import Union, Any if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal # noqa: F401 + from typing import Final, Literal, SupportsIndex # noqa: F401 else: - from typing_extensions import Final, Literal # noqa: F401 + from typing_extensions import Final, Literal, SupportsIndex # noqa: F401 -if sys.version_info[:2] < (3, 6): - # os.PathLike (PEP-519) only got introduced with Python 3.6 - PathLike = str -elif sys.version_info[:2] < (3, 9): +if sys.version_info[:2] < (3, 9): # Python >= 3.6, < 3.9 PathLike = Union[str, os.PathLike] elif sys.version_info[:2] >= (3, 9): diff --git a/git/util.py b/git/util.py index 05eeb4ad3..516c315c1 100644 --- a/git/util.py +++ b/git/util.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo -from .types import PathLike, TBD, Literal +from .types import PathLike, TBD, Literal, SupportsIndex # --------------------------------------------------------------------- @@ -971,7 +971,10 @@ def __getattr__(self, attr: str) -> Any: # END for each item return list.__getattribute__(self, attr) - def __getitem__(self, index: Union[int, slice, str]) -> Any: # type: ignore + def __getitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: + + 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): @@ -983,12 +986,13 @@ def __getitem__(self, index: Union[int, slice, str]) -> Any: # type: ignore 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: # type: ignore + def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: + + 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: From bef6d375fd21e3047ed94b79a26183050c1cc4cb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 18 Jun 2021 11:23:26 +0800 Subject: [PATCH 0119/1849] Remove travis file as it's not used anymore in favor or Github Actions --- .travis.yml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1bed8368c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -# UNUSED, only for reference. If adjustments are needed, please see github actions -language: python -python: -<<<<<<< HEAD - - "3.4" -======= ->>>>>>> b0f79c58ad919e90261d1e332df79a4ad0bc40de - - "3.6" - - "3.7" - - "3.8" - - "nightly" - # - "pypy" - won't work as smmap doesn't work (see gitdb/.travis.yml for details) -matrix: - allow_failures: - - python: "nightly" -git: - # a higher depth is needed for most of the tests - must be high enough to not actually be shallow - # as we clone our own repository in the process - depth: 99999 -install: - - python --version; git --version - - git submodule update --init --recursive - - git fetch --tags - - pip install -r test-requirements.txt - - pip install -r doc/requirements.txt - - pip install codecov - - # generate some reflog as git-python tests need it (in master) - - ./init-tests-after-clone.sh - - # as commits are performed with the default user, it needs to be set for travis too - - git config --global user.email "travis@ci.com" - - git config --global user.name "Travis Runner" - # If we rewrite the user's config by accident, we will mess it up - # and cause subsequent tests to fail - - cat git/test/fixtures/.gitconfig >> ~/.gitconfig -script: - # Make sure we limit open handles to see if we are leaking them - - ulimit -n 128 - - ulimit -n - - coverage run --omit="test/*" -m unittest --buffer - - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then cd doc && make html; fi - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi -after_success: - - codecov From 8340e0bb6ad0d7c1cdb26cbe62828d3595c3b7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Mon, 21 Jun 2021 16:34:38 +0200 Subject: [PATCH 0120/1849] Fix link to latest changelog --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index aa8116b23..9796566ae 100644 --- a/CHANGES +++ b/CHANGES @@ -1,2 +1,2 @@ Please see the online documentation for the latest changelog: -https://github.com/gitpython-developers/GitPython/blob/master/doc/source/changes.rst +https://github.com/gitpython-developers/GitPython/blob/main/doc/source/changes.rst From 5b6fe83f4d817a3b73b44df16cfb4f96bd4d9904 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Wed, 23 Jun 2021 02:22:34 +0100 Subject: [PATCH 0121/1849] Update typing-extensions version in requirements.txt --- .appveyor.yml | 29 +------ .github/workflows/pythonpackage.yml | 2 +- .travis.yml | 44 ---------- CHANGES | 2 +- README.md | 2 +- VERSION | 2 +- doc/source/changes.rst | 8 ++ doc/source/intro.rst | 2 +- doc/source/tutorial.rst | 2 +- git/cmd.py | 12 +-- git/diff.py | 4 +- git/index/fun.py | 2 +- git/objects/base.py | 65 +++++++++----- git/objects/blob.py | 4 +- git/objects/commit.py | 20 +++-- git/objects/submodule/base.py | 6 +- git/objects/tag.py | 28 ++++-- git/objects/tree.py | 22 ++++- git/objects/util.py | 129 ++++++++++++++++++++-------- git/repo/base.py | 22 ++++- git/types.py | 9 +- git/util.py | 12 ++- requirements.txt | 2 +- setup.py | 5 +- test-requirements.txt | 2 +- test/test_repo.py | 30 ++++++- tox.ini | 2 +- 27 files changed, 279 insertions(+), 190 deletions(-) delete mode 100644 .travis.yml diff --git a/.appveyor.yml b/.appveyor.yml index 0a86c1a75..833f5c7b9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,29 +6,12 @@ environment: CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%" matrix: - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" GIT_PATH: "%GIT_DAEMON_PATH%" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" GIT_PATH: "%GIT_DAEMON_PATH%" - - PYTHON: "C:\\Miniconda35-x64" - PYTHON_VERSION: "3.5" - IS_CONDA: "yes" - MAYFAIL: "yes" - GIT_PATH: "%GIT_DAEMON_PATH%" - ## Cygwin - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - IS_CYGWIN: "yes" - MAYFAIL: "yes" - GIT_PATH: "%CYGWIN64_GIT_PATH%" matrix: allow_failures: @@ -76,18 +59,10 @@ install: build: false test_script: - - IF "%IS_CYGWIN%" == "yes" ( - nosetests -v - ) ELSE ( - IF "%PYTHON_VERSION%" == "3.5" ( - nosetests -v --with-coverage - ) ELSE ( - nosetests -v - ) - ) + - nosetests -v on_success: - - IF "%PYTHON_VERSION%" == "3.5" IF NOT "%IS_CYGWIN%" == "yes" (codecov) + - IF "%PYTHON_VERSION%" == "3.6" IF NOT "%IS_CYGWIN%" == "yes" (codecov) # Enable this to be able to login to the build worker. You can use the # `remmina` program in Ubuntu, use the login information that the line below diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3c7215cbe..53da76149 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1fbb1ddb8..000000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -# UNUSED, only for reference. If adjustments are needed, please see github actions -language: python -python: - - "3.4" - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "nightly" - # - "pypy" - won't work as smmap doesn't work (see gitdb/.travis.yml for details) -matrix: - allow_failures: - - python: "nightly" -git: - # a higher depth is needed for most of the tests - must be high enough to not actually be shallow - # as we clone our own repository in the process - depth: 99999 -install: - - python --version; git --version - - git submodule update --init --recursive - - git fetch --tags - - pip install -r test-requirements.txt - - pip install -r doc/requirements.txt - - pip install codecov - - # generate some reflog as git-python tests need it (in master) - - ./init-tests-after-clone.sh - - # as commits are performed with the default user, it needs to be set for travis too - - git config --global user.email "travis@ci.com" - - git config --global user.name "Travis Runner" - # If we rewrite the user's config by accident, we will mess it up - # and cause subsequent tests to fail - - cat git/test/fixtures/.gitconfig >> ~/.gitconfig -script: - # Make sure we limit open handles to see if we are leaking them - - ulimit -n 128 - - ulimit -n - - coverage run --omit="test/*" -m unittest --buffer - - coverage report - - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi - - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi -after_success: - - codecov diff --git a/CHANGES b/CHANGES index aa8116b23..9796566ae 100644 --- a/CHANGES +++ b/CHANGES @@ -1,2 +1,2 @@ Please see the online documentation for the latest changelog: -https://github.com/gitpython-developers/GitPython/blob/master/doc/source/changes.rst +https://github.com/gitpython-developers/GitPython/blob/main/doc/source/changes.rst diff --git a/README.md b/README.md index 0d0edeb43..4725d3aeb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. * Git (1.7.x or newer) -* Python >= 3.5 +* Python >= 3.6 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. diff --git a/VERSION b/VERSION index 3797f3f9c..5762a6ffe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.17 +3.1.18 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 68a94516c..aabef8023 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ========= +3.1.18 +====== + +* drop support for python 3.5 to reduce maintenance burden on typing. Lower patch levels of python 3.5 would break, too. + +See the following for details: +https://github.com/gitpython-developers/gitpython/milestone/50?closed=1 + 3.1.17 ====== diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 7168c91b1..956a36073 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.5 +* `Python`_ >= 3.6 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index d548f8829..303e89cff 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -10,7 +10,7 @@ GitPython Tutorial GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explains a real-life usecase. -All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. +All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. Meet the Repo type ****************** diff --git a/git/cmd.py b/git/cmd.py index d8b82352d..e078e4a18 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,9 +17,7 @@ import subprocess import sys import threading -from collections import OrderedDict from textwrap import dedent -import warnings from git.compat import ( defenc, @@ -150,7 +148,6 @@ def dashify(string: str) -> str: def slots_to_dict(self, exclude: Sequence[str] = ()) -> Dict[str, Any]: - # annotate self.__slots__ as Tuple[str, ...] once 3.5 dropped return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} @@ -462,7 +459,7 @@ class CatFileContentStream(object): If not all data is read to the end of the objects's lifetime, we read the rest to assure the underlying stream continues to work""" - __slots__ = ('_stream', '_nbr', '_size') + __slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size') def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream @@ -1005,13 +1002,6 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: """Transforms Python style kwargs into git command line options.""" - # Python 3.6 preserves the order of kwargs and thus has a stable - # order. For older versions sort the kwargs by the key to get a stable - # order. - if sys.version_info[:2] < (3, 6): - kwargs = OrderedDict(sorted(kwargs.items(), key=lambda x: x[0])) - warnings.warn("Python 3.5 support is deprecated and will be removed 2021-09-05.\n" + - "It does not preserve the order for key-word arguments and enforce lexical sorting instead.") args = [] for k, v in kwargs.items(): if isinstance(v, (list, tuple)): diff --git a/git/diff.py b/git/diff.py index a40fc244e..346a2ca7b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,7 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union, TYPE_CHECKING -from git.types import PathLike, TBD, Final, Literal +from git.types import PathLike, TBD, Literal if TYPE_CHECKING: from .objects.tree import Tree @@ -31,7 +31,7 @@ __all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE') # Special object to compare against the empty tree in diffs -NULL_TREE = object() # type: Final[object] +NULL_TREE = object() _octal_byte_re = re.compile(b'\\\\([0-9]{3})') diff --git a/git/index/fun.py b/git/index/fun.py index 1012f4801..3fded3473 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -109,7 +109,7 @@ def run_commit_hook(name: str, index: 'IndexFile', *args: str) -> None: # end handle return code -def stat_mode_to_index_mode(mode): +def stat_mode_to_index_mode(mode: int) -> int: """Convert the given mode from a stat call to the corresponding index mode and return it""" if S_ISLNK(mode): # symlinks diff --git a/git/objects/base.py b/git/objects/base.py index 59f0e8368..884f96515 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -3,16 +3,34 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php + +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 typing import Optional # noqa: F401 unused import from .util import get_object_type_by_name -_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutal git object type %r" +# typing ------------------------------------------------------------------ + +from typing import Any, TYPE_CHECKING, Optional, Union + +from git.types import PathLike + +if TYPE_CHECKING: + from git.repo import Repo + from gitdb.base import OStream + from .tree import Tree + from .blob import Blob + from .tag import TagObject + from .commit import Commit + +# -------------------------------------------------------------------------- + + +_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutual git object type %r" __all__ = ("Object", "IndexObject") @@ -27,7 +45,7 @@ class Object(LazyMixin): __slots__ = ("repo", "binsha", "size") type = None # type: Optional[str] # to be set by subclass - def __init__(self, repo, binsha): + def __init__(self, repo: 'Repo', binsha: bytes): """Initialize an object by identifying it by its binary sha. All keyword arguments will be set on demand if None. @@ -40,7 +58,7 @@ def __init__(self, repo, binsha): assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (binsha, len(binsha)) @classmethod - def new(cls, repo, id): # @ReservedAssignment + def new(cls, repo: 'Repo', id): # @ReservedAssignment """ :return: New Object instance of a type appropriate to the object type behind id. The id of the newly created object will be a binsha even though @@ -53,7 +71,7 @@ def new(cls, repo, id): # @ReservedAssignment return repo.rev_parse(str(id)) @classmethod - def new_from_sha(cls, repo, sha1): + def new_from_sha(cls, repo: 'Repo', sha1: bytes) -> Union['Commit', 'TagObject', 'Tree', 'Blob']: """ :return: new object instance of a type appropriate to represent the given binary sha1 @@ -67,52 +85,52 @@ def new_from_sha(cls, repo, sha1): inst.size = oinfo.size return inst - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: """Retrieve object information""" if attr == "size": oinfo = self.repo.odb.info(self.binsha) - self.size = oinfo.size + self.size = oinfo.size # type: int # assert oinfo.type == self.type, _assertion_msg_format % (self.binsha, oinfo.type, self.type) else: super(Object, self)._set_cache_(attr) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """:return: True if the objects have the same SHA1""" if not hasattr(other, 'binsha'): return False return self.binsha == other.binsha - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: """:return: True if the objects do not have the same SHA1 """ if not hasattr(other, 'binsha'): return True return self.binsha != other.binsha - def __hash__(self): + def __hash__(self) -> int: """:return: Hash of our id allowing objects to be used in dicts and sets""" return hash(self.binsha) - def __str__(self): + def __str__(self) -> str: """:return: string of our SHA1 as understood by all git commands""" return self.hexsha - def __repr__(self): + def __repr__(self) -> str: """:return: string with pythonic representation of our object""" return '' % (self.__class__.__name__, self.hexsha) @property - def hexsha(self): + def hexsha(self) -> str: """:return: 40 byte hex version of our 20 byte binary sha""" # b2a_hex produces bytes return bin_to_hex(self.binsha).decode('ascii') @property - def data_stream(self): + def data_stream(self) -> 'OStream': """ :return: File Object compatible stream to the uncompressed raw data of the object :note: returned streams must be read in order""" return self.repo.odb.stream(self.binsha) - def stream_data(self, ostream): + def stream_data(self, ostream: 'OStream') -> 'Object': """Writes our data directly to the given output stream :param ostream: File object compatible stream object. :return: self""" @@ -130,7 +148,9 @@ class IndexObject(Object): # for compatibility with iterable lists _id_attribute_ = 'path' - def __init__(self, repo, binsha, mode=None, path=None): + def __init__(self, + repo: 'Repo', binsha: bytes, mode: Union[None, int] = None, path: Union[None, PathLike] = None + ) -> None: """Initialize a newly instanced IndexObject :param repo: is the Repo we are located in @@ -150,14 +170,14 @@ def __init__(self, repo, binsha, mode=None, path=None): if path is not None: self.path = path - def __hash__(self): + def __hash__(self) -> int: """ :return: Hash of our path as index items are uniquely identifiable by path, not by their data !""" return hash(self.path) - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr in IndexObject.__slots__: # they cannot be retrieved lateron ( not without searching for them ) raise AttributeError( @@ -168,16 +188,19 @@ def _set_cache_(self, attr): # END handle slot attribute @property - def name(self): + def name(self) -> str: """:return: Name portion of the path, effectively being the basename""" return osp.basename(self.path) @property - def abspath(self): + def abspath(self) -> PathLike: """ :return: Absolute path to this index object in the file system ( as opposed to the .path field which is a path relative to the git repository ). The returned path will be native to the system and contains '\' on windows. """ - return join_path_native(self.repo.working_tree_dir, self.path) + if self.repo.working_tree_dir is not None: + return join_path_native(self.repo.working_tree_dir, self.path) + else: + raise WorkTreeRepositoryUnsupported("Working_tree_dir was None or empty") diff --git a/git/objects/blob.py b/git/objects/blob.py index 897f892bf..017178f05 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -23,11 +23,11 @@ class Blob(base.IndexObject): __slots__ = () @property - def mime_type(self): + def mime_type(self) -> str: """ :return: String describing the mime type of this file (based on the filename) :note: Defaults to 'text/plain' in case the actual file type is unknown. """ guesses = None if self.path: - guesses = guess_type(self.path) + guesses = guess_type(str(self.path)) return guesses and guesses[0] or self.DEFAULT_MIME_TYPE diff --git a/git/objects/commit.py b/git/objects/commit.py index 45e6d772c..26db6e36d 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -36,6 +36,11 @@ from io import BytesIO import logging +from typing import List, Tuple, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from git.repo import Repo + log = logging.getLogger('git.objects.commit') log.addHandler(logging.NullHandler()) @@ -70,7 +75,8 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None, committer=None, committed_date=None, committer_tz_offset=None, - message=None, parents=None, encoding=None, gpgsig=None): + message=None, parents: Union[Tuple['Commit', ...], List['Commit'], None] = None, + encoding=None, gpgsig=None): """Instantiate a new Commit. All keyword arguments taking None as default will be implicitly set on first query. @@ -133,11 +139,11 @@ def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, aut self.gpgsig = gpgsig @classmethod - def _get_intermediate_items(cls, commit): - return commit.parents + def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: # type: ignore ## cos overriding super + return tuple(commit.parents) @classmethod - def _calculate_sha_(cls, repo, commit): + def _calculate_sha_(cls, repo: 'Repo', commit: 'Commit') -> bytes: '''Calculate the sha of a commit. :param repo: Repo object the commit should be part of @@ -430,7 +436,7 @@ def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, #{ Serializable Implementation - def _serialize(self, stream): + def _serialize(self, stream: BytesIO) -> 'Commit': write = stream.write write(("tree %s\n" % self.tree).encode('ascii')) for p in self.parents: @@ -471,7 +477,7 @@ def _serialize(self, stream): # END handle encoding return self - def _deserialize(self, stream): + def _deserialize(self, stream: BytesIO) -> 'Commit': """:param from_rev_list: if true, the stream format is coming from the rev-list command Otherwise it is assumed to be a plain data stream from our object""" readline = stream.readline @@ -511,7 +517,7 @@ def _deserialize(self, stream): buf = enc.strip() while buf: if buf[0:10] == b"encoding ": - self.encoding = buf[buf.find(' ') + 1:].decode( + self.encoding = buf[buf.find(b' ') + 1:].decode( self.encoding, 'ignore') elif buf[0:7] == b"gpgsig ": sig = buf[buf.find(b' ') + 1:] + b"\n" diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index e3be1a728..b03fa22a5 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,6 +3,7 @@ import logging import os import stat +from typing import List from unittest import SkipTest import uuid @@ -134,10 +135,11 @@ def _set_cache_(self, attr): super(Submodule, self)._set_cache_(attr) # END handle attribute name - def _get_intermediate_items(self, item): + @classmethod + def _get_intermediate_items(cls, item: 'Submodule') -> List['Submodule']: # type: ignore """:return: all the submodules of our module repository""" try: - return type(self).list_items(item.module()) + return cls.list_items(item.module()) except InvalidGitRepositoryError: return [] # END handle intermediate items diff --git a/git/objects/tag.py b/git/objects/tag.py index b9bc6c248..cb6efbe9b 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -9,6 +9,15 @@ from ..util import hex_to_bin from ..compat import defenc +from typing import List, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from git.repo import Repo + from git.util import Actor + from .commit import Commit + from .blob import Blob + from .tree import Tree + __all__ = ("TagObject", ) @@ -18,8 +27,14 @@ class TagObject(base.Object): type = "tag" __slots__ = ("object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message") - def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment - tagger=None, tagged_date=None, tagger_tz_offset=None, message=None): + def __init__(self, repo: 'Repo', binsha: bytes, + object: Union[None, base.Object] = None, + tag: Union[None, str] = None, + tagger: Union[None, 'Actor'] = None, + tagged_date: Union[int, None] = None, + tagger_tz_offset: Union[int, None] = None, + message: Union[str, None] = None + ) -> None: # @ReservedAssignment """Initialize a tag object with additional data :param repo: repository this object is located in @@ -34,7 +49,7 @@ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment authored_date is in, in a format similar to time.altzone""" super(TagObject, self).__init__(repo, binsha) if object is not None: - self.object = object + self.object = object # type: Union['Commit', 'Blob', 'Tree', 'TagObject'] if tag is not None: self.tag = tag if tagger is not None: @@ -46,16 +61,17 @@ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment if message is not None: self.message = message - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: """Cache all our attributes at once""" if attr in TagObject.__slots__: ostream = self.repo.odb.stream(self.binsha) - lines = ostream.read().decode(defenc, 'replace').splitlines() + lines = ostream.read().decode(defenc, 'replace').splitlines() # type: List[str] _obj, hexsha = lines[0].split(" ") _type_token, type_name = lines[1].split(" ") + object_type = get_object_type_by_name(type_name.encode('ascii')) self.object = \ - get_object_type_by_name(type_name.encode('ascii'))(self.repo, hex_to_bin(hexsha)) + object_type(self.repo, hex_to_bin(hexsha)) self.tag = lines[2][4:] # tag diff --git a/git/objects/tree.py b/git/objects/tree.py index 68e98329b..29b2a6846 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -17,6 +17,17 @@ tree_to_stream ) + +# typing ------------------------------------------------- + +from typing import Iterable, Iterator, Tuple, Union, cast, TYPE_CHECKING + +if TYPE_CHECKING: + from io import BytesIO + +#-------------------------------------------------------- + + cmp = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") @@ -182,8 +193,10 @@ def __init__(self, repo, binsha, mode=tree_id << 12, path=None): super(Tree, self).__init__(repo, binsha, mode, path) @classmethod - def _get_intermediate_items(cls, index_object): + def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore + ) -> Tuple['Tree', ...]: if index_object.type == "tree": + index_object = cast('Tree', index_object) return tuple(index_object._iter_convert_to_object(index_object._cache)) return () @@ -196,7 +209,8 @@ def _set_cache_(self, attr): super(Tree, self)._set_cache_(attr) # END handle attribute - def _iter_convert_to_object(self, iterable): + def _iter_convert_to_object(self, iterable: Iterable[Tuple[bytes, int, str]] + ) -> Iterator[Union[Blob, 'Tree', Submodule]]: """Iterable yields tuples of (binsha, mode, name), which will be converted to the respective object representation""" for binsha, mode, name in iterable: @@ -317,7 +331,7 @@ def __contains__(self, item): def __reversed__(self): return reversed(self._iter_convert_to_object(self._cache)) - def _serialize(self, stream): + def _serialize(self, stream: 'BytesIO') -> 'Tree': """Serialize this tree into the stream. Please note that we will assume our tree data to be in a sorted state. If this is not the case, serialization will not generate a correct tree representation as these are assumed to be sorted @@ -325,7 +339,7 @@ def _serialize(self, stream): tree_to_stream(self._cache, stream.write) return self - def _deserialize(self, stream): + def _deserialize(self, stream: 'BytesIO') -> 'Tree': self._cache = tree_entries_from_data(stream.read()) return self diff --git a/git/objects/util.py b/git/objects/util.py index d15d83c35..087f0166b 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -4,19 +4,34 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php """Module for general utility functions""" + from git.util import ( IterableList, Actor ) import re -from collections import deque as Deque +from collections import deque from string import digits import time import calendar from datetime import datetime, timedelta, tzinfo +# typing ------------------------------------------------------------ +from typing import (Any, Callable, Deque, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) + +if TYPE_CHECKING: + from io import BytesIO, StringIO + from .submodule.base import Submodule + from .commit import Commit + from .blob import Blob + from .tag import TagObject + from .tree import Tree + from subprocess import Popen + +# -------------------------------------------------------------------- + __all__ = ('get_object_type_by_name', 'parse_date', 'parse_actor_and_date', 'ProcessStreamAdapter', 'Traversable', 'altz_to_utctz_str', 'utctz_to_altz', 'verify_utctz', 'Actor', 'tzoffset', 'utc') @@ -26,7 +41,7 @@ #{ Functions -def mode_str_to_int(modestr): +def mode_str_to_int(modestr: Union[bytes, str]) -> int: """ :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used :return: @@ -36,12 +51,14 @@ def mode_str_to_int(modestr): for example.""" mode = 0 for iteration, char in enumerate(reversed(modestr[-6:])): + char = cast(Union[str, int], char) mode += int(char) << iteration * 3 # END for each char return mode -def get_object_type_by_name(object_type_name): +def get_object_type_by_name(object_type_name: bytes + ) -> Union[Type['Commit'], Type['TagObject'], Type['Tree'], Type['Blob']]: """ :return: type suitable to handle the given object type name. Use the type to create new instances. @@ -62,10 +79,10 @@ def get_object_type_by_name(object_type_name): from . import tree return tree.Tree else: - raise ValueError("Cannot handle unknown object type: %s" % object_type_name) + raise ValueError("Cannot handle unknown object type: %s" % object_type_name.decode()) -def utctz_to_altz(utctz): +def utctz_to_altz(utctz: str) -> int: """we convert utctz to the timezone in seconds, it is the format time.altzone returns. Git stores it as UTC timezone which has the opposite sign as well, which explains the -1 * ( that was made explicit here ) @@ -73,7 +90,7 @@ def utctz_to_altz(utctz): return -1 * int(float(utctz) / 100 * 3600) -def altz_to_utctz_str(altz): +def altz_to_utctz_str(altz: int) -> str: """As above, but inverses the operation, returning a string that can be used in commit objects""" utci = -1 * int((float(altz) / 3600) * 100) @@ -83,7 +100,7 @@ def altz_to_utctz_str(altz): return prefix + utcs -def verify_utctz(offset): +def verify_utctz(offset: str) -> str: """:raise ValueError: if offset is incorrect :return: offset""" fmt_exc = ValueError("Invalid timezone offset format: %s" % offset) @@ -101,27 +118,28 @@ def verify_utctz(offset): class tzoffset(tzinfo): - def __init__(self, secs_west_of_utc, name=None): + + def __init__(self, secs_west_of_utc: float, name: Union[None, str] = None) -> None: self._offset = timedelta(seconds=-secs_west_of_utc) self._name = name or 'fixed' - def __reduce__(self): + def __reduce__(self) -> Tuple[Type['tzoffset'], Tuple[float, str]]: return tzoffset, (-self._offset.total_seconds(), self._name) - def utcoffset(self, dt): + def utcoffset(self, dt) -> timedelta: return self._offset - def tzname(self, dt): + def tzname(self, dt) -> str: return self._name - def dst(self, dt): + def dst(self, dt) -> timedelta: return ZERO utc = tzoffset(0, 'UTC') -def from_timestamp(timestamp, tz_offset): +def from_timestamp(timestamp, tz_offset: float) -> datetime: """Converts a timestamp + tz_offset into an aware datetime instance.""" utc_dt = datetime.fromtimestamp(timestamp, utc) try: @@ -131,7 +149,7 @@ def from_timestamp(timestamp, tz_offset): return utc_dt -def parse_date(string_date): +def parse_date(string_date: str) -> Tuple[int, int]: """ Parse the given date as one of the following @@ -152,18 +170,18 @@ def parse_date(string_date): # git time try: if string_date.count(' ') == 1 and string_date.rfind(':') == -1: - timestamp, offset = string_date.split() + timestamp, offset_str = string_date.split() if timestamp.startswith('@'): timestamp = timestamp[1:] - timestamp = int(timestamp) - return timestamp, utctz_to_altz(verify_utctz(offset)) + timestamp_int = int(timestamp) + return timestamp_int, utctz_to_altz(verify_utctz(offset_str)) else: - offset = "+0000" # local time by default + offset_str = "+0000" # local time by default if string_date[-5] in '-+': - offset = verify_utctz(string_date[-5:]) + offset_str = verify_utctz(string_date[-5:]) string_date = string_date[:-6] # skip space as well # END split timezone info - offset = utctz_to_altz(offset) + offset = utctz_to_altz(offset_str) # now figure out the date and time portion - split time date_formats = [] @@ -218,13 +236,13 @@ def parse_date(string_date): _re_only_actor = re.compile(r'^.+? (.*)$') -def parse_actor_and_date(line): +def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: """Parse out the actor (author or committer) info from a line like:: author Tom Preston-Werner 1191999972 -0700 :return: [Actor, int_seconds_since_epoch, int_timezone_offset]""" - actor, epoch, offset = '', 0, 0 + actor, epoch, offset = '', '0', '0' m = _re_actor_epoch.search(line) if m: actor, epoch, offset = m.groups() @@ -247,11 +265,11 @@ class ProcessStreamAdapter(object): it if the instance goes out of scope.""" __slots__ = ("_proc", "_stream") - def __init__(self, process, stream_name): + def __init__(self, process: 'Popen', stream_name: str) -> None: self._proc = process - self._stream = getattr(process, stream_name) + self._stream = getattr(process, stream_name) # type: StringIO ## guess - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: return getattr(self._stream, attr) @@ -260,29 +278,61 @@ class Traversable(object): """Simple interface to perform depth-first or breadth-first traversals into one direction. Subclasses only need to implement one function. - Instances of the Subclass must be hashable""" + Instances of the Subclass must be hashable + + Defined subclasses = [Commit, Tree, SubModule] + """ __slots__ = () + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]: + ... + + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Submodule') -> Tuple['Submodule', ...]: + ... + + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]: + ... + + @overload + @classmethod + def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]: + ... + @classmethod - def _get_intermediate_items(cls, item): + def _get_intermediate_items(cls, item: 'Traversable' + ) -> Sequence['Traversable']: """ Returns: - List of items connected to the given item. + Tuple of items connected to the given item. Must be implemented in subclass + + class Commit:: (cls, Commit) -> Tuple[Commit, ...] + class Submodule:: (cls, Submodule) -> Iterablelist[Submodule] + class Tree:: (cls, Tree) -> Tuple[Tree, ...] """ raise NotImplementedError("To be implemented in subclass") - def list_traverse(self, *args, **kwargs): + def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList: """ :return: IterableList with the results of the traversal as produced by traverse()""" - out = IterableList(self._id_attribute_) + out = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses out.extend(self.traverse(*args, **kwargs)) return out - def traverse(self, predicate=lambda i, d: True, - prune=lambda i, d: False, depth=-1, branch_first=True, - visit_once=True, ignore_self=1, as_edge=False): + def traverse(self, + predicate: Callable[[object, int], bool] = lambda i, d: True, + prune: Callable[[object, int], bool] = lambda i, d: False, + depth: int = -1, + branch_first: bool = True, + visit_once: bool = True, ignore_self: int = 1, as_edge: bool = False + ) -> Union[Iterator['Traversable'], Iterator[Tuple['Traversable', 'Traversable']]]: """:return: iterator yielding of items found when traversing self :param predicate: f(i,d) returns False if item i at depth d should not be included in the result @@ -314,13 +364,16 @@ def traverse(self, predicate=lambda i, d: True, destination, i.e. tuple(src, dest) with the edge spanning from source to destination""" visited = set() - stack = Deque() + stack = deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]] stack.append((0, self, None)) # self is always depth level 0 - def addToStack(stack, item, branch_first, depth): + def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None]]], + item: 'Traversable', + branch_first: bool, + depth) -> None: lst = self._get_intermediate_items(item) if not lst: - return + return None if branch_first: stack.extendleft((depth, i, item) for i in lst) else: @@ -359,14 +412,14 @@ class Serializable(object): """Defines methods to serialize and deserialize objects from and into a data stream""" __slots__ = () - def _serialize(self, stream): + def _serialize(self, stream: 'BytesIO') -> 'Serializable': """Serialize the data of this object into the given data stream :note: a serialized object would ``_deserialize`` into the same object :param stream: a file-like object :return: self""" raise NotImplementedError("To be implemented in subclass") - def _deserialize(self, stream): + def _deserialize(self, stream: 'BytesIO') -> 'Serializable': """Deserialize all information regarding this object from the stream :param stream: a file-like object :return: self""" diff --git a/git/repo/base.py b/git/repo/base.py index 55682411a..5abd49618 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -3,12 +3,13 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - import logging import os import re import warnings +from gitdb.exc import BadObject + from git.cmd import ( Git, handle_process_output @@ -529,7 +530,7 @@ def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator['Tree']: :note: Takes all arguments known to iter_commits method""" return (c.tree for c in self.iter_commits(*args, **kwargs)) - def tree(self, rev: Union['Commit', 'Tree', None] = None) -> 'Tree': + def tree(self, rev: Union['Commit', 'Tree', str, None] = None) -> 'Tree': """The Tree object for the given treeish revision Examples:: @@ -618,6 +619,23 @@ def is_ancestor(self, ancestor_rev: 'Commit', rev: 'Commit') -> bool: raise return True + def is_valid_object(self, sha: str, object_type: str = None) -> bool: + try: + complete_sha = self.odb.partial_to_complete_sha_hex(sha) + object_info = self.odb.info(complete_sha) + if object_type: + if object_info.type == object_type.encode(): + return True + else: + log.debug("Commit hash points to an object of type '%s'. Requested were objects of type '%s'", + object_info.type.decode(), object_type) + return False + else: + return True + except BadObject: + log.debug("Commit hash is invalid.") + return False + def _get_daemon_export(self) -> bool: if self.git_dir: filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) diff --git a/git/types.py b/git/types.py index 91d35b567..a410cb366 100644 --- a/git/types.py +++ b/git/types.py @@ -7,15 +7,12 @@ from typing import Union, Any if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal # noqa: F401 + from typing import Final, Literal, SupportsIndex # noqa: F401 else: - from typing_extensions import Final, Literal # noqa: F401 + from typing_extensions import Final, Literal, SupportsIndex # noqa: F401 -if sys.version_info[:2] < (3, 6): - # os.PathLike (PEP-519) only got introduced with Python 3.6 - PathLike = str -elif sys.version_info[:2] < (3, 9): +if sys.version_info[:2] < (3, 9): # Python >= 3.6, < 3.9 PathLike = Union[str, os.PathLike] elif sys.version_info[:2] >= (3, 9): diff --git a/git/util.py b/git/util.py index edbd5f1e7..516c315c1 100644 --- a/git/util.py +++ b/git/util.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo -from .types import PathLike, TBD, Literal +from .types import PathLike, TBD, Literal, SupportsIndex # --------------------------------------------------------------------- @@ -971,7 +971,10 @@ def __getattr__(self, attr: str) -> Any: # 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]) -> Any: + + 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): @@ -983,12 +986,13 @@ def __getitem__(self, index: Union[int, slice, str]) -> Any: 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]) -> Any: + + 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: diff --git a/requirements.txt b/requirements.txt index d980f6682..7159416a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.8" diff --git a/setup.py b/setup.py index f8829c386..2845bbecd 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def build_py_modules(basedir, excludes=[]): include_package_data=True, py_modules=build_py_modules("./git", excludes=["git.ext.*"]), package_dir={'git': 'git'}, - python_requires='>=3.5', + python_requires='>=3.6', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -123,10 +123,9 @@ def build_py_modules(basedir, excludes=[]): "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9" ] ) diff --git a/test-requirements.txt b/test-requirements.txt index e06d2be14..16dc0d2c1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,4 +5,4 @@ tox virtualenv nose gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.8" diff --git a/test/test_repo.py b/test/test_repo.py index 0311653a2..8aced94d4 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -422,7 +422,7 @@ def test_tag_to_full_tag_path(self): self.rorepo.tag(tag) except ValueError as valueError: value_errors.append(valueError.args[0]) - raise ValueError('. '.join(value_errors)) + self.assertEqual(value_errors, []) def test_archive(self): tmpfile = tempfile.mktemp(suffix='archive-test') @@ -989,6 +989,34 @@ def test_is_ancestor(self): for i, j in itertools.permutations([c1, 'ffffff', ''], r=2): self.assertRaises(GitCommandError, repo.is_ancestor, i, j) + def test_is_valid_object(self): + repo = self.rorepo + commit_sha = 'f6aa8d1' + blob_sha = '1fbe3e4375' + tree_sha = '960b40fe36' + tag_sha = '42c2f60c43' + + # Check for valid objects + self.assertTrue(repo.is_valid_object(commit_sha)) + self.assertTrue(repo.is_valid_object(blob_sha)) + self.assertTrue(repo.is_valid_object(tree_sha)) + self.assertTrue(repo.is_valid_object(tag_sha)) + + # Check for valid objects of specific type + self.assertTrue(repo.is_valid_object(commit_sha, 'commit')) + self.assertTrue(repo.is_valid_object(blob_sha, 'blob')) + self.assertTrue(repo.is_valid_object(tree_sha, 'tree')) + self.assertTrue(repo.is_valid_object(tag_sha, 'tag')) + + # Check for invalid objects + self.assertFalse(repo.is_valid_object(b'1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a', 'blob')) + + # Check for invalid objects of specific type + self.assertFalse(repo.is_valid_object(commit_sha, 'blob')) + self.assertFalse(repo.is_valid_object(blob_sha, 'commit')) + self.assertFalse(repo.is_valid_object(tree_sha, 'commit')) + self.assertFalse(repo.is_valid_object(tag_sha, 'commit')) + @with_rw_directory def test_git_work_tree_dotgit(self, rw_dir): """Check that we find .git as a worktree file and find the worktree diff --git a/tox.ini b/tox.ini index a0cb1c9f1..e3dd84b6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37,py38,py39,flake8 +envlist = py36,py37,py38,py39,flake8 [testenv] commands = python -m unittest --buffer {posargs} From 93954d20310a7b77322211fd7c1eb8bd34217612 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Wed, 23 Jun 2021 02:22:34 +0100 Subject: [PATCH 0122/1849] Update typing-extensions version in requirements.txt --- doc/source/tutorial.rst | 2 +- requirements.txt | 2 +- test-requirements.txt | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index d548f8829..303e89cff 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -10,7 +10,7 @@ GitPython Tutorial GitPython provides object model access to your git repository. This tutorial is composed of multiple sections, most of which explains a real-life usecase. -All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. +All code presented here originated from `test_docs.py `_ to assure correctness. Knowing this should also allow you to more easily run the code for your own testing purposes, all you need is a developer installation of git-python. Meet the Repo type ****************** diff --git a/requirements.txt b/requirements.txt index d980f6682..7159416a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.8" diff --git a/test-requirements.txt b/test-requirements.txt index e06d2be14..16dc0d2c1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,4 +5,4 @@ tox virtualenv nose gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.0;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.8" diff --git a/tox.ini b/tox.ini index a0cb1c9f1..e3dd84b6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37,py38,py39,flake8 +envlist = py36,py37,py38,py39,flake8 [testenv] commands = python -m unittest --buffer {posargs} From 8bbf6520460dc4d2bfd7943cda666436f860cf71 Mon Sep 17 00:00:00 2001 From: Max Fan Date: Wed, 23 Jun 2021 14:20:11 -0400 Subject: [PATCH 0123/1849] Fix exsit typo --- git/objects/submodule/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index b03fa22a5..49d6aae14 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -390,7 +390,7 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N mrepo = None if url is None: if not has_module: - raise ValueError("A URL was not given and existing repository did not exsit at %s" % path) + raise ValueError("A URL was not given and existing repository did not exist at %s" % path) # END check url mrepo = sm.module() urls = [r.url for r in mrepo.remotes] From 091ac2960fe30fa5477fcb5bae203eb317090b3f Mon Sep 17 00:00:00 2001 From: Max Fan Date: Wed, 23 Jun 2021 14:21:39 -0400 Subject: [PATCH 0124/1849] Add missing article to error message --- git/objects/submodule/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 49d6aae14..cac9618da 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -390,7 +390,7 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N mrepo = None if url is None: if not has_module: - raise ValueError("A URL was not given and existing repository did not exist at %s" % path) + raise ValueError("A URL was not given and an existing repository did not exist at %s" % path) # END check url mrepo = sm.module() urls = [r.url for r in mrepo.remotes] From 703280b8c3df6f9b1a5cbe0997b717edbcaa8979 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 24 Jun 2021 08:00:18 +0800 Subject: [PATCH 0125/1849] remove duplication --- git/objects/submodule/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index cac9618da..8cf4dd1eb 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -390,7 +390,7 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N mrepo = None if url is None: if not has_module: - raise ValueError("A URL was not given and an existing repository did not exist at %s" % path) + raise ValueError("A URL was not given and a repository did not exist at %s" % path) # END check url mrepo = sm.module() urls = [r.url for r in mrepo.remotes] From 42e4f5e26b812385df65f8f32081035e2fb2a121 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 05:52:48 +0100 Subject: [PATCH 0126/1849] Add types to tree.Tree --- git/index/base.py | 2 +- git/index/fun.py | 2 +- git/objects/fun.py | 15 +++++++++--- git/objects/tree.py | 56 ++++++++++++++++++++++----------------------- git/repo/base.py | 3 ++- 5 files changed, 44 insertions(+), 34 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 044240602..e2b3f8fa4 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -568,7 +568,7 @@ def write_tree(self) -> Tree: # note: additional deserialization could be saved if write_tree_from_cache # would return sorted tree entries root_tree = Tree(self.repo, binsha, path='') - root_tree._cache = tree_items + root_tree._cache = tree_items # type: ignore return root_tree def _process_diff_args(self, args: List[Union[str, diff.Diffable, object]] diff --git a/git/index/fun.py b/git/index/fun.py index 3fded3473..10a440501 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -293,7 +293,7 @@ def write_tree_from_cache(entries: List[IndexEntry], odb, sl: slice, si: int = 0 # finally create the tree sio = BytesIO() tree_to_stream(tree_items, sio.write) # converts bytes of each item[0] to str - tree_items_stringified = cast(List[Tuple[str, int, str]], tree_items) # type: List[Tuple[str, int, str]] + tree_items_stringified = cast(List[Tuple[str, int, str]], tree_items) sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) diff --git a/git/objects/fun.py b/git/objects/fun.py index 9b36712e1..339a53b8c 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -1,10 +1,19 @@ """Module with functions which are supposed to be as fast as possible""" from stat import S_ISDIR + from git.compat import ( safe_decode, defenc ) +# typing ---------------------------------------------- + +from typing import List, Tuple + + +# --------------------------------------------------- + + __all__ = ('tree_to_stream', 'tree_entries_from_data', 'traverse_trees_recursive', 'traverse_tree_recursive') @@ -38,7 +47,7 @@ def tree_to_stream(entries, write): # END for each item -def tree_entries_from_data(data): +def tree_entries_from_data(data: bytes) -> List[Tuple[bytes, int, str]]: """Reads the binary representation of a tree and returns tuples of Tree items :param data: data block with tree data (as bytes) :return: list(tuple(binsha, mode, tree_relative_path), ...)""" @@ -72,8 +81,8 @@ def tree_entries_from_data(data): # default encoding for strings in git is utf8 # Only use the respective unicode object if the byte stream was encoded - name = data[ns:i] - name = safe_decode(name) + name_bytes = data[ns:i] + name = safe_decode(name_bytes) # byte is NULL, get next 20 i += 1 diff --git a/git/objects/tree.py b/git/objects/tree.py index 29b2a6846..ec7d8e885 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -20,20 +20,23 @@ # typing ------------------------------------------------- -from typing import Iterable, Iterator, Tuple, Union, cast, TYPE_CHECKING +from typing import Callable, Dict, Iterable, Iterator, List, Tuple, Type, Union, cast, TYPE_CHECKING + +from git.types import PathLike if TYPE_CHECKING: + from git.repo import Repo from io import BytesIO #-------------------------------------------------------- -cmp = lambda a, b: (a > b) - (a < b) +cmp: Callable[[int, int], int] = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") -def git_cmp(t1, t2): +def git_cmp(t1: 'Tree', t2: 'Tree') -> int: a, b = t1[2], t2[2] len_a, len_b = len(a), len(b) min_len = min(len_a, len_b) @@ -45,9 +48,9 @@ def git_cmp(t1, t2): return len_a - len_b -def merge_sort(a, cmp): +def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: if len(a) < 2: - return + return None mid = len(a) // 2 lefthalf = a[:mid] @@ -182,29 +185,29 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable): symlink_id = 0o12 tree_id = 0o04 - _map_id_to_type = { + _map_id_to_type: Dict[int, Union[Type[Submodule], Type[Blob], Type['Tree']]] = { commit_id: Submodule, blob_id: Blob, symlink_id: Blob # tree id added once Tree is defined } - def __init__(self, repo, binsha, mode=tree_id << 12, path=None): + def __init__(self, repo: 'Repo', binsha: bytes, mode: int = tree_id << 12, path: Union[PathLike, None] = None): super(Tree, self).__init__(repo, binsha, mode, path) @classmethod def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore - ) -> Tuple['Tree', ...]: + ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": index_object = cast('Tree', index_object) return tuple(index_object._iter_convert_to_object(index_object._cache)) return () - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr == "_cache": # Set the data when we need it ostream = self.repo.odb.stream(self.binsha) - self._cache = tree_entries_from_data(ostream.read()) + self._cache: List[Tuple[bytes, int, str]] = tree_entries_from_data(ostream.read()) else: super(Tree, self)._set_cache_(attr) # END handle attribute @@ -221,7 +224,7 @@ def _iter_convert_to_object(self, iterable: Iterable[Tuple[bytes, int, str]] raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e # END for each item - def join(self, file): + def join(self, file: str) -> Union[Blob, 'Tree', Submodule]: """Find the named object in this tree's contents :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule`` @@ -254,26 +257,22 @@ def join(self, file): raise KeyError(msg % file) # END handle long paths - def __div__(self, file): - """For PY2 only""" - return self.join(file) - - def __truediv__(self, file): + def __truediv__(self, file: str) -> Union['Tree', Blob, Submodule]: """For PY3 only""" return self.join(file) @property - def trees(self): + def trees(self) -> List['Tree']: """:return: list(Tree, ...) list of trees directly below this tree""" return [i for i in self if i.type == "tree"] @property - def blobs(self): + def blobs(self) -> List['Blob']: """:return: list(Blob, ...) list of blobs directly below this tree""" return [i for i in self if i.type == "blob"] @property - def cache(self): + def cache(self) -> TreeModifier: """ :return: An object allowing to modify the internal cache. This can be used to change the tree's contents. When done, make sure you call ``set_done`` @@ -289,16 +288,16 @@ def traverse(self, predicate=lambda i, d: True, return super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self) # List protocol - def __getslice__(self, i, j): + def __getslice__(self, i: int, j: int) -> List[Union[Blob, 'Tree', Submodule]]: return list(self._iter_convert_to_object(self._cache[i:j])) - def __iter__(self): + def __iter__(self) -> Iterator[Union[Blob, 'Tree', Submodule]]: return self._iter_convert_to_object(self._cache) - def __len__(self): + def __len__(self) -> int: return len(self._cache) - def __getitem__(self, item): + def __getitem__(self, item: Union[str, int, slice]) -> Union[Blob, 'Tree', Submodule]: if isinstance(item, int): info = self._cache[item] return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2])) @@ -310,7 +309,7 @@ def __getitem__(self, item): raise TypeError("Invalid index type: %r" % item) - def __contains__(self, item): + def __contains__(self, item: Union[IndexObject, PathLike]) -> bool: if isinstance(item, IndexObject): for info in self._cache: if item.binsha == info[0]: @@ -321,10 +320,11 @@ def __contains__(self, item): # compatibility # treat item as repo-relative path - path = self.path - for info in self._cache: - if item == join_path(path, info[2]): - return True + else: + path = self.path + for info in self._cache: + if item == join_path(path, info[2]): + return True # END for each item return False diff --git a/git/repo/base.py b/git/repo/base.py index 5abd49618..779477310 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -7,6 +7,7 @@ import os import re import warnings +from gitdb.db.loose import LooseObjectDB from gitdb.exc import BadObject @@ -100,7 +101,7 @@ class Repo(object): # Subclasses may easily bring in their own custom types by placing a constructor or type here GitCommandWrapperType = Git - def __init__(self, path: Optional[PathLike] = None, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, + def __init__(self, path: Optional[PathLike] = None, odbt: Type[LooseObjectDB] = GitCmdObjectDB, search_parent_directories: bool = False, expand_vars: bool = True) -> None: """Create a new Repo instance From c3903d8e03af5c1e01c1a96919b926c55f45052e Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 14:27:13 +0100 Subject: [PATCH 0127/1849] Make IterableList generic and update throughout --- git/objects/commit.py | 4 ++-- git/objects/submodule/base.py | 20 ++++++++++++++------ git/objects/util.py | 11 ++++++----- git/refs/reference.py | 4 ++-- git/remote.py | 31 ++++++++++++++++--------------- git/repo/base.py | 12 ++++++------ git/util.py | 22 ++++++++++++++++------ 7 files changed, 62 insertions(+), 42 deletions(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 26db6e36d..0b707450c 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -8,7 +8,7 @@ from git.util import ( hex_to_bin, Actor, - Iterable, + IterableObj, Stats, finalize_process ) @@ -47,7 +47,7 @@ __all__ = ('Commit', ) -class Commit(base.Object, Iterable, Diffable, Traversable, Serializable): +class Commit(base.Object, IterableObj, Diffable, Traversable, Serializable): """Wraps a git Commit object. diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 8cf4dd1eb..57396a467 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,7 +3,6 @@ import logging import os import stat -from typing import List from unittest import SkipTest import uuid @@ -27,7 +26,7 @@ from git.objects.base import IndexObject, Object from git.objects.util import Traversable from git.util import ( - Iterable, + IterableObj, join_path_native, to_native_path_linux, RemoteProgress, @@ -47,6 +46,15 @@ ) +# typing ---------------------------------------------------------------------- + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from git.util import IterableList + +# ----------------------------------------------------------------------------- + __all__ = ["Submodule", "UpdateProgress"] @@ -74,7 +82,7 @@ class UpdateProgress(RemoteProgress): # IndexObject comes via util module, its a 'hacky' fix thanks to pythons import # mechanism which cause plenty of trouble of the only reason for packages and # modules is refactoring - subpackages shouldn't depend on parent packages -class Submodule(IndexObject, Iterable, Traversable): +class Submodule(IndexObject, IterableObj, Traversable): """Implements access to a git submodule. They are special in that their sha represents a commit in the submodule's repository which is to be checked out @@ -136,12 +144,12 @@ def _set_cache_(self, attr): # END handle attribute name @classmethod - def _get_intermediate_items(cls, item: 'Submodule') -> List['Submodule']: # type: ignore + def _get_intermediate_items(cls, item: 'Submodule') -> IterableList['Submodule']: """:return: all the submodules of our module repository""" try: return cls.list_items(item.module()) except InvalidGitRepositoryError: - return [] + return IterableList('') # END handle intermediate items @classmethod @@ -1163,7 +1171,7 @@ def config_reader(self): :raise IOError: If the .gitmodules file/blob could not be read""" return self._config_parser_constrained(read_only=True) - def children(self): + def children(self) -> IterableList['Submodule']: """ :return: IterableList(Submodule, ...) an iterable list of submodules instances which are children of this submodule or 0 if the submodule is not checked out""" diff --git a/git/objects/util.py b/git/objects/util.py index 087f0166b..a565cf42f 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,11 +19,11 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast, overload) +from typing import (Any, Callable, Deque, Iterator, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO - from .submodule.base import Submodule + from .submodule.base import Submodule # noqa: F401 from .commit import Commit from .blob import Blob from .tag import TagObject @@ -284,6 +284,7 @@ class Traversable(object): """ __slots__ = () + """ @overload @classmethod def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]: @@ -303,10 +304,10 @@ def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]: @classmethod def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]: ... + """ @classmethod - def _get_intermediate_items(cls, item: 'Traversable' - ) -> Sequence['Traversable']: + def _get_intermediate_items(cls, item): """ Returns: Tuple of items connected to the given item. @@ -322,7 +323,7 @@ def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList: """ :return: IterableList with the results of the traversal as produced by traverse()""" - out = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses + out: IterableList = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses out.extend(self.traverse(*args, **kwargs)) return out diff --git a/git/refs/reference.py b/git/refs/reference.py index 9014f5558..8a9b04873 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -1,6 +1,6 @@ from git.util import ( LazyMixin, - Iterable, + IterableObj, ) from .symbolic import SymbolicReference @@ -23,7 +23,7 @@ def wrapper(self, *args): #}END utilities -class Reference(SymbolicReference, LazyMixin, Iterable): +class Reference(SymbolicReference, LazyMixin, IterableObj): """Represents a named reference to any object. Subclasses may apply restrictions though, i.e. Heads can only point to commits.""" diff --git a/git/remote.py b/git/remote.py index 6ea4b2a1a..a85297c17 100644 --- a/git/remote.py +++ b/git/remote.py @@ -13,7 +13,7 @@ from git.exc import GitCommandError from git.util import ( LazyMixin, - Iterable, + IterableObj, IterableList, RemoteProgress, CallableRemoteProgress, @@ -107,7 +107,7 @@ def to_progress_instance(progress: Union[Callable[..., Any], RemoteProgress, Non return progress -class PushInfo(object): +class PushInfo(IterableObj, object): """ Carries information about the result of a push operation of a single head:: @@ -220,7 +220,7 @@ def _from_line(cls, remote: 'Remote', line: str) -> 'PushInfo': return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) -class FetchInfo(object): +class FetchInfo(IterableObj, object): """ Carries information about the results of a fetch operation of a single head:: @@ -421,7 +421,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': return cls(remote_local_ref, flags, note, old_commit, local_remote_ref) -class Remote(LazyMixin, Iterable): +class Remote(LazyMixin, IterableObj): """Provides easy read and write access to a git remote. @@ -580,18 +580,18 @@ def urls(self) -> Iterator[str]: raise ex @property - def refs(self) -> IterableList: + def refs(self) -> IterableList[RemoteReference]: """ :return: IterableList of RemoteReference objects. It is prefixed, allowing you to omit the remote path portion, i.e.:: remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) return out_refs @property - def stale_refs(self) -> IterableList: + def stale_refs(self) -> IterableList[Reference]: """ :return: IterableList RemoteReference objects that do not have a corresponding @@ -606,7 +606,7 @@ def stale_refs(self) -> IterableList: as well. This is a fix for the issue described here: https://github.com/gitpython-developers/GitPython/issues/260 """ - out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: # expecting # * [would prune] origin/new_branch @@ -681,11 +681,12 @@ def update(self, **kwargs: Any) -> 'Remote': return self def _get_fetch_info_from_stderr(self, proc: TBD, - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: + progress: Union[Callable[..., Any], RemoteProgress, None] + ) -> IterableList['FetchInfo']: progress = to_progress_instance(progress) # skip first line as it is some remote info we are not interested in - output = IterableList('name') + output: IterableList['FetchInfo'] = IterableList('name') # lines which are no progress are fetch info lines # this also waits for the command to finish @@ -741,7 +742,7 @@ def _get_fetch_info_from_stderr(self, proc: TBD, return output def _get_push_info(self, proc: TBD, - progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList: + progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: progress = to_progress_instance(progress) # read progress information from stderr @@ -749,7 +750,7 @@ def _get_push_info(self, proc: TBD, # read the lines manually as it will use carriage returns between the messages # to override the previous one. This is why we read the bytes manually progress_handler = progress.new_message_handler() - output = IterableList('push_infos') + output: IterableList[PushInfo] = IterableList('push_infos') def stdout_handler(line: str) -> None: try: @@ -785,7 +786,7 @@ def _assert_refspec(self) -> None: def fetch(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - verbose: bool = True, **kwargs: Any) -> IterableList: + verbose: bool = True, **kwargs: Any) -> IterableList[FetchInfo]: """Fetch the latest changes for this remote :param refspec: @@ -832,7 +833,7 @@ def fetch(self, refspec: Union[str, List[str], None] = None, def pull(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - **kwargs: Any) -> IterableList: + **kwargs: Any) -> IterableList[FetchInfo]: """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. @@ -853,7 +854,7 @@ def pull(self, refspec: Union[str, List[str], None] = None, def push(self, refspec: Union[str, List[str], None] = None, progress: Union[Callable[..., Any], None] = None, - **kwargs: Any) -> IterableList: + **kwargs: Any) -> IterableList[PushInfo]: """Push changes from source branch in refspec to target branch in refspec. :param refspec: see 'fetch' method diff --git a/git/repo/base.py b/git/repo/base.py index 779477310..52727504b 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -309,7 +309,7 @@ def bare(self) -> bool: return self._bare @property - def heads(self) -> 'IterableList': + def heads(self) -> 'IterableList[Head]': """A list of ``Head`` objects representing the branch heads in this repo @@ -317,7 +317,7 @@ def heads(self) -> 'IterableList': return Head.list_items(self) @property - def references(self) -> 'IterableList': + def references(self) -> 'IterableList[Reference]': """A list of Reference objects representing tags, heads and remote references. :return: IterableList(Reference, ...)""" @@ -342,7 +342,7 @@ def head(self) -> 'HEAD': return HEAD(self, 'HEAD') @property - def remotes(self) -> 'IterableList': + def remotes(self) -> 'IterableList[Remote]': """A list of Remote objects allowing to access and manipulate remotes :return: ``git.IterableList(Remote, ...)``""" return Remote.list_items(self) @@ -358,13 +358,13 @@ def remote(self, name: str = 'origin') -> 'Remote': #{ Submodules @property - def submodules(self) -> 'IterableList': + def submodules(self) -> 'IterableList[Submodule]': """ :return: git.IterableList(Submodule, ...) of direct submodules available from the current head""" return Submodule.list_items(self) - def submodule(self, name: str) -> 'IterableList': + def submodule(self, name: str) -> 'Submodule': """ :return: Submodule with the given name :raise ValueError: If no such submodule exists""" try: @@ -396,7 +396,7 @@ def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator: #}END submodules @property - def tags(self) -> 'IterableList': + def tags(self) -> 'IterableList[TagReference]': """A list of ``Tag`` objects that are available in this repo :return: ``git.IterableList(TagReference, ...)`` """ return TagReference.list_items(self) diff --git a/git/util.py b/git/util.py index 516c315c1..5f184b7a2 100644 --- a/git/util.py +++ b/git/util.py @@ -22,7 +22,7 @@ # typing --------------------------------------------------------- from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List, - Optional, Pattern, Sequence, Tuple, Union, cast, TYPE_CHECKING, overload) + Optional, Pattern, Sequence, Tuple, TypeVar, Union, cast, TYPE_CHECKING, overload) import pathlib @@ -920,7 +920,10 @@ def _obtain_lock(self) -> None: # END endless loop -class IterableList(list): +T = TypeVar('T', bound='IterableObj') + + +class IterableList(List[T]): """ List of iterable objects allowing to query an object by id or by named index:: @@ -930,6 +933,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. @@ -938,7 +944,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: @@ -1015,7 +1021,7 @@ 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) -> IterableList['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 @@ -1024,12 +1030,12 @@ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> 'IterableList': :note: Favor the iter_items method as it will :return:list(Item,...) list of item instances""" - out_list = IterableList(cls._id_attribute_) + out_list: IterableList = 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) -> Iterator[TBD]: + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: # return typed to be compatible with subtypes e.g. Remote """For more information about the arguments, see list_items :return: iterator yielding Items""" @@ -1038,6 +1044,10 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator[TBD]: #} END classes +class IterableObj(Iterable): + pass + + class NullHandler(logging.Handler): def emit(self, record: object) -> None: pass From 3cef949913659584dd980f3de363dd830392bb68 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 15:32:25 +0100 Subject: [PATCH 0128/1849] Rename Iterable due to typing.Iterable. Add deprecation warning --- git/util.py | 44 +++++++++++++++++++++++++++++++++++++++----- t.py | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 t.py diff --git a/git/util.py b/git/util.py index 5f184b7a2..f72cd355a 100644 --- a/git/util.py +++ b/git/util.py @@ -18,6 +18,7 @@ import time from unittest import SkipTest from urllib.parse import urlsplit, urlunsplit +import warnings # typing --------------------------------------------------------- @@ -1013,15 +1014,52 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: list.__delitem__(self, delindex) +class IterableClassWatcher(type): + def __init__(cls, name, bases, clsdict): + for base in bases: + if type(base) == cls: + warnings.warn("GitPython Iterable is deprecated due to naming clash. Use IterableObj instead", + DeprecationWarning) + super(IterableClassWatcher, cls).__init__(name, bases, clsdict) + + class Iterable(object): """Defines an interface for iterable items which is to assure a uniform way to retrieve and iterate items within the git repository""" __slots__ = () _id_attribute_ = "attribute that most suitably identifies your instance" + __metaclass__ = IterableClassWatcher @classmethod - def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList['IterableObj']: + def list_items(cls, repo, *args, **kwargs): + """ + 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(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): + # 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") + + +class IterableObj(): + """Defines an interface for iterable items which is to assure a uniform + way to retrieve and iterate items within the git repository""" + __slots__ = () + _id_attribute_ = "attribute that most suitably identifies your instance" + + @classmethod + def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T]: """ 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 @@ -1044,10 +1082,6 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: #} END classes -class IterableObj(Iterable): - pass - - class NullHandler(logging.Handler): def emit(self, record: object) -> None: pass diff --git a/t.py b/t.py new file mode 100644 index 000000000..05d59c0cf --- /dev/null +++ b/t.py @@ -0,0 +1,19 @@ +class Watcher(type): + def __init__(cls, name, bases, clsdict): + [print("ooooo") for base in bases if issubclass(base, name)] + super(Watcher, cls).__init__(name, bases, clsdict) + + +class SuperClass(metaclass=Watcher): + pass + + +class SubClass0(SuperClass): + pass + + +class SubClass1(SuperClass): + print("test") + +class normo(): + print("wooo") From ae9d56e0fdd4df335a9def66aa2ac96459ed6e5c Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:05:03 +0100 Subject: [PATCH 0129/1849] Make Iterable deprecation warning on subclassing --- git/util.py | 10 ++++++---- t.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/git/util.py b/git/util.py index f72cd355a..78a60c9ae 100644 --- a/git/util.py +++ b/git/util.py @@ -1017,10 +1017,12 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> Any: class IterableClassWatcher(type): def __init__(cls, name, bases, clsdict): for base in bases: - if type(base) == cls: - warnings.warn("GitPython Iterable is deprecated due to naming clash. Use IterableObj instead", - DeprecationWarning) - super(IterableClassWatcher, cls).__init__(name, bases, clsdict) + if type(base) == IterableClassWatcher: + warnings.warn(f"GitPython Iterable subclassed by {name}. " + "Iterable is deprecated due to naming clash, " + "Use IterableObj instead \n", + DeprecationWarning, + stacklevel=2) class Iterable(object): diff --git a/t.py b/t.py index 05d59c0cf..215c26674 100644 --- a/t.py +++ b/t.py @@ -1,7 +1,15 @@ +import warnings + + class Watcher(type): def __init__(cls, name, bases, clsdict): - [print("ooooo") for base in bases if issubclass(base, name)] - super(Watcher, cls).__init__(name, bases, clsdict) + for base in bases: + if type(base) == Watcher: + warnings.warn(f"GitPython Iterable subclassed by {name}. " + "Iterable is deprecated due to naming clash, " + "Use IterableObj instead \n", + DeprecationWarning, + stacklevel=2) class SuperClass(metaclass=Watcher): @@ -15,5 +23,6 @@ class SubClass0(SuperClass): class SubClass1(SuperClass): print("test") + class normo(): print("wooo") From 8bf00a6719804c2fc5cca280e9dae6774acc1237 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:16:32 +0100 Subject: [PATCH 0130/1849] fix an import --- git/objects/submodule/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 57396a467..ce0f944e0 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -31,7 +31,8 @@ to_native_path_linux, RemoteProgress, rmtree, - unbare_repo + unbare_repo, + IterableList ) from git.util import HIDE_WINDOWS_KNOWN_ERRORS @@ -48,10 +49,6 @@ # typing ---------------------------------------------------------------------- -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from git.util import IterableList # ----------------------------------------------------------------------------- From 26dfeb66be61e9a2a9087bdecc98d255c0306079 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:23:15 +0100 Subject: [PATCH 0131/1849] fix indent --- git/objects/util.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/git/objects/util.py b/git/objects/util.py index a565cf42f..71137264a 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -284,28 +284,6 @@ class Traversable(object): """ __slots__ = () - """ - @overload - @classmethod - def _get_intermediate_items(cls, item: 'Commit') -> Tuple['Commit', ...]: - ... - - @overload - @classmethod - def _get_intermediate_items(cls, item: 'Submodule') -> Tuple['Submodule', ...]: - ... - - @overload - @classmethod - def _get_intermediate_items(cls, item: 'Tree') -> Tuple['Tree', ...]: - ... - - @overload - @classmethod - def _get_intermediate_items(cls, item: 'Traversable') -> Tuple['Traversable', ...]: - ... - """ - @classmethod def _get_intermediate_items(cls, item): """ From 4f5d2fd68e784c2b2fd914a196c66960c7f48b49 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 16:30:32 +0100 Subject: [PATCH 0132/1849] update docstring --- git/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git/util.py b/git/util.py index 78a60c9ae..79952be56 100644 --- a/git/util.py +++ b/git/util.py @@ -1036,11 +1036,13 @@ class Iterable(object): @classmethod def list_items(cls, repo, *args, **kwargs): """ + Deprecaated, 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. :note: Favor the iter_items method as it will + :return:list(Item,...) list of item instances""" out_list = IterableList(cls._id_attribute_) out_list.extend(cls.iter_items(repo, *args, **kwargs)) From d9f9027779931c3cdb04d570df5f01596539791b Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 17:09:51 +0100 Subject: [PATCH 0133/1849] update some TBDs to configparser --- git/objects/submodule/base.py | 2 +- git/util.py | 35 ++++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index ce0f944e0..cbf6cd0db 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1158,7 +1158,7 @@ def name(self): """ return self._name - def config_reader(self): + def config_reader(self) -> SectionConstraint: """ :return: ConfigReader instance which allows you to qurey the configuration values of this submodule, as provided by the .gitmodules file diff --git a/git/util.py b/git/util.py index 79952be56..245f45d1f 100644 --- a/git/util.py +++ b/git/util.py @@ -30,6 +30,8 @@ if TYPE_CHECKING: from git.remote import Remote from git.repo.base import Repo + from git.config import GitConfigParser, SectionConstraint + from .types import PathLike, TBD, Literal, SupportsIndex # --------------------------------------------------------------------- @@ -82,7 +84,7 @@ def unbare_repo(func: Callable) -> Callable: encounter a bare repository""" @wraps(func) - def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> TBD: + def wrapper(self: 'Remote', *args: Any, **kwargs: Any) -> Callable: if self.repo.bare: raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__) # END bare method @@ -108,7 +110,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) @@ -448,7 +450,7 @@ 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._seen_ops = [] # type: List[int] self._cur_line = None # type: Optional[str] self.error_lines = [] # type: List[str] self.other_lines = [] # type: List[str] @@ -669,7 +671,8 @@ def _from_string(cls, string: str) -> 'Actor': # 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() @@ -698,7 +701,7 @@ def default_name() -> str: 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 @@ -709,7 +712,7 @@ def committer(cls, config_reader: Optional[TBD] = None) -> 'Actor': 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) @@ -752,9 +755,14 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': """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: Dict[str, Dict[str, Union[int, Dict[str, int]]]] + hsh: Dict[str, Dict[str, TBD]] = {'total': {'insertions': 0, + 'deletions': 0, + 'lines': 0, + 'files': 0}, + 'files': {} + } # need typeddict? for line in text.splitlines(): (raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != '-' and int(raw_insertions) or 0 @@ -763,9 +771,10 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': 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 = {'insertions': insertions, + 'deletions': deletions, + 'lines': insertions + deletions} + hsh['files'][filename.strip()] = files_dict return Stats(hsh['total'], hsh['files']) @@ -1077,7 +1086,7 @@ def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T]: return out_list @classmethod - def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator: + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> Iterator[T]: # return typed to be compatible with subtypes e.g. Remote """For more information about the arguments, see list_items :return: iterator yielding Items""" From affee359af09cf7971676263f59118de82e7e059 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 17:35:22 +0100 Subject: [PATCH 0134/1849] Add typedDict --- git/types.py | 24 +++++++++++++++++++++--- git/util.py | 25 +++++++++++++------------ t.py | 28 ---------------------------- 3 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 t.py diff --git a/git/types.py b/git/types.py index a410cb366..8c431e53e 100644 --- a/git/types.py +++ b/git/types.py @@ -4,12 +4,12 @@ import os import sys -from typing import Union, Any +from typing import Dict, Union, Any if sys.version_info[:2] >= (3, 8): - from typing import Final, Literal, SupportsIndex # noqa: F401 + from typing import Final, Literal, SupportsIndex, TypedDict # noqa: F401 else: - from typing_extensions import Final, Literal, SupportsIndex # noqa: F401 + from typing_extensions import Final, Literal, SupportsIndex, TypedDict # noqa: F401 if sys.version_info[:2] < (3, 9): @@ -22,3 +22,21 @@ TBD = Any Lit_config_levels = Literal['system', 'global', 'user', 'repository'] + + +class Files_TD(TypedDict): + insertions: int + deletions: int + lines: int + + +class Total_TD(TypedDict): + insertions: int + deletions: int + lines: int + files: int + + +class HSH_TD(TypedDict): + total: Total_TD + files: Dict[str, Files_TD] diff --git a/git/util.py b/git/util.py index 245f45d1f..0783918d1 100644 --- a/git/util.py +++ b/git/util.py @@ -32,7 +32,7 @@ from git.repo.base import Repo from git.config import GitConfigParser, SectionConstraint -from .types import PathLike, TBD, Literal, SupportsIndex +from .types import PathLike, Literal, SupportsIndex, HSH_TD, Files_TD # --------------------------------------------------------------------- @@ -746,7 +746,9 @@ 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]]): + from git.types import Total_TD, Files_TD + + def __init__(self, total: Total_TD, files: Dict[str, Files_TD]): self.total = total self.files = files @@ -756,13 +758,12 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': :return: git.Stat""" - # hsh: Dict[str, Dict[str, Union[int, Dict[str, int]]]] - hsh: Dict[str, Dict[str, TBD]] = {'total': {'insertions': 0, - 'deletions': 0, - 'lines': 0, - 'files': 0}, - 'files': {} - } # need typeddict? + 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 @@ -771,9 +772,9 @@ def _list_from_string(cls, repo: 'Repo', text: str) -> 'Stats': hsh['total']['deletions'] += deletions hsh['total']['lines'] += insertions + deletions hsh['total']['files'] += 1 - files_dict = {'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']) diff --git a/t.py b/t.py deleted file mode 100644 index 215c26674..000000000 --- a/t.py +++ /dev/null @@ -1,28 +0,0 @@ -import warnings - - -class Watcher(type): - def __init__(cls, name, bases, clsdict): - for base in bases: - if type(base) == Watcher: - warnings.warn(f"GitPython Iterable subclassed by {name}. " - "Iterable is deprecated due to naming clash, " - "Use IterableObj instead \n", - DeprecationWarning, - stacklevel=2) - - -class SuperClass(metaclass=Watcher): - pass - - -class SubClass0(SuperClass): - pass - - -class SubClass1(SuperClass): - print("test") - - -class normo(): - print("wooo") From fe594eb345fbefaee3b82436183d6560991724cc Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:14:13 +0100 Subject: [PATCH 0135/1849] Add T_Tre_cache TypeVar --- git/objects/tree.py | 32 ++++++++++++++++++-------------- git/types.py | 2 +- git/util.py | 6 +++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/git/objects/tree.py b/git/objects/tree.py index ec7d8e885..97a4b7485 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -20,7 +20,7 @@ # typing ------------------------------------------------- -from typing import Callable, Dict, Iterable, Iterator, List, Tuple, Type, Union, cast, TYPE_CHECKING +from typing import Callable, Dict, Generic, Iterable, Iterator, List, Tuple, Type, TypeVar, Union, cast, TYPE_CHECKING from git.types import PathLike @@ -31,13 +31,16 @@ #-------------------------------------------------------- -cmp: Callable[[int, int], int] = lambda a, b: (a > b) - (a < b) +cmp: Callable[[str, str], int] = lambda a, b: (a > b) - (a < b) __all__ = ("TreeModifier", "Tree") +T_Tree_cache = TypeVar('T_Tree_cache', bound=Union[Tuple[bytes, int, str]]) -def git_cmp(t1: 'Tree', t2: 'Tree') -> int: + +def git_cmp(t1: T_Tree_cache, t2: T_Tree_cache) -> int: a, b = t1[2], t2[2] + assert isinstance(a, str) and isinstance(b, str) # Need as mypy 9.0 cannot unpack TypeVar properly len_a, len_b = len(a), len(b) min_len = min(len_a, len_b) min_cmp = cmp(a[:min_len], b[:min_len]) @@ -48,7 +51,8 @@ def git_cmp(t1: 'Tree', t2: 'Tree') -> int: return len_a - len_b -def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: +def merge_sort(a: List[T_Tree_cache], + cmp: Callable[[T_Tree_cache, T_Tree_cache], int]) -> None: if len(a) < 2: return None @@ -83,7 +87,7 @@ def merge_sort(a: List[int], cmp: Callable[[int, int], int]) -> None: k = k + 1 -class TreeModifier(object): +class TreeModifier(Generic[T_Tree_cache], object): """A utility class providing methods to alter the underlying cache in a list-like fashion. @@ -91,10 +95,10 @@ class TreeModifier(object): the cache of a tree, will be sorted. Assuring it will be in a serializable state""" __slots__ = '_cache' - def __init__(self, cache): + def __init__(self, cache: List[T_Tree_cache]) -> None: self._cache = cache - def _index_by_name(self, name): + def _index_by_name(self, name: str) -> int: """:return: index of an item with name, or -1 if not found""" for i, t in enumerate(self._cache): if t[2] == name: @@ -104,7 +108,7 @@ def _index_by_name(self, name): return -1 #{ Interface - def set_done(self): + def set_done(self) -> 'TreeModifier': """Call this method once you are done modifying the tree information. It may be called several times, but be aware that each call will cause a sort operation @@ -114,7 +118,7 @@ def set_done(self): #} END interface #{ Mutators - def add(self, sha, mode, name, force=False): + def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeModifier': """Add the given item to the tree. If an item with the given name already exists, nothing will be done, but a ValueError will be raised if the sha and mode of the existing item do not match the one you add, unless @@ -132,7 +136,7 @@ def add(self, sha, mode, name, force=False): sha = to_bin_sha(sha) index = self._index_by_name(name) - item = (sha, mode, name) + item: T_Tree_cache = (sha, mode, name) # type: ignore ## use Typeguard from typing-extensions 3.10.0 if index == -1: self._cache.append(item) else: @@ -195,7 +199,7 @@ class Tree(IndexObject, diff.Diffable, util.Traversable, util.Serializable): def __init__(self, repo: 'Repo', binsha: bytes, mode: int = tree_id << 12, path: Union[PathLike, None] = None): super(Tree, self).__init__(repo, binsha, mode, path) - @classmethod + @ classmethod def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": @@ -261,17 +265,17 @@ def __truediv__(self, file: str) -> Union['Tree', Blob, Submodule]: """For PY3 only""" return self.join(file) - @property + @ property def trees(self) -> List['Tree']: """:return: list(Tree, ...) list of trees directly below this tree""" return [i for i in self if i.type == "tree"] - @property + @ property def blobs(self) -> List['Blob']: """:return: list(Blob, ...) list of blobs directly below this tree""" return [i for i in self if i.type == "blob"] - @property + @ property def cache(self) -> TreeModifier: """ :return: An object allowing to modify the internal cache. This can be used diff --git a/git/types.py b/git/types.py index 8c431e53e..c01ea27e1 100644 --- a/git/types.py +++ b/git/types.py @@ -39,4 +39,4 @@ class Total_TD(TypedDict): class HSH_TD(TypedDict): total: Total_TD - files: Dict[str, Files_TD] + files: Dict[PathLike, Files_TD] diff --git a/git/util.py b/git/util.py index 0783918d1..bcc634ec1 100644 --- a/git/util.py +++ b/git/util.py @@ -672,7 +672,7 @@ def _from_string(cls, string: str) -> 'Actor': @classmethod def _main_actor(cls, env_name: str, env_email: str, - config_reader: Union[None, GitConfigParser, SectionConstraint] = None) -> 'Actor': + config_reader: Union[None, 'GitConfigParser', 'SectionConstraint'] = None) -> 'Actor': actor = Actor('', '') user_id = None # We use this to avoid multiple calls to getpass.getuser() @@ -701,7 +701,7 @@ def default_name() -> str: return actor @classmethod - def committer(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = 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 @@ -748,7 +748,7 @@ class Stats(object): from git.types import Total_TD, Files_TD - def __init__(self, total: Total_TD, files: Dict[str, Files_TD]): + def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]): self.total = total self.files = files From 59c89441fb81b0f4549e4bf7ab01f4c27da54aad Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:37:41 +0100 Subject: [PATCH 0136/1849] forward ref Gitconfigparser --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index bcc634ec1..eccaa74ed 100644 --- a/git/util.py +++ b/git/util.py @@ -712,7 +712,7 @@ def committer(cls, config_reader: Union[None, 'GitConfigParser', 'SectionConstra return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod - def author(cls, config_reader: Union[None, GitConfigParser, SectionConstraint] = 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) From b72118e231c7bc42f457e2b02e0f90e8f87a5794 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:45:14 +0100 Subject: [PATCH 0137/1849] Import TypeGuard to replace casts --- git/types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/git/types.py b/git/types.py index c01ea27e1..e3b49170d 100644 --- a/git/types.py +++ b/git/types.py @@ -11,6 +11,11 @@ else: from typing_extensions import Final, Literal, SupportsIndex, TypedDict # noqa: F401 +if sys.version_info[:2] >= (3, 10): + from typing import TypeGuard # noqa: F401 +else: + from typing_extensions import TypeGuard # noqa: F401 + if sys.version_info[:2] < (3, 9): # Python >= 3.6, < 3.9 From fb3fec340f89955a4b0adfd64636d26300d22af9 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Thu, 24 Jun 2021 23:56:29 +0100 Subject: [PATCH 0138/1849] Update typing-extensions dependancy to =4.0.1,<5 -typing-extensions>=3.7.4.3;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.10" diff --git a/test-requirements.txt b/test-requirements.txt index 16dc0d2c1..ab3f86109 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,4 +5,4 @@ tox virtualenv nose gitdb>=4.0.1,<5 -typing-extensions>=3.7.4.3;python_version<"3.8" +typing-extensions>=3.7.4.3;python_version<"3.10" From a2d9011c05b0e27f1324f393e65954542544250d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 00:06:15 +0100 Subject: [PATCH 0139/1849] Add asserts and casts for T_Tree_cache --- git/objects/tree.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/git/objects/tree.py b/git/objects/tree.py index 97a4b7485..191fe27c3 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -136,7 +136,9 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeMod sha = to_bin_sha(sha) index = self._index_by_name(name) - item: T_Tree_cache = (sha, mode, name) # type: ignore ## use Typeguard from typing-extensions 3.10.0 + + assert isinstance(sha, bytes) and isinstance(mode, int) and isinstance(name, str) + item = cast(T_Tree_cache, (sha, mode, name)) # use Typeguard from typing-extensions 3.10.0 if index == -1: self._cache.append(item) else: @@ -151,14 +153,17 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeMod # END handle name exists return self - def add_unchecked(self, binsha, mode, name): + def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None: """Add the given item to the tree, its correctness is assumed, which puts the caller into responsibility to assure the input is correct. For more information on the parameters, see ``add`` :param binsha: 20 byte binary sha""" - self._cache.append((binsha, mode, name)) + assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str) + tree_cache = cast(T_Tree_cache, (binsha, mode, name)) + + self._cache.append(tree_cache) - def __delitem__(self, name): + def __delitem__(self, name: str) -> None: """Deletes an item with the given name if it exists""" index = self._index_by_name(name) if index > -1: From 0eae33d324376a0a1800e51bddf7f23a343f45a1 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:21:59 +0100 Subject: [PATCH 0140/1849] Add is_flatLiteral() Typeguard[] to remote.py --- git/remote.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index a85297c17..2bf64150f 100644 --- a/git/remote.py +++ b/git/remote.py @@ -38,7 +38,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, cast, overload -from git.types import PathLike, Literal, TBD +from git.types import PathLike, Literal, TBD, TypeGuard if TYPE_CHECKING: from git.repo.base import Repo @@ -48,8 +48,15 @@ from git.objects.tag import TagObject flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't'] + + +def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: + return inp in [' ', '!', '+', '-', '=', '*', 't'] + + # ------------------------------------------------------------- + log = logging.getLogger('git.remote') log.addHandler(logging.NullHandler()) @@ -325,7 +332,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': # parse lines control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() - control_character = cast(flagKeyLiteral, control_character) # can do this neater once 3.5 dropped + assert is_flagKeyLiteral(control_character) try: _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") From 5b0465c9bcca64c3a863a95735cc5e602946facb Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:27:22 +0100 Subject: [PATCH 0141/1849] fix assert --- git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/remote.py b/git/remote.py index 2bf64150f..748dcbbd3 100644 --- a/git/remote.py +++ b/git/remote.py @@ -332,7 +332,7 @@ def _from_line(cls, repo: 'Repo', line: str, fetch_line: str) -> 'FetchInfo': # parse lines control_character, operation, local_remote_ref, remote_local_ref_str, note = match.groups() - assert is_flagKeyLiteral(control_character) + assert is_flagKeyLiteral(control_character), f"{control_character}" try: _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") From dc8d23d3d6e735d70fd0a60641c58f6e44e17029 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:30:44 +0100 Subject: [PATCH 0142/1849] Add '?' to controlcharacter literal --- git/remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/remote.py b/git/remote.py index 748dcbbd3..e6daffe0c 100644 --- a/git/remote.py +++ b/git/remote.py @@ -47,11 +47,11 @@ from git.objects.tree import Tree from git.objects.tag import TagObject -flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't'] +flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: - return inp in [' ', '!', '+', '-', '=', '*', 't'] + return inp in [' ', '!', '+', '-', '=', '*', 't', '?'] # ------------------------------------------------------------- From 7b09003fffa8196277bcfaa9984a3e6833805a6d Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 20:52:29 +0100 Subject: [PATCH 0143/1849] replace cast()s with asserts in remote.py --- git/refs/log.py | 12 ++++++------ git/remote.py | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/git/refs/log.py b/git/refs/log.py index 363c3c5d5..f850ba24c 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -82,23 +82,23 @@ def new(cls, oldhexsha, newhexsha, actor, time, tz_offset, message): # skipcq: return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message)) @classmethod - def from_line(cls, line): + def from_line(cls, line: bytes) -> 'RefLogEntry': """:return: New RefLogEntry instance from the given revlog line. :param line: line bytes without trailing newline :raise ValueError: If line could not be parsed""" - line = line.decode(defenc) - fields = line.split('\t', 1) + line_str = line.decode(defenc) + fields = line_str.split('\t', 1) if len(fields) == 1: info, msg = fields[0], None elif len(fields) == 2: info, msg = fields else: raise ValueError("Line must have up to two TAB-separated fields." - " Got %s" % repr(line)) + " Got %s" % repr(line_str)) # END handle first split - oldhexsha = info[:40] # type: str - newhexsha = info[41:81] # type: str + oldhexsha = info[:40] + newhexsha = info[41:81] for hexsha in (oldhexsha, newhexsha): if not cls._re_hexsha_only.match(hexsha): raise ValueError("Invalid hexsha: %r" % (hexsha,)) diff --git a/git/remote.py b/git/remote.py index e6daffe0c..a6232db32 100644 --- a/git/remote.py +++ b/git/remote.py @@ -36,7 +36,7 @@ # typing------------------------------------------------------- -from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, cast, overload +from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, overload from git.types import PathLike, Literal, TBD, TypeGuard @@ -559,8 +559,8 @@ def delete_url(self, url: str, **kwargs: Any) -> 'Remote': def urls(self) -> Iterator[str]: """:return: Iterator yielding all configured URL targets on a remote as strings""" try: - # can replace cast with type assert? - remote_details = cast(str, self.repo.git.remote("get-url", "--all", self.name)) + remote_details = self.repo.git.remote("get-url", "--all", self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): yield line except GitCommandError as ex: @@ -571,14 +571,16 @@ def urls(self) -> Iterator[str]: # if 'Unknown subcommand: get-url' in str(ex): try: - remote_details = cast(str, self.repo.git.remote("show", self.name)) + remote_details = self.repo.git.remote("show", self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): if ' Push URL:' in line: yield line.split(': ')[-1] except GitCommandError as _ex: if any(msg in str(_ex) for msg in ['correct access rights', 'cannot run ssh']): # If ssh is not setup to access this repository, see issue 694 - remote_details = cast(str, self.repo.git.config('--get-all', 'remote.%s.url' % self.name)) + remote_details = self.repo.git.config('--get-all', 'remote.%s.url' % self.name) + assert isinstance(remote_details, str) for line in remote_details.split('\n'): yield line else: From aba4d9b4029373d2bccc961a23134454072936ce Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:03:17 +0100 Subject: [PATCH 0144/1849] replace cast()s with asserts in fun.py --- git/index/fun.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/git/index/fun.py b/git/index/fun.py index 10a440501..ffd109b1f 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -53,7 +53,7 @@ from typing import (Dict, IO, List, Sequence, TYPE_CHECKING, Tuple, Type, Union, cast) -from git.types import PathLike +from git.types import PathLike, TypeGuard if TYPE_CHECKING: from .base import IndexFile @@ -185,11 +185,17 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]: """:return: Key suitable to be used for the index.entries dictionary :param entry: One instance of type BaseIndexEntry or the path and the stage""" + + def is_entry_tuple(entry: Tuple) -> TypeGuard[Tuple[PathLike, int]]: + return isinstance(entry, tuple) and len(entry) == 2 + if len(entry) == 1: - entry_first = cast(BaseIndexEntry, entry[0]) # type: BaseIndexEntry + entry_first = entry[0] + assert isinstance(entry_first, BaseIndexEntry) return (entry_first.path, entry_first.stage) else: - entry = cast(Tuple[PathLike, int], tuple(entry)) + # entry = tuple(entry) + assert is_entry_tuple(entry) return entry # END handle entry From 07bfe1a60ae93d8b40c9aa01a3775f334d680daa Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:23:03 +0100 Subject: [PATCH 0145/1849] trigger checks to rurun --- git/objects/submodule/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index 0b4ce3c53..045fb47d6 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -65,7 +65,7 @@ def set_submodule(self, submodule): the first write operation begins""" self._smref = weakref.ref(submodule) - def flush_to_index(self): + def flush_to_index(self) -> None: """Flush changes in our configuration file to the index""" assert self._smref is not None # should always have a file here From 09fb2274db09e44bf3bc14da482ffa9a98659c54 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Fri, 25 Jun 2021 21:29:55 +0100 Subject: [PATCH 0146/1849] Add type to submodule to trigger checks to rurun --- git/objects/submodule/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index 045fb47d6..b4796b300 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -4,6 +4,11 @@ from io import BytesIO import weakref +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .base import Submodule + __all__ = ('sm_section', 'sm_name', 'mkhead', 'find_first_remote_branch', 'SubmoduleConfigParser') @@ -60,7 +65,7 @@ def __init__(self, *args, **kwargs): super(SubmoduleConfigParser, self).__init__(*args, **kwargs) #{ Interface - def set_submodule(self, submodule): + def set_submodule(self, submodule: 'Submodule') -> None: """Set this instance's submodule. It must be called before the first write operation begins""" self._smref = weakref.ref(submodule) From eff48b8ba25a0ea36a7286aa16d8888315eb1205 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:38:42 +0100 Subject: [PATCH 0147/1849] Import typevar in util.py --- git/objects/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 71137264a..7736a0b23 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,7 +19,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, TYPE_CHECKING, Tuple, Type, Union, cast) +from typing import (Any, Callable, Deque, Iterator, typevar, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO @@ -29,6 +29,8 @@ from .tag import TagObject from .tree import Tree from subprocess import Popen + +T_Iterableobj = typevar('T_Iterableobj', bound=T_Iterableobj) # -------------------------------------------------------------------- From 17c750a0803ae222f1cdaf3d6282a7e1b2046adb Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:40:41 +0100 Subject: [PATCH 0148/1849] flake8 fix --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 7736a0b23..79bf73aea 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -30,7 +30,7 @@ from .tree import Tree from subprocess import Popen -T_Iterableobj = typevar('T_Iterableobj', bound=T_Iterableobj) +T_Iterableobj = typevar('T_Iterableobj') # -------------------------------------------------------------------- From ff56dbbfceef2211087aed2619b7da2e42f235e4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:43:10 +0100 Subject: [PATCH 0149/1849] fix typo --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 79bf73aea..4609a80b1 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -19,7 +19,7 @@ from datetime import datetime, timedelta, tzinfo # typing ------------------------------------------------------------ -from typing import (Any, Callable, Deque, Iterator, typevar, TYPE_CHECKING, Tuple, Type, Union, cast) +from typing import (Any, Callable, Deque, Iterator, TypeVar, TYPE_CHECKING, Tuple, Type, Union, cast) if TYPE_CHECKING: from io import BytesIO, StringIO From 5d7b8ba9f2e9298496232e4ae66bd904a1d71001 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 25 Jun 2021 21:44:54 +0100 Subject: [PATCH 0150/1849] another typo --- git/objects/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 4609a80b1..8b8148a9f 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -30,7 +30,7 @@ from .tree import Tree from subprocess import Popen -T_Iterableobj = typevar('T_Iterableobj') +T_Iterableobj = TypeVar('T_Iterableobj') # -------------------------------------------------------------------- From ba5717549b32f6b5cee304fdff87cb26b3be688a Mon Sep 17 00:00:00 2001 From: Igor Lakhtenkov Date: Wed, 30 Jun 2021 12:33:33 +0300 Subject: [PATCH 0151/1849] Added clone multi_options to Submodule --- git/objects/submodule/base.py | 14 ++++++-- test/test_submodule.py | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index cbf6cd0db..f0b8babca 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -316,7 +316,8 @@ def _write_git_file_and_module_config(cls, working_tree_dir, module_abspath): #{ Edit Interface @classmethod - def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=None, env=None): + def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=None, env=None, + clone_multi_options=None): """Add a new submodule to the given repository. This will alter the index as well as the .gitmodules file, but will not create a new commit. If the submodule already exists, no matter if the configuration differs @@ -349,6 +350,8 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N and is defined in `os.environ`, value from `os.environ` will be used. If you want to unset some variable, consider providing empty string as its value. + :param clone_multi_options: A list of Clone options. Please see ``git.repo.base.Repo.clone`` + for details. :return: The newly created submodule instance :note: works atomically, such that no change will be done if the repository update fails for instance""" @@ -415,6 +418,8 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N kwargs['depth'] = depth else: raise ValueError("depth should be an integer") + if clone_multi_options: + kwargs['multi_options'] = clone_multi_options # _clone_repo(cls, repo, url, path, name, **kwargs): mrepo = cls._clone_repo(repo, url, path, name, env=env, **kwargs) @@ -449,7 +454,7 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N return sm def update(self, recursive=False, init=True, to_latest_revision=False, progress=None, dry_run=False, - force=False, keep_going=False, env=None): + force=False, keep_going=False, env=None, clone_multi_options=None): """Update the repository of this submodule to point to the checkout we point at with the binsha of this instance. @@ -480,6 +485,8 @@ def update(self, recursive=False, init=True, to_latest_revision=False, progress= and is defined in `os.environ`, value from `os.environ` will be used. If you want to unset some variable, consider providing empty string as its value. + :param clone_multi_options: list of Clone options. Please see ``git.repo.base.Repo.clone`` + for details. Only take effect with `init` option. :note: does nothing in bare repositories :note: method is definitely not atomic if recurisve is True :return: self""" @@ -546,7 +553,8 @@ def update(self, recursive=False, init=True, to_latest_revision=False, progress= progress.update(BEGIN | CLONE, 0, 1, prefix + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name)) if not dry_run: - mrepo = self._clone_repo(self.repo, self.url, self.path, self.name, n=True, env=env) + mrepo = self._clone_repo(self.repo, self.url, self.path, self.name, n=True, env=env, + multi_options=clone_multi_options) # END handle dry-run progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath) diff --git a/test/test_submodule.py b/test/test_submodule.py index eb821b54e..85191a896 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -9,6 +9,7 @@ import git from git.cmd import Git from git.compat import is_win +from git.config import GitConfigParser, cp from git.exc import ( InvalidGitRepositoryError, RepositoryDirtyError @@ -945,3 +946,68 @@ def test_depth(self, rwdir): sm_depth = 1 sm = parent.create_submodule(sm_name, sm_name, url=self._small_repo_url(), depth=sm_depth) self.assertEqual(len(list(sm.module().iter_commits())), sm_depth) + + @with_rw_directory + def test_update_clone_multi_options_argument(self, rwdir): + #Arrange + parent = git.Repo.init(osp.join(rwdir, 'parent')) + sm_name = 'foo' + sm_url = self._small_repo_url() + sm_branch = 'refs/heads/master' + sm_hexsha = git.Repo(self._small_repo_url()).head.commit.hexsha + sm = Submodule(parent, bytes.fromhex(sm_hexsha), name=sm_name, path=sm_name, url=sm_url, + branch_path=sm_branch) + + #Act + sm.update(init=True, clone_multi_options=['--config core.eol=true']) + + #Assert + sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, 'modules', sm_name, 'config')) + self.assertTrue(sm_config.get_value('core', 'eol')) + + @with_rw_directory + def test_update_no_clone_multi_options_argument(self, rwdir): + #Arrange + parent = git.Repo.init(osp.join(rwdir, 'parent')) + sm_name = 'foo' + sm_url = self._small_repo_url() + sm_branch = 'refs/heads/master' + sm_hexsha = git.Repo(self._small_repo_url()).head.commit.hexsha + sm = Submodule(parent, bytes.fromhex(sm_hexsha), name=sm_name, path=sm_name, url=sm_url, + branch_path=sm_branch) + + #Act + sm.update(init=True) + + #Assert + sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, 'modules', sm_name, 'config')) + with self.assertRaises(cp.NoOptionError): + sm_config.get_value('core', 'eol') + + @with_rw_directory + def test_add_clone_multi_options_argument(self, rwdir): + #Arrange + parent = git.Repo.init(osp.join(rwdir, 'parent')) + sm_name = 'foo' + + #Act + Submodule.add(parent, sm_name, sm_name, url=self._small_repo_url(), + clone_multi_options=['--config core.eol=true']) + + #Assert + sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, 'modules', sm_name, 'config')) + self.assertTrue(sm_config.get_value('core', 'eol')) + + @with_rw_directory + def test_add_no_clone_multi_options_argument(self, rwdir): + #Arrange + parent = git.Repo.init(osp.join(rwdir, 'parent')) + sm_name = 'foo' + + #Act + Submodule.add(parent, sm_name, sm_name, url=self._small_repo_url()) + + #Assert + sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, 'modules', sm_name, 'config')) + with self.assertRaises(cp.NoOptionError): + sm_config.get_value('core', 'eol') From 75dbf90efb5e292bac5f54700f7f0efedf3e47d5 Mon Sep 17 00:00:00 2001 From: Yobmod Date: Wed, 30 Jun 2021 18:39:17 +0100 Subject: [PATCH 0152/1849] move py.typed from setup.py to MANIFEST.INI --- MANIFEST.in | 1 + et --soft HEAD~62 | 82 + output.txt | 18691 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 18775 insertions(+), 1 deletion(-) create mode 100644 et --soft HEAD~62 create mode 100644 output.txt diff --git a/MANIFEST.in b/MANIFEST.in index f02721fc6..eac2a1514 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include README.md include VERSION include requirements.txt include test-requirements.txt +include git/py.typed recursive-include doc * recursive-exclude test * diff --git a/et --soft HEAD~62 b/et --soft HEAD~62 new file mode 100644 index 000000000..533a51344 --- /dev/null +++ b/et --soft HEAD~62 @@ -0,0 +1,82 @@ +[33m47bd0c1c9d7ef877a259c70782fef8f0d0f1a4c7[m[33m ([m[1;36mHEAD -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m pppppp +[33m4bdfa63ef2d3e24ec54dba98d636bb5bfe26b11a[m ppppp +[33mb5cc7404512440f1c16c8345b19a91e9618a81db[m pppp +[33mad668a5293bd9a4ba8d7b5de21641d81e09ceb4e[m ppp +[33m3f259be8841544b4afb3e03aa74f45db48deae26[m pp +[33md0d29024c9148972ddc5a61b8e81b46a4d2dd6f7[m p +[33m9f93e93fe38ccdeb32907649ffb8bde4944020dc[m tyofoiotg +[33m4336da76aff9460a778c0029c0e8e521a2e43795[m tyofootg +[33m67604c15e0fc91cf160ac8f54370ed290c2e93a1[m tyooollllltg +[33m54cd3f2b86c4474664783294f8f3b7a64090b53f[m tyooolqqltg +[33mb86c4d252a44d9db85c9ee747926cb7566325b5d[m tyooolltg +[33mc406cca34ba8563baa3496ecd3ee2e8021b32eb4[m tyoootg +[33m379275d4f6d0c15a6ac8ef0acae777cfead3e9ea[m tytgll +[33m60e6203ab2b9282bfb5aa52540d7150c04ea6b23[m tytg +[33mf7a4d1fe7d9e5b58d6be8998942cbe723a630efc[m tytg +[33m3f88dff46cb336c5011c0c88a2fd2ddf6600930f[m tytg +[33mb3cff0cc07a8fc221d8a8c5d0e3fea5e6c18f1ca[m type oeklllljs +[33m12788f0ef8503a15f44f393364a028cef8d3ba49[m type oeklllljs +[33m2f65d888a8f22895c9da79b51a4ac6bf718ddcc9[m type oekjs +[33m19e0503bd229d46523c78da85c9a997da19b8e95[m type oekjs +[33m6b241703b46ecda2e73bfa4a401ec4c91c4a47a3[m type oeklgpllpjs +[33m6ca3261073ba2f22c233550e75eed315416f7d4a[m type oeklgpllpjs +[33m9a0640a41db895bb155560d022ac4becc0104bbc[m type oeklgppjs +[33m2e552f7575426373132eb93ca3b09cdd389e3aff[m type oeklgppjs +[33md6c1054528ac81f29829a374eee04e76cd622d85[m type oekgppjs +[33mc0aed37318448f4eeaa7d2be4d0a6fd3092b290e[m type oekgjs +[33m57cd84b65fde0f73592113c9664705f3a3ad6b1b[m type oekjs +[33m57a8757d83eadb797a0c5bbbb73972bf320f0fcf[m type oejks +[33m78291356e11f0a7e96dbe977b0a2a2487923cabc[m type oejs +[33mf0cafdc02cd3561ebfe5eb2bd64aa931f6bdc746[m type ojdejs +[33mb5a6ebbc11dde3fc38f4bb795ae12f18d685ba9d[m type opcojjs +[33mdf16fd3d024f1cecf8389a1ae99c08ca92487c85[m type opcojdejs +[33mfe3e973258fb4491001836bd4730699a2109c05a[m type opcodejs +[33m6167858688feac893fb88b8a3504690811ca6fc1[m type opcodejs fllr +[33m1671a07d895db2bc3ddd7df5f882e69e5fc461b4[m type opcodejs frrrllr +[33m2fe49943535e43cab3c9ccc91f45c249b9a7f8e1[m type opcodejs frrrlr +[33m55edcf789d12fddf2ef0d8817e95b7ef98ba0f0f[m type opcodejs frrrr +[33m8b0492f6931b1cc42e505748e936dbc0e3303795[m type opcodes frrrr +[33md8cea4f39dc57a25128bae42bd9f2a68aeaed5fa[m type opcodes frrr +[33mee571ed31cfeeb15586fb2d3f91a673e178f8f2b[m type opcodes frr +[33me6014e40dbf08028e819e7dcef24e91e1ddf2079[m type opcodes fr +[33mf4b63c1958e8743c44d247be5b6bd1b52d0e3904[m type opcodes +[33mfb37cd2b9cd808e8b15e5c36e90b62d987c1d00a[m fix travehhhs +[33m00f4fd2b01a6a843cfc1ceb4a116b38b4acdabd3[m fix traves +[33medcda782b72b503ceb2460ddcd5a0e4998dbc6e1[m fix traversjjjes +[33mdebe00372bad46556f271e86a64ededb967c28d1[m fix traverses +[33me69c51051b6fd7a702f6b757f0ea68fcb45037c7[m fix traverse +[33md8c77302fc8ce89c35b40b3da898e81d39f6ac8d[m fix traverse subbbbocdfsb +[33m521736c82e08fb3cb8000a36dca9d04532e3ba39[m fix +[33me81abd85565bcb8b6922a629e811bf9dfdf0d0ca[m fix traverse subbbbocdfsdfb +[33m253e7be871abc949f15a99315a5fd8b1f1bc0e70[m fix traverse subbbbb +[33m1fd49bb457e35fd80963572dd0cea08f3f694430[m fix traverse subbbbb +[33mad1203946f3532ba2a76f5ab1bc76df963057ca5[m fix traverse subbbbb +[33m15117cb47d58c82843cc3a11522eeb353e760f1b[m fix traverse subbbb +[33m628f4b39d5abf81a32c0f31cea5d88dca643a966[m fix traverse subbbb +[33mb253b629648202eacb98284d2996aaf7cf2a6107[m fix traverse subbb +[33m19c1ca82ecd96b00f296d5343612b22df0f7a923[m fix traverse sub +[33m2f5d2b26de1ae290880a1cce9cd2d9745fab49c1[m fix traverse sub list +[33m48f2daff1a45c2a38073310b025e5f3700473372[m fix traverse comitted list +[33m1bb39e7bf892ce8d3d4dd9dc443c30acb06c34d3[m fix traverse comitted +[33m5365916ea915dc76ecff2c607a141467a532980c[m fix traverse comito +[33m26ef4581eb6af03f97edea56131661c2c1bafc2b[m fix traverse comit +[33mce54b5ab62fd4b558e98fe70eb3241e64912192b[m fix traverseobkl1#00 +[33m9d9e3dd1b17799850ce745e3ce098f3e3df92d58[m fix traverse comit +[33m2a7bb484201fdb881d7a660cc22465ce844fe92c[m fix traverseo1#00 +[33m3297c10984f8744b9bacae28f3efe84a49fbe376[m fix traverseo0000 +[33m46f9080b86742c1abe6aebd5d654440892b96ad8[m fix traverseo +[33m52c874e538d107618fcb21f06b154acd921aedbf[m fix traverseo +[33m367ae7572d4752cced72872488103e61033e65fc[m fix traverse +[33mc5c3538ca38054f0bd078386d2edc7d2a151859e[m fix tr +[33m11235ecbba5594bfd12cfdd001f1dd70c20c1f98[m fix t +[33m4e39b4a31bf5e5a654c6ff9aa97784e87c90670a[m rmv assert basic traverses +[33mb648e15db47d3e3b24be5013c5c50701635af0f6[m rmv assert basic traverse +[33m066326a34e7d5abe8f4ab9c40f4baa8b44e3ef89[m assert basic traverse +[33m8ffd0ae959a7260e9d5f918ae88ab61ccf1991dd[m assert lst traverse +[33mb3cfbcbce72652e61ddb19c1a6db131703bd0ef6[m asser traverse +[33m1a724ece29e0888106ad659c6a77a24314cc41ca[m rmv errrrr +[33m0c475fa885bb306b1ff8855590687ec52892a90e[m rmv errrrr +[33m701fefc01a52f7d97ed903a0887ef7b9541b26e2[m rmv errrr +[33m4da9492b835da97dad730a404f1c12218f92e0dd[m rmv errr +[33mc18937232b4a093aa87c660b20ff2d262b6ffbda[m rmv err +[33m37fa6002c0b03203758cabd7a2335fd1f559f957[m flake8 fxc diff --git a/output.txt b/output.txt new file mode 100644 index 000000000..25c8c95e5 --- /dev/null +++ b/output.txt @@ -0,0 +1,18691 @@ +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c3d33c113b1dfa4be7e3c9924fae029c178505c3 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- e3068025b64bee24efc1063aba5138708737c158 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 6ee9751d4e9e9526dbe810b280a4b95a43105ec9 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- f3e78d98e06439eea1036957796f8df9f386930f ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 141b78f42c7a3c1da1e5d605af3fc56aceb921ab ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c5e4334f38dce4cf02db5f11a6e5844f3a7c785c ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 9aaaa83c44d5d23565e982a705d483c656e6c157 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- bbf04348b0c79be2103fd3aaa746685578eb12fd ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 1578baf817c2526d29276067d2f23d28b6fab2b1 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 9db24bc7a617cf93321bb60de6af2a20efd6afc1 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 80d43111b6bb73008683ad2f5a7c6abbab3c74ed ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 86ec7573a87cd8136d7d497b99594f29e17406d3 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 02cb16916851336fcf2778d66a6d54ee15d086d7 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c5a9bbef0d2d8fc5877dab55879464a13955a341 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 369e564174bfdd592d64a027bebc3f3f41ee8f11 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 36dbe7e9b55a09c68ba179bcf2c3d3e1b7898ef3 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 6d0f693184884ffac97908397b074e208c63742b ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 040108747e2f868c61f870799a78850b792ddd0a ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 300831066507bf8b729a36a074b5c8dbc739128f ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- bea9077e8356b103289ba481a48d27e92c63ae7a ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 2c0f47b3076a84c5c32cccc95748f18c50e3d948 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 195ecc3da4a851734a853af6d739c21b44e0d7f0 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 85a45a691ad068a4a25566cc1ed26db09d46daa4 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- f2efb814d0c3720f018f01329e43d9afa11ddf54 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- cfc70fe92d42a853d4171943bde90d86061e3f3a ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 8724dfa6bf8f6de9c36d10b9b52ab8a1ea30c3b2 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 640d1506e7f259d675976e7fffcbc854d41d4246 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- d1a9a232fbde88a347935804721ec7cd08de6f65 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 4a771adc5352dd3876dd2ef3d0c52c8e803fc084 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 643e6369553759407ca433ed51a5a06b47e763d4 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- aa0ccead680443b07fd675f8b906758907bdb415 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 982eefb2008826604d54c1a6622c12240efb0961 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c1d5c614c38db015485190d65db05b0b75d171d4 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 152a60f0bd7c5b495a3ce91d0e45393879ec0477 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 14851034ab5204ddb7329eb34bb0964d3f206f2b ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- e15b57bf0bb40ccc6ecbebc5b008f7e96b436f19 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c8c696b3cf0c2441aefaef3592e2b815472f162a ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 32e4e08cf9ccfa90f0bc6d26f0c7007aeafcffeb ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 539980d51c159cb9922d0631a2994835b4243808 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- a771e102ec2238a60277e2dce68283833060fd37 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 22d2e4a0bfd4c2b83e718ead7f198234e3064929 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 1199f90c523f170c377bf7a5ca04c2ccaab67e08 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- bafb4cc0a95428cbedaaa225abdceecee7533fac ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- b8e700e952d135c6903b97f022211809338232e5 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 3e79604c8bdfc367f10a4a522c9bf548bdb3ab9a ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 83017bce083c58f9a6fb0c7b13db8eeec6859493 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 1143e1c66b8b054a2fefca2a23b1f499522ddb76 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 5385cbd969cc8727777d503a513ccee8372cd506 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 2c594195eb614a200e1abb85706ec7b8b7c91268 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 9563d27fbde02b8b2a8b0d808759cb235b4e083b ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 3470e269bcdc9091d0c5e25e7c09ce175c7cee77 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 96928d2eb3d98c475fd0737240c06bf8e5f96ad6 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 545ac2574cfb75b02e407e814e10f76bc485926d ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- b9a2ea80aa9970bbd625da4c986d29a36c405629 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- d8bf7d416f60f52335d128cf16fbba0344702e49 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- e39c8b07d1c98ddf267fbc69649ecbbe043de0fd ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- a08733d76a8254a20a28f4c3875a173dcf0ad129 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 6cc1e7e08094494916db1aadda17e03ce695d049 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 5fe80b03196b1d2421109fad5b456ba7ae4393e2 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- c1cedc5c417ddf3c2a955514dcca6fe74913259b ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 1c2dd54358dd526d1d08a8e4a977f041aff74174 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 158bc981130bfbe214190cac19da228d1f321fe1 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 8c6d952b17e63c92a060c08eac38165c6fafa869 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 2f233e2405c16e2b185aa90cc8e7ad257307b991 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- bccdb483aaa7235b85a49f2c208ee1befd2706dd ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- e9f8f159ebad405b2c08aa75f735146bb8e216ef ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- f51fe3e66d358e997f4af4e91a894a635f7cb601 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- e129846a1a5344c8d7c0abe5ec52136c3a581cce ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- abd23a37d8b93721c0e58e8c133cef26ed5ba1f0 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 723f100a422577235e06dc024a73285710770fca ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- 28d6c90782926412ba76838e7a81e60b41083290 ---- +58c78e649cbac271dee187b055335c876fcb1937 with predicate ----- ae334e4d145f6787fd08e346a925bbc09e870341 ----