diff --git a/git/config.py b/git/config.py index cc6fcfa4f..7982baa36 100644 --- a/git/config.py +++ b/git/config.py @@ -29,8 +29,6 @@ import configparser as cp -from pathlib import Path - # typing------------------------------------------------------- from typing import Any, Callable, IO, List, Dict, Sequence, TYPE_CHECKING, Tuple, Union, cast, overload @@ -330,7 +328,7 @@ def _acquire_lock(self) -> None: "Write-ConfigParsers can operate on a single file only, multiple files have been passed") # END single file check - if isinstance(self._file_or_files, (str, Path)): # cannot narrow by os._pathlike until 3.5 dropped + if isinstance(self._file_or_files, (str, os.PathLike)): file_or_files = self._file_or_files else: file_or_files = cast(IO, self._file_or_files).name diff --git a/git/index/base.py b/git/index/base.py index e2b3f8fa4..debd710f1 100644 --- a/git/index/base.py +++ b/git/index/base.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.refs.reference import Reference + import glob from io import BytesIO import os @@ -74,6 +74,7 @@ if TYPE_CHECKING: from subprocess import Popen from git.repo import Repo + from git.refs.reference import Reference StageType = int @@ -1191,7 +1192,7 @@ def handle_stderr(proc: 'Popen[bytes]', iter_checked_out_files: Iterable[PathLik assert "Should not reach this point" @default_index - def reset(self, commit: Union[Commit, Reference, str] = 'HEAD', working_tree: bool = False, + 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 diff --git a/git/objects/base.py b/git/objects/base.py index 884f96515..6bc1945cf 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -17,15 +17,12 @@ from typing import Any, TYPE_CHECKING, Optional, Union -from git.types import PathLike +from git.types import PathLike, Commit_ish 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 + # from .tree import Tree, Blob, Commit, TagObject # -------------------------------------------------------------------------- @@ -71,7 +68,7 @@ def new(cls, repo: 'Repo', id): # @ReservedAssignment return repo.rev_parse(str(id)) @classmethod - def new_from_sha(cls, repo: 'Repo', sha1: bytes) -> Union['Commit', 'TagObject', 'Tree', 'Blob']: + def new_from_sha(cls, repo: 'Repo', sha1: bytes) -> Commit_ish: """ :return: new object instance of a type appropriate to represent the given binary sha1 diff --git a/git/objects/commit.py b/git/objects/commit.py index 0b707450c..7d3ea4fa7 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -3,12 +3,10 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - from gitdb import IStream from git.util import ( hex_to_bin, Actor, - IterableObj, Stats, finalize_process ) @@ -17,8 +15,8 @@ from .tree import Tree from . import base from .util import ( - Traversable, Serializable, + TraversableIterableObj, parse_date, altz_to_utctz_str, parse_actor_and_date, @@ -36,18 +34,25 @@ from io import BytesIO import logging -from typing import List, Tuple, Union, TYPE_CHECKING + +# typing ------------------------------------------------------------------ + +from typing import Any, Iterator, List, Sequence, Tuple, Union, TYPE_CHECKING + +from git.types import PathLike if TYPE_CHECKING: from git.repo import Repo +# ------------------------------------------------------------------------ + log = logging.getLogger('git.objects.commit') log.addHandler(logging.NullHandler()) __all__ = ('Commit', ) -class Commit(base.Object, IterableObj, Diffable, Traversable, Serializable): +class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): """Wraps a git Commit object. @@ -73,7 +78,8 @@ class Commit(base.Object, IterableObj, Diffable, Traversable, Serializable): "message", "parents", "encoding", "gpgsig") _id_attribute_ = "hexsha" - def __init__(self, repo, binsha, tree=None, author=None, authored_date=None, author_tz_offset=None, + def __init__(self, repo, binsha, tree=None, author: Union[Actor, None] = None, + authored_date=None, author_tz_offset=None, committer=None, committed_date=None, committer_tz_offset=None, message=None, parents: Union[Tuple['Commit', ...], List['Commit'], None] = None, encoding=None, gpgsig=None): @@ -139,7 +145,7 @@ 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 ## cos overriding super + def _get_intermediate_items(cls, commit: 'Commit') -> Tuple['Commit', ...]: return tuple(commit.parents) @classmethod @@ -225,7 +231,9 @@ def name_rev(self): return self.repo.git.name_rev(self) @classmethod - def iter_items(cls, repo, rev, paths='', **kwargs): + def iter_items(cls, repo: 'Repo', rev, # type: ignore + paths: Union[PathLike, Sequence[PathLike]] = '', **kwargs: Any + ) -> Iterator['Commit']: """Find all commits matching the given criteria. :param repo: is the Repo @@ -245,15 +253,23 @@ def iter_items(cls, repo, rev, paths='', **kwargs): # use -- in any case, to prevent possibility of ambiguous arguments # see https://github.com/gitpython-developers/GitPython/issues/264 - args = ['--'] + + args_list: List[Union[PathLike, Sequence[PathLike]]] = ['--'] + if paths: - args.extend((paths, )) + paths_tup: Tuple[PathLike, ...] + if isinstance(paths, (str, os.PathLike)): + paths_tup = (paths, ) + else: + paths_tup = tuple(paths) + + args_list.extend(paths_tup) # END if paths - proc = repo.git.rev_list(rev, args, as_process=True, **kwargs) + proc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs) return cls._iter_from_process_or_stream(repo, proc) - def iter_parents(self, paths='', **kwargs): + def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = '', **kwargs) -> Iterator['Commit']: """Iterate _all_ parents of this commit. :param paths: @@ -269,7 +285,7 @@ def iter_parents(self, paths='', **kwargs): return self.iter_items(self.repo, self, paths, **kwargs) - @property + @ property def stats(self): """Create a git stat from changes between this commit and its first parent or from all changes done if this is the very first commit. @@ -286,7 +302,7 @@ def stats(self): text = self.repo.git.diff(self.parents[0].hexsha, self.hexsha, '--', numstat=True) return Stats._list_from_string(self.repo, text) - @classmethod + @ classmethod def _iter_from_process_or_stream(cls, repo, proc_or_stream): """Parse out commit information into a list of Commit objects We expect one-line per commit, and parse the actual commit information directly @@ -317,7 +333,7 @@ def _iter_from_process_or_stream(cls, repo, proc_or_stream): if hasattr(proc_or_stream, 'wait'): finalize_process(proc_or_stream) - @classmethod + @ classmethod def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, author=None, committer=None, author_date=None, commit_date=None): """Commit the given tree, creating a commit object. diff --git a/git/objects/output.txt b/git/objects/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index cbf6cd0db..9b6ef6eb3 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -3,6 +3,7 @@ import logging import os import stat + from unittest import SkipTest import uuid @@ -24,9 +25,9 @@ BadName ) from git.objects.base import IndexObject, Object -from git.objects.util import Traversable +from git.objects.util import TraversableIterableObj + from git.util import ( - IterableObj, join_path_native, to_native_path_linux, RemoteProgress, @@ -48,6 +49,13 @@ # typing ---------------------------------------------------------------------- +from typing import Dict, TYPE_CHECKING +from typing import Any, Iterator, Union + +from git.types import Commit_ish, PathLike + +if TYPE_CHECKING: + from git.repo import Repo # ----------------------------------------------------------------------------- @@ -64,7 +72,7 @@ class UpdateProgress(RemoteProgress): """Class providing detailed progress information to the caller who should derive from it and implement the ``update(...)`` message""" CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)] - _num_op_codes = RemoteProgress._num_op_codes + 3 + _num_op_codes: int = RemoteProgress._num_op_codes + 3 __slots__ = () @@ -79,7 +87,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, IterableObj, Traversable): +class Submodule(IndexObject, TraversableIterableObj): """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 @@ -101,7 +109,14 @@ class Submodule(IndexObject, IterableObj, Traversable): __slots__ = ('_parent_commit', '_url', '_branch_path', '_name', '__weakref__') _cache_attrs = ('path', '_url', '_branch_path') - def __init__(self, repo, binsha, mode=None, path=None, name=None, parent_commit=None, url=None, branch_path=None): + def __init__(self, repo: 'Repo', binsha: bytes, + mode: Union[int, None] = None, + path: Union[PathLike, None] = None, + name: Union[str, None] = None, + parent_commit: Union[Commit_ish, None] = None, + url: str = None, + branch_path: Union[PathLike, None] = None + ) -> None: """Initialize this instance with its attributes. We only document the ones that differ from ``IndexObject`` @@ -121,15 +136,16 @@ def __init__(self, repo, binsha, mode=None, path=None, name=None, parent_commit= if name is not None: self._name = name - def _set_cache_(self, attr): + def _set_cache_(self, attr: str) -> None: if attr in ('path', '_url', '_branch_path'): reader = self.config_reader() # default submodule values try: self.path = reader.get('path') except cp.NoSectionError as e: - raise ValueError("This submodule instance does not exist anymore in '%s' file" - % osp.join(self.repo.working_tree_dir, '.gitmodules')) from e + if self.repo.working_tree_dir is not None: + raise ValueError("This submodule instance does not exist anymore in '%s' file" + % osp.join(self.repo.working_tree_dir, '.gitmodules')) from e # end self._url = reader.get('url') # git-python extension values - optional @@ -150,33 +166,35 @@ def _get_intermediate_items(cls, item: 'Submodule') -> IterableList['Submodule'] # END handle intermediate items @classmethod - def _need_gitfile_submodules(cls, git): + def _need_gitfile_submodules(cls, git: Git) -> bool: return git.version_info[:3] >= (1, 7, 5) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Compare with another submodule""" # we may only compare by name as this should be the ID they are hashed with # Otherwise this type wouldn't be hashable # return self.path == other.path and self.url == other.url and super(Submodule, self).__eq__(other) return self._name == other._name - def __ne__(self, other): + def __ne__(self, other: object) -> bool: """Compare with another submodule for inequality""" return not (self == other) - def __hash__(self): + def __hash__(self) -> int: """Hash this instance using its logical id, not the sha""" return hash(self._name) - def __str__(self): + def __str__(self) -> str: return self._name - def __repr__(self): + def __repr__(self) -> str: return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)"\ % (type(self).__name__, self._name, self.path, self.url, self.branch_path) @classmethod - def _config_parser(cls, repo, parent_commit, read_only): + def _config_parser(cls, repo: 'Repo', + parent_commit: Union[Commit_ish, None], + read_only: bool) -> SubmoduleConfigParser: """:return: Config Parser constrained to our submodule in read or write mode :raise IOError: If the .gitmodules file cannot be found, either locally or in the repository at the given parent commit. Otherwise the exception would be delayed until the first @@ -189,8 +207,8 @@ def _config_parser(cls, repo, parent_commit, read_only): # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref pass # end handle parent_commit - - if not repo.bare and parent_matches_head: + fp_module: Union[str, BytesIO] + if not repo.bare and parent_matches_head and repo.working_tree_dir: fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file) else: assert parent_commit is not None, "need valid parent_commit in bare repositories" @@ -219,13 +237,13 @@ def _clear_cache(self): # END for each name to delete @classmethod - def _sio_modules(cls, parent_commit): + def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO: """:return: Configuration file as BytesIO - we only access it through the respective blob's data""" sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read()) sio.name = cls.k_modules_file return sio - def _config_parser_constrained(self, read_only): + def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: """:return: Config Parser constrained to our submodule in read or write mode""" try: pc = self.parent_commit @@ -248,7 +266,7 @@ def _clone_repo(cls, repo, url, path, name, **kwargs): """:return: Repo instance of newly cloned repository :param repo: our parent repository :param url: url to clone from - :param path: repository-relative path to the submodule checkout location + :param path: repository - relative path to the submodule checkout location :param name: canonical of the submodule :param kwrags: additinoal arguments given to git.clone""" module_abspath = cls._module_abspath(repo, path, name) @@ -269,7 +287,7 @@ def _clone_repo(cls, repo, url, path, name, **kwargs): @classmethod def _to_relative_path(cls, parent_repo, path): - """:return: a path guaranteed to be relative to the given parent-repository + """:return: a path guaranteed to be relative to the given parent - repository :raise ValueError: if path is not contained in the parent repository's working tree""" path = to_native_path_linux(path) if path.endswith('/'): @@ -291,11 +309,11 @@ def _to_relative_path(cls, parent_repo, path): @classmethod def _write_git_file_and_module_config(cls, working_tree_dir, module_abspath): - """Writes a .git file containing a (preferably) relative path to the actual git module repository. + """Writes a .git file containing a(preferably) relative path to the actual git module repository. It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir :note: will overwrite existing files ! :note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration - and will not roll back changes done to the git file. This should be a non-issue, but may easily be fixed + and will not roll back changes done to the git file. This should be a non - issue, but may easily be fixed if it becomes one :param working_tree_dir: directory to write the .git file into :param module_abspath: absolute path to the bare repository @@ -316,7 +334,9 @@ 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: 'Repo', name: str, path: PathLike, url: Union[str, None] = None, + branch=None, no_checkout: bool = False, depth=None, env=None + ) -> 'Submodule': """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 @@ -360,8 +380,8 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N # assure we never put backslashes into the url, as some operating systems # like it ... - if url is not None: - url = to_native_path_linux(url) + # if url is not None: + # url = to_native_path_linux(url) to_native_path_linux does nothing?? # END assure url correctness # INSTANTIATE INTERMEDIATE SM @@ -405,7 +425,7 @@ def add(cls, repo, name, path, url=None, branch=None, no_checkout=False, depth=N url = urls[0] else: # clone new repo - kwargs = {'n': no_checkout} + kwargs: Dict[str, Union[bool, int]] = {'n': no_checkout} if not branch_is_default: kwargs['b'] = br.name # END setup checkout-branch @@ -463,7 +483,7 @@ def update(self, recursive=False, init=True, to_latest_revision=False, progress= was specified for this submodule and the branch existed remotely :param progress: UpdateProgress instance or None if no progress should be shown :param dry_run: if True, the operation will only be simulated, but not performed. - All performed operations are read-only + All performed operations are read - only :param force: If True, we may reset heads even if the repository in question is dirty. Additinoally we will be allowed to set a tracking branch which is ahead of its remote branch back into the past or the location of the @@ -471,7 +491,7 @@ def update(self, recursive=False, init=True, to_latest_revision=False, progress= If False, local tracking branches that are in the future of their respective remote branches will simply not be moved. :param keep_going: if True, we will ignore but log all errors, and keep going recursively. - Unless dry_run is set as well, keep_going could cause subsequent/inherited errors you wouldn't see + Unless dry_run is set as well, keep_going could cause subsequent / inherited errors you wouldn't see otherwise. In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules :param env: Optional dictionary containing the desired environment variables. @@ -677,9 +697,9 @@ def move(self, module_path, configuration=True, module=True): adjusting our index entry accordingly. :param module_path: the path to which to move our module in the parent repostory's working tree, - given as repository-relative or absolute path. Intermediate directories will be created + given as repository - relative or absolute path. Intermediate directories will be created accordingly. If the path already exists, it must be empty. - Trailing (back)slashes are removed automatically + Trailing(back)slashes are removed automatically :param configuration: if True, the configuration will be adjusted to let the submodule point to the given path. :param module: if True, the repository managed by this submodule @@ -688,7 +708,7 @@ def move(self, module_path, configuration=True, module=True): :return: self :raise ValueError: if the module path existed and was not empty, or was a file :note: Currently the method is not atomic, and it could leave the repository - in an inconsistent state if a sub-step fails for some reason + in an inconsistent state if a sub - step fails for some reason """ if module + configuration < 1: raise ValueError("You must specify to move at least the module or the configuration of the submodule") @@ -782,19 +802,19 @@ def move(self, module_path, configuration=True, module=True): @unbare_repo def remove(self, module=True, force=False, configuration=True, dry_run=False): """Remove this submodule from the repository. This will remove our entry - from the .gitmodules file and the entry in the .git/config file. + from the .gitmodules file and the entry in the .git / config file. :param module: If True, the module checkout we point to will be deleted as well. If the module is currently on a commit which is not part of any branch in the remote, if the currently checked out branch working tree, or untracked files, - is ahead of its tracking branch, if you have modifications in the + is ahead of its tracking branch, if you have modifications in the In case the removal of the repository fails for these reasons, the submodule status will not have been altered. - If this submodule has child-modules on its own, these will be deleted + If this submodule has child - modules on its own, these will be deleted prior to touching the own module. :param force: Enforces the deletion of the module even though it contains - modifications. This basically enforces a brute-force file system based + modifications. This basically enforces a brute - force file system based deletion. :param configuration: if True, the submodule is deleted from the configuration, otherwise it isn't. Although this should be enabled most of the times, @@ -934,7 +954,7 @@ def remove(self, module=True, force=False, configuration=True, dry_run=False): return self - def set_parent_commit(self, commit, check=True): + def set_parent_commit(self, commit: Union[Commit_ish, None], check=True): """Set this instance to use the given commit whose tree is supposed to contain the .gitmodules blob. @@ -1007,9 +1027,9 @@ def rename(self, new_name): """Rename this submodule :note: This method takes care of renaming the submodule in various places, such as - * $parent_git_dir/config - * $working_tree_dir/.gitmodules - * (git >=v1.8.0: move submodule repository to new name) + * $parent_git_dir / config + * $working_tree_dir / .gitmodules + * (git >= v1.8.0: move submodule repository to new name) As .gitmodules will be changed, you would need to make a commit afterwards. The changed .gitmodules file will already be added to the index @@ -1083,7 +1103,7 @@ def module_exists(self): def exists(self): """ :return: True if the submodule exists, False otherwise. Please note that - a submodule may exist (in the .gitmodules file) even though its module + a submodule may exist ( in the .gitmodules file) even though its module doesn't exist on disk""" # keep attributes for later, and restore them if we have no valid data # this way we do not actually alter the state of the object @@ -1123,7 +1143,7 @@ def branch(self): @property def branch_path(self): """ - :return: full (relative) path as string to the branch we would checkout + :return: full(relative) path as string to the branch we would checkout from the remote and track""" return self._branch_path @@ -1136,7 +1156,7 @@ def branch_name(self): @property def url(self): - """:return: The url to the repository which our module-repository refers to""" + """:return: The url to the repository which our module - repository refers to""" return self._url @property @@ -1152,7 +1172,7 @@ def name(self): """:return: The name of this submodule. It is used to identify it within the .gitmodules file. :note: by default, the name is the path at which to find the submodule, but - in git-python it should be a unique identifier similar to the identifiers + in git - python it should be a unique identifier similar to the identifiers used for remotes, which allows to change the path of the submodule easily """ @@ -1179,17 +1199,16 @@ def children(self) -> IterableList['Submodule']: #{ Iterable Interface @classmethod - def iter_items(cls, repo, parent_commit='HEAD'): + def iter_items(cls, repo: 'Repo', parent_commit: Union[Commit_ish, str] = 'HEAD', *Args: Any, **kwargs: Any + ) -> Iterator['Submodule']: """:return: iterator yielding Submodule instances available in the given repository""" try: pc = repo.commit(parent_commit) # parent commit instance parser = cls._config_parser(repo, pc, read_only=True) except (IOError, BadName): - return + return iter([]) # END handle empty iterator - rt = pc.tree # root tree - for sms in parser.sections(): n = sm_name(sms) p = parser.get(sms, 'path') @@ -1202,6 +1221,7 @@ def iter_items(cls, repo, parent_commit='HEAD'): # get the binsha index = repo.index try: + rt = pc.tree # root tree sm = rt[p] except KeyError: # try the index, maybe it was just added diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index b4796b300..5290000be 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -4,10 +4,12 @@ from io import BytesIO import weakref -from typing import TYPE_CHECKING + +from typing import Any, TYPE_CHECKING, Union if TYPE_CHECKING: from .base import Submodule + from weakref import ReferenceType __all__ = ('sm_section', 'sm_name', 'mkhead', 'find_first_remote_branch', 'SubmoduleConfigParser') @@ -58,8 +60,8 @@ class SubmoduleConfigParser(GitConfigParser): Please note that no mutating method will work in bare mode """ - def __init__(self, *args, **kwargs): - self._smref = None + def __init__(self, *args: Any, **kwargs: Any) -> None: + self._smref: Union['ReferenceType[Submodule]', None] = None self._index = None self._auto_write = True super(SubmoduleConfigParser, self).__init__(*args, **kwargs) @@ -89,7 +91,7 @@ def flush_to_index(self) -> None: #} END interface #{ Overridden Methods - def write(self): + def write(self) -> None: rval = super(SubmoduleConfigParser, self).write() self.flush_to_index() return rval diff --git a/git/objects/tree.py b/git/objects/tree.py index 191fe27c3..44d3b3da9 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 git.util import join_path import git.diff as diff from git.util import to_bin_sha @@ -20,14 +21,18 @@ # typing ------------------------------------------------- -from typing import Callable, Dict, Generic, Iterable, Iterator, List, Tuple, Type, TypeVar, 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 +from git.types import PathLike, TypeGuard if TYPE_CHECKING: from git.repo import Repo + from git.objects.util import TraversedTup from io import BytesIO +T_Tree_cache = TypeVar('T_Tree_cache', bound=Union[Tuple[bytes, int, str]]) + #-------------------------------------------------------- @@ -35,8 +40,6 @@ __all__ = ("TreeModifier", "Tree") -T_Tree_cache = TypeVar('T_Tree_cache', bound=Union[Tuple[bytes, int, str]]) - def git_cmp(t1: T_Tree_cache, t2: T_Tree_cache) -> int: a, b = t1[2], t2[2] @@ -137,8 +140,12 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> 'TreeMod sha = to_bin_sha(sha) index = self._index_by_name(name) - 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 + def is_tree_cache(inp: Tuple[bytes, int, str]) -> TypeGuard[T_Tree_cache]: + return isinstance(inp[0], bytes) and isinstance(inp[1], int) and isinstance([inp], str) + + item = (sha, mode, name) + assert is_tree_cache(item) + if index == -1: self._cache.append(item) else: @@ -205,7 +212,7 @@ def __init__(self, repo: 'Repo', binsha: bytes, mode: int = tree_id << 12, path: super(Tree, self).__init__(repo, binsha, mode, path) @ classmethod - def _get_intermediate_items(cls, index_object: 'Tree', # type: ignore + def _get_intermediate_items(cls, index_object: 'Tree', ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": index_object = cast('Tree', index_object) @@ -289,14 +296,37 @@ def cache(self) -> TreeModifier: See the ``TreeModifier`` for more information on how to alter the cache""" return TreeModifier(self._cache) - def traverse(self, predicate=lambda i, d: True, - prune=lambda i, d: False, depth=-1, branch_first=True, - visit_once=False, ignore_self=1): - """For documentation, see util.Traversable.traverse + def traverse(self, + predicate: Callable[[Union['Tree', 'Submodule', 'Blob', + 'TraversedTup'], int], bool] = lambda i, d: True, + prune: Callable[[Union['Tree', 'Submodule', 'Blob', 'TraversedTup'], int], bool] = lambda i, d: False, + depth: int = -1, + branch_first: bool = True, + visit_once: bool = False, + ignore_self: int = 1, + as_edge: bool = False + ) -> Union[Iterator[Union['Tree', 'Blob', 'Submodule']], + Iterator[Tuple[Union['Tree', 'Submodule', None], Union['Tree', 'Blob', 'Submodule']]]]: + """For documentation, see util.Traversable._traverse() Trees are set to visit_once = False to gain more performance in the traversal""" - return super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self) + + # """ + # # To typecheck instead of using cast. + # import itertools + # def is_tree_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Union['Tree', 'Blob', 'Submodule']]]]: + # return all(isinstance(x, (Blob, Tree, Submodule)) for x in inp[1]) + + # ret = super(Tree, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self) + # ret_tup = itertools.tee(ret, 2) + # assert is_tree_traversed(ret_tup), f"Type is {[type(x) for x in list(ret_tup[0])]}" + # return ret_tup[0]""" + return cast(Union[Iterator[Union['Tree', 'Blob', 'Submodule']], + Iterator[Tuple[Union['Tree', 'Submodule', None], Union['Tree', 'Blob', 'Submodule']]]], + super(Tree, self).traverse(predicate, prune, depth, # type: ignore + branch_first, visit_once, ignore_self)) # List protocol + def __getslice__(self, i: int, j: int) -> List[Union[Blob, 'Tree', Submodule]]: return list(self._iter_convert_to_object(self._cache[i:j])) diff --git a/git/objects/util.py b/git/objects/util.py index 8b8148a9f..24511652c 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -7,6 +7,7 @@ from git.util import ( IterableList, + IterableObj, Actor ) @@ -19,18 +20,22 @@ 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, NamedTuple, overload, Sequence, + TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast) + +from git.types import Literal if TYPE_CHECKING: from io import BytesIO, StringIO - from .submodule.base import Submodule # noqa: F401 from .commit import Commit from .blob import Blob from .tag import TagObject from .tree import Tree from subprocess import Popen - -T_Iterableobj = TypeVar('T_Iterableobj') + + +T_TIobj = TypeVar('T_TIobj', bound='TraversableIterableObj') # for TraversableIterableObj.traverse() +TraversedTup = Tuple[Union['Traversable', None], Union['Traversable', 'Blob']] # for Traversable.traverse() # -------------------------------------------------------------------- @@ -287,7 +292,7 @@ class Traversable(object): __slots__ = () @classmethod - def _get_intermediate_items(cls, item): + def _get_intermediate_items(cls, item) -> Sequence['Traversable']: """ Returns: Tuple of items connected to the given item. @@ -299,23 +304,34 @@ class Tree:: (cls, Tree) -> Tuple[Tree, ...] """ raise NotImplementedError("To be implemented in subclass") - def list_traverse(self, *args: Any, **kwargs: Any) -> IterableList: + def list_traverse(self, *args: Any, **kwargs: Any + ) -> Union[IterableList['TraversableIterableObj'], + IterableList[Tuple[Union[None, 'TraversableIterableObj'], 'TraversableIterableObj']]]: """ :return: IterableList with the results of the traversal as produced by - traverse()""" - out: IterableList = IterableList(self._id_attribute_) # type: ignore[attr-defined] # defined in sublcasses - out.extend(self.traverse(*args, **kwargs)) + traverse() + List objects must be IterableObj and Traversable e.g. Commit, Submodule""" + + out: Union[IterableList['TraversableIterableObj'], + IterableList[Tuple[Union[None, 'TraversableIterableObj'], 'TraversableIterableObj']]] + + # def is_TraversableIterableObj(inp: Union['Traversable', IterableObj]) -> TypeGuard['TraversableIterableObj']: + # return isinstance(self, TraversableIterableObj) + # assert is_TraversableIterableObj(self), f"{type(self)}" + + self = cast('TraversableIterableObj', self) + out = IterableList(self._id_attribute_) + out.extend(self.traverse(*args, **kwargs)) # type: ignore return out 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']]]: + predicate: Callable[[Union['Traversable', TraversedTup], int], bool] = lambda i, d: True, + prune: Callable[[Union['Traversable', TraversedTup], 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[Union['Traversable', 'Blob']], + Iterator[TraversedTup]]: """: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 :param prune: @@ -344,21 +360,37 @@ def traverse(self, if True, return a pair of items, first being the source, second the destination, i.e. tuple(src, dest) with the edge spanning from source to destination""" + + """ + Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] + Submodule -> Iterator[Submodule, Tuple[Submodule, Submodule]] + Tree -> Iterator[Union[Blob, Tree, Submodule, + Tuple[Union[Submodule, Tree], Union[Blob, Tree, Submodule]]] + + ignore_self=True is_edge=True -> Iterator[item] + ignore_self=True is_edge=False --> Iterator[item] + ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]] + ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]""" + class TraverseNT(NamedTuple): + depth: int + item: 'Traversable' + src: Union['Traversable', None] + visited = set() - stack = deque() # type: Deque[Tuple[int, Traversable, Union[Traversable, None]]] - stack.append((0, self, None)) # self is always depth level 0 + stack = deque() # type: Deque[TraverseNT] + stack.append(TraverseNT(0, self, None)) # self is always depth level 0 - def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None]]], - item: 'Traversable', + def addToStack(stack: Deque[TraverseNT], + src_item: 'Traversable', branch_first: bool, - depth) -> None: + depth: int) -> None: lst = self._get_intermediate_items(item) - if not lst: + if not lst: # empty list return None if branch_first: - stack.extendleft((depth, i, item) for i in lst) + stack.extendleft(TraverseNT(depth, i, src_item) for i in lst) else: - reviter = ((depth, lst[i], item) for i in range(len(lst) - 1, -1, -1)) + reviter = (TraverseNT(depth, lst[i], src_item) for i in range(len(lst) - 1, -1, -1)) stack.extend(reviter) # END addToStack local method @@ -371,7 +403,12 @@ def addToStack(stack: Deque[Tuple[int, 'Traversable', Union['Traversable', None] if visit_once: visited.add(item) - rval = (as_edge and (src, item)) or item + rval: Union['Traversable', Tuple[Union[None, 'Traversable'], 'Traversable']] + if as_edge: # if as_edge return (src, item) unless rrc is None (e.g. for first item) + rval = (src, item) + else: + rval = item + if prune(rval, d): continue @@ -405,3 +442,73 @@ def _deserialize(self, stream: 'BytesIO') -> 'Serializable': :param stream: a file-like object :return: self""" raise NotImplementedError("To be implemented in subclass") + + +class TraversableIterableObj(Traversable, IterableObj): + __slots__ = () + + TIobj_tuple = Tuple[Union[T_TIobj, None], T_TIobj] + + @overload # type: ignore + def traverse(self: T_TIobj, + predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool], + prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool], + depth: int, branch_first: bool, visit_once: bool, + ignore_self: Literal[True], + as_edge: Literal[False], + ) -> Iterator[T_TIobj]: + ... + + @overload + def traverse(self: T_TIobj, + predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool], + prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], bool], + depth: int, branch_first: bool, visit_once: bool, + ignore_self: Literal[False], + as_edge: Literal[True], + ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: + ... + + @overload + def traverse(self: T_TIobj, + predicate: Callable[[Union[T_TIobj, TIobj_tuple], int], bool], + prune: Callable[[Union[T_TIobj, TIobj_tuple], int], bool], + depth: int, branch_first: bool, visit_once: bool, + ignore_self: Literal[True], + as_edge: Literal[True], + ) -> Iterator[Tuple[T_TIobj, T_TIobj]]: + ... + + def traverse(self: T_TIobj, + predicate: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], int], + bool] = lambda i, d: True, + prune: Callable[[Union[T_TIobj, Tuple[Union[T_TIobj, None], T_TIobj]], 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[T_TIobj], + Iterator[Tuple[T_TIobj, T_TIobj]], + Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]]: + """For documentation, see util.Traversable._traverse()""" + + """ + # To typecheck instead of using cast. + import itertools + from git.types import TypeGuard + def is_commit_traversed(inp: Tuple) -> TypeGuard[Tuple[Iterator[Tuple['Commit', 'Commit']]]]: + for x in inp[1]: + if not isinstance(x, tuple) and len(x) != 2: + if all(isinstance(inner, Commit) for inner in x): + continue + return True + + ret = super(Commit, self).traverse(predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge) + ret_tup = itertools.tee(ret, 2) + assert is_commit_traversed(ret_tup), f"{[type(x) for x in list(ret_tup[0])]}" + return ret_tup[0] + """ + return cast(Union[Iterator[T_TIobj], + Iterator[Tuple[Union[None, T_TIobj], T_TIobj]]], + super(TraversableIterableObj, self).traverse( + predicate, prune, depth, branch_first, visit_once, ignore_self, as_edge # type: ignore + )) diff --git a/git/refs/head.py b/git/refs/head.py index cc8385908..c698004dc 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -5,6 +5,9 @@ from .symbolic import SymbolicReference from .reference import Reference +from typing import Union +from git.types import Commit_ish + __all__ = ["HEAD", "Head"] @@ -12,7 +15,7 @@ def strip_quotes(string): if string.startswith('"') and string.endswith('"'): return string[1:-1] return string - + class HEAD(SymbolicReference): @@ -33,7 +36,7 @@ def orig_head(self): to contain the previous value of HEAD""" return SymbolicReference(self.repo, self._ORIG_HEAD_NAME) - def reset(self, commit='HEAD', index=True, working_tree=False, + def reset(self, commit: Union[Commit_ish, SymbolicReference, str] = 'HEAD', index=True, working_tree=False, paths=None, **kwargs): """Reset our HEAD to the given commit optionally synchronizing the index and working tree. The reference we refer to will be set to @@ -60,6 +63,7 @@ def reset(self, commit='HEAD', index=True, working_tree=False, Additional arguments passed to git-reset. :return: self""" + mode: Union[str, None] mode = "--soft" if index: mode = "--mixed" diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 64a6591aa..ca0691d92 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -1,7 +1,8 @@ import os from git.compat import defenc -from git.objects import Object, Commit +from git.objects import Object +from git.objects.commit import Commit from git.util import ( join_path, join_path_native, @@ -19,7 +20,6 @@ from .log import RefLog - __all__ = ["SymbolicReference"] diff --git a/git/remote.py b/git/remote.py index a6232db32..a036446ee 100644 --- a/git/remote.py +++ b/git/remote.py @@ -38,14 +38,12 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Union, overload -from git.types import PathLike, Literal, TBD, TypeGuard +from git.types import PathLike, Literal, TBD, TypeGuard, Commit_ish 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 + # from git.objects.commit import Commit + # from git.objects import Blob, Tree, TagObject flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] @@ -154,7 +152,7 @@ def __init__(self, flags: int, local_ref: Union[SymbolicReference, None], remote self.summary = summary @property - def old_commit(self) -> Union[str, SymbolicReference, 'Commit', 'TagObject', 'Blob', 'Tree', None]: + def old_commit(self) -> Union[str, SymbolicReference, 'Commit_ish', None]: return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None @property @@ -284,7 +282,7 @@ def refresh(cls) -> Literal[True]: return True def __init__(self, ref: SymbolicReference, flags: int, note: str = '', - old_commit: Union['Commit', TagReference, 'Tree', 'Blob', None] = None, + old_commit: Union[Commit_ish, None] = None, remote_ref_path: Optional[PathLike] = None) -> None: """ Initialize a new instance @@ -304,7 +302,7 @@ def name(self) -> str: return self.ref.name @property - def commit(self) -> 'Commit': + def commit(self) -> Commit_ish: """:return: Commit of our remote ref""" return self.ref.commit @@ -349,7 +347,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 # type: Union[Commit, TagReference, Tree, Blob, None] + old_commit = None # type: Union[Commit_ish, None] is_tag_operation = False if 'rejected' in operation: flags |= cls.REJECTED diff --git a/git/repo/base.py b/git/repo/base.py index 52727504b..fd20deed3 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -36,7 +36,7 @@ # typing ------------------------------------------------------ -from git.types import TBD, PathLike, Lit_config_levels +from git.types import TBD, PathLike, Lit_config_levels, Commit_ish, Tree_ish from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, Sequence, TextIO, Tuple, Type, Union, @@ -45,7 +45,7 @@ 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 + from git.objects import Tree # ----------------------------------------------------------- @@ -515,8 +515,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']: + def commit(self, rev: Optional[str] = None + ) -> Commit: """The Commit object for the specified revision :param rev: revision specifier, see git-rev-parse for viable options. @@ -531,7 +531,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', str, None] = None) -> 'Tree': + def tree(self, rev: Union[Tree_ish, str, None] = None) -> 'Tree': """The Tree object for the given treeish revision Examples:: @@ -574,7 +574,7 @@ def iter_commits(self, rev: Optional[TBD] = None, paths: Union[PathLike, Sequenc return Commit.iter_items(self, rev, paths, **kwargs) def merge_base(self, *rev: TBD, **kwargs: Any - ) -> List[Union['SymbolicReference', Commit, 'TagObject', 'Blob', 'Tree', None]]: + ) -> List[Union['SymbolicReference', Commit_ish, 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. @@ -587,7 +587,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_ish, None]] try: lines = self.git.merge_base(*rev, **kwargs).splitlines() # List[str] except GitCommandError as err: @@ -1159,7 +1159,7 @@ 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_ish, None]: """ :return: The commit which is currently being replayed while rebasing. diff --git a/git/types.py b/git/types.py index e3b49170d..ea91d038b 100644 --- a/git/types.py +++ b/git/types.py @@ -4,7 +4,7 @@ import os import sys -from typing import Dict, Union, Any +from typing import Dict, Union, Any, TYPE_CHECKING if sys.version_info[:2] >= (3, 8): from typing import Final, Literal, SupportsIndex, TypedDict # noqa: F401 @@ -24,8 +24,15 @@ # os.PathLike only becomes subscriptable from Python 3.9 onwards PathLike = Union[str, 'os.PathLike[str]'] # forward ref as pylance complains unless editing with py3.9+ +if TYPE_CHECKING: + from git.objects import Commit, Tree, TagObject, Blob + # from git.refs import SymbolicReference + TBD = Any +Tree_ish = Union['Commit', 'Tree'] +Commit_ish = Union['Commit', 'TagObject', 'Blob', 'Tree'] + Lit_config_levels = Literal['system', 'global', 'user', 'repository'] diff --git a/git/util.py b/git/util.py index eccaa74ed..c7c8d07f9 100644 --- a/git/util.py +++ b/git/util.py @@ -4,6 +4,9 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php +from .exc import InvalidGitRepositoryError +import os.path as osp +from .compat import is_win import contextlib from functools import wraps import getpass @@ -20,9 +23,11 @@ from urllib.parse import urlsplit, urlunsplit import warnings +# from git.objects.util import Traversable + # typing --------------------------------------------------------- -from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterator, List, +from typing import (Any, AnyStr, BinaryIO, Callable, Dict, Generator, IO, Iterable as typIter, Iterator, List, Optional, Pattern, Sequence, Tuple, TypeVar, Union, cast, TYPE_CHECKING, overload) import pathlib @@ -32,8 +37,11 @@ from git.repo.base import Repo from git.config import GitConfigParser, SectionConstraint -from .types import PathLike, Literal, SupportsIndex, HSH_TD, Files_TD +from .types import PathLike, Literal, SupportsIndex, HSH_TD, Total_TD, Files_TD + +T_IterableObj = TypeVar('T_IterableObj', bound=Union['IterableObj', typIter], covariant=True) +# So IterableList[Head] is subtype of IterableList[IterableObj] # --------------------------------------------------------------------- @@ -49,11 +57,6 @@ hex_to_bin, # @UnusedImport ) -from .compat import is_win -import os.path as osp - -from .exc import InvalidGitRepositoryError - # NOTE: Some of the unused imports might be used/imported by others. # Handle once test-cases are back up and running. @@ -181,6 +184,7 @@ def to_native_path_linux(path: PathLike) -> PathLike: # no need for any work on linux def to_native_path_linux(path: PathLike) -> PathLike: return path + to_native_path = to_native_path_linux @@ -433,7 +437,7 @@ class RemoteProgress(object): Handler providing an interface to parse progress information emitted by git-push and git-fetch and to dispatch callbacks allowing subclasses to react to the progress. """ - _num_op_codes = 9 + _num_op_codes: int = 9 BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT = \ [1 << x for x in range(_num_op_codes)] STAGE_MASK = BEGIN | END @@ -746,8 +750,6 @@ class Stats(object): files = number of changed files as int""" __slots__ = ("total", "files") - from git.types import Total_TD, Files_TD - def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]): self.total = total self.files = files @@ -931,10 +933,7 @@ def _obtain_lock(self) -> None: # END endless loop -T = TypeVar('T', bound='IterableObj') - - -class IterableList(List[T]): +class IterableList(List[T_IterableObj]): """ List of iterable objects allowing to query an object by id or by named index:: @@ -1046,7 +1045,7 @@ class Iterable(object): @classmethod def list_items(cls, repo, *args, **kwargs): """ - Deprecaated, use IterableObj instead. + Deprecated, use IterableObj instead. Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional arguments arg given. @@ -1068,12 +1067,15 @@ def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any): class IterableObj(): """Defines an interface for iterable items which is to assure a uniform - way to retrieve and iterate items within the git repository""" + way to retrieve and iterate items within the git repository + + Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]""" + __slots__ = () _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod - def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T]: + def list_items(cls, repo: 'Repo', *args: Any, **kwargs: Any) -> IterableList[T_IterableObj]: """ Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional @@ -1087,7 +1089,8 @@ 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[T]: + def iter_items(cls, repo: 'Repo', *args: Any, **kwargs: Any + ) -> Iterator[T_IterableObj]: # return typed to be compatible with subtypes e.g. Remote """For more information about the arguments, see list_items :return: iterator yielding Items""" diff --git a/test/test_commit.py b/test/test_commit.py index 2fe80530d..34b91ac7b 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -179,6 +179,19 @@ def test_traversal(self): # at some point, both iterations should stop self.assertEqual(list(bfirst)[-1], first) + + stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(ignore_self=0, + as_edge=True) + stoptraverse_list = list(stoptraverse) + for itemtup in stoptraverse_list: + self.assertIsInstance(itemtup, (tuple)) and self.assertEqual(len(itemtup), 2) # as_edge=True -> tuple + src, item = itemtup + self.assertIsInstance(item, Commit) + if src: + self.assertIsInstance(src, Commit) + else: + self.assertIsNone(src) # ignore_self=0 -> first is (None, Commit) + stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(as_edge=True) self.assertEqual(len(next(stoptraverse)), 2) diff --git a/test/test_tree.py b/test/test_tree.py index 49b34c5e7..0607d8e3c 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -8,7 +8,7 @@ import sys from unittest import skipIf -from git import ( +from git.objects import ( Tree, Blob )