diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8581c0bfc..dd94ab9d5 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, "3.10.0-beta.4"] + python-version: [3.7, 3.8, 3.9, "3.10.0-beta.4"] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 5087dbccb..dd449d32f 100644 --- a/README.md +++ b/README.md @@ -24,23 +24,21 @@ or low-level like git-plumbing. It provides abstractions of git objects for easy access of repository data, and additionally allows you to access the git repository more directly using either a pure python implementation, -or the faster, but more resource intensive *git command* implementation. +or the faster, but more resource intensive _git command_ implementation. The object database implementation is optimized for handling large quantities of objects and large datasets, which is achieved by using low-level structures and data streaming. - ### DEVELOPMENT STATUS This project is in **maintenance mode**, which means that -* …there will be no feature development, unless these are contributed -* …there will be no bug fixes, unless they are relevant to the safety of users, or contributed -* …issues will be responded to with waiting times of up to a month +- …there will be no feature development, unless these are contributed +- …there will be no bug fixes, unless they are relevant to the safety of users, or contributed +- …issues will be responded to with waiting times of up to a month The project is open to contributions of all kinds, as well as new maintainers. - ### REQUIREMENTS GitPython needs the `git` executable to be installed on the system and available @@ -48,8 +46,8 @@ in your `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=<path/to/git>` environment variable. -* Git (1.7.x or newer) -* Python >= 3.6 +- Git (1.7.x or newer) +- Python >= 3.7 The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. @@ -98,20 +96,20 @@ See [Issue #525](https://github.com/gitpython-developers/GitPython/issues/525). ### RUNNING TESTS -*Important*: Right after cloning this repository, please be sure to have executed +_Important_: Right after cloning this repository, please be sure to have executed the `./init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures. -On *Windows*, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` +On _Windows_, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe` exists in `Git\mingw64\libexec\git-core\`; CYGWIN has no daemon, but should get along fine with MINGW's. -Ensure testing libraries are installed. -In the root directory, run: `pip install -r test-requirements.txt` +Ensure testing libraries are installed. +In the root directory, run: `pip install -r test-requirements.txt` To lint, run: `flake8` -To typecheck, run: `mypy -p git` +To typecheck, run: `mypy -p git` To test, run: `pytest` @@ -119,36 +117,35 @@ Configuration for flake8 is in the ./.flake8 file. Configurations for mypy, pytest and coverage.py are in ./pyproject.toml. -The same linting and testing will also be performed against different supported python versions +The same linting and testing will also be performed against different supported python versions upon submitting a pull request (or on each push if you have a fork with a "main" branch and actions enabled). - ### Contributions Please have a look at the [contributions file][contributing]. ### INFRASTRUCTURE -* [User Documentation](http://gitpython.readthedocs.org) -* [Questions and Answers](http://stackexchange.com/filters/167317/gitpython) - * Please post on stackoverflow and use the `gitpython` tag -* [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues) - * Post reproducible bugs and feature requests as a new issue. +- [User Documentation](http://gitpython.readthedocs.org) +- [Questions and Answers](http://stackexchange.com/filters/167317/gitpython) +- Please post on stackoverflow and use the `gitpython` tag +- [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues) + - Post reproducible bugs and feature requests as a new issue. Please be sure to provide the following information if posting bugs: - * GitPython version (e.g. `import git; git.__version__`) - * Python version (e.g. `python --version`) - * The encountered stack-trace, if applicable - * Enough information to allow reproducing the issue + - GitPython version (e.g. `import git; git.__version__`) + - Python version (e.g. `python --version`) + - The encountered stack-trace, if applicable + - Enough information to allow reproducing the issue ### How to make a new release -* Update/verify the **version** in the `VERSION` file -* Update/verify that the `doc/source/changes.rst` changelog file was updated -* Commit everything -* Run `git tag -s <version>` to tag the version in Git -* Run `make release` -* Close the milestone mentioned in the _changelog_ and create a new one. _Do not reuse milestones by renaming them_. -* set the upcoming version in the `VERSION` file, usually be +- Update/verify the **version** in the `VERSION` file +- Update/verify that the `doc/source/changes.rst` changelog file was updated +- Commit everything +- Run `git tag -s <version>` to tag the version in Git +- Run `make release` +- Close the milestone mentioned in the _changelog_ and create a new one. _Do not reuse milestones by renaming them_. +- set the upcoming version in the `VERSION` file, usually be incrementing the patch level, and possibly by appending `-dev`. Probably you want to `git push` once more. @@ -200,22 +197,22 @@ gpg --edit-key 4C08421980C9 ### Projects using GitPython -* [PyDriller](https://github.com/ishepard/pydriller) -* [Kivy Designer](https://github.com/kivy/kivy-designer) -* [Prowl](https://github.com/nettitude/Prowl) -* [Python Taint](https://github.com/python-security/pyt) -* [Buster](https://github.com/axitkhurana/buster) -* [git-ftp](https://github.com/ezyang/git-ftp) -* [Git-Pandas](https://github.com/wdm0006/git-pandas) -* [PyGitUp](https://github.com/msiemens/PyGitUp) -* [PyJFuzz](https://github.com/mseclab/PyJFuzz) -* [Loki](https://github.com/Neo23x0/Loki) -* [Omniwallet](https://github.com/OmniLayer/omniwallet) -* [GitViper](https://github.com/BeayemX/GitViper) -* [Git Gud](https://github.com/bthayer2365/git-gud) +- [PyDriller](https://github.com/ishepard/pydriller) +- [Kivy Designer](https://github.com/kivy/kivy-designer) +- [Prowl](https://github.com/nettitude/Prowl) +- [Python Taint](https://github.com/python-security/pyt) +- [Buster](https://github.com/axitkhurana/buster) +- [git-ftp](https://github.com/ezyang/git-ftp) +- [Git-Pandas](https://github.com/wdm0006/git-pandas) +- [PyGitUp](https://github.com/msiemens/PyGitUp) +- [PyJFuzz](https://github.com/mseclab/PyJFuzz) +- [Loki](https://github.com/Neo23x0/Loki) +- [Omniwallet](https://github.com/OmniLayer/omniwallet) +- [GitViper](https://github.com/BeayemX/GitViper) +- [Git Gud](https://github.com/bthayer2365/git-gud) ### LICENSE -New BSD License. See the LICENSE file. +New BSD License. See the LICENSE file. [contributing]: https://github.com/gitpython-developers/GitPython/blob/master/CONTRIBUTING.md diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 956a36073..4f22a0942 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,15 +13,17 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.6 +* `Python`_ >= 3.7 * `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. * `GitDB`_ - a pure python git database implementation +* `typing_extensions`_ >= 3.7.3.4 (if python < 3.10) .. _Python: https://www.python.org .. _Git: https://git-scm.com/ .. _GitDB: https://pypi.python.org/pypi/gitdb +.. _typing_extensions: https://pypi.org/project/typing-extensions/ Installing GitPython ==================== @@ -60,7 +62,7 @@ Leakage of System Resources --------------------------- GitPython is not suited for long-running processes (like daemons) as it tends to -leak system resources. It was written in a time where destructors (as implemented +leak system resources. It was written in a time where destructors (as implemented in the `__del__` method) still ran deterministically. In case you still want to use it in such a context, you will want to search the diff --git a/git/cmd.py b/git/cmd.py index f82127453..b84c43df3 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -42,7 +42,7 @@ from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, Iterator, List, Mapping, Sequence, TYPE_CHECKING, TextIO, Tuple, 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 @@ -68,7 +68,7 @@ # Documentation ## @{ -def handle_process_output(process: subprocess.Popen, +def handle_process_output(process: Union[subprocess.Popen, 'Git.AutoInterrupt'], stdout_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None], @@ -77,7 +77,7 @@ def handle_process_output(process: subprocess.Popen, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]], finalizer: Union[None, - Callable[[subprocess.Popen], None]] = None, + Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], 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. @@ -165,7 +165,7 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc ## 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 # type: ignore[attr-defined] - if is_win else 0) + if is_win else 0) # mypy error if not windows class Git(LazyMixin): @@ -421,15 +421,15 @@ def __getattr__(self, attr: str) -> Any: return getattr(self.proc, attr) # TODO: Bad choice to mimic `proc.wait()` but with different args. - def wait(self, stderr: Union[None, bytes] = b'') -> int: + def wait(self, stderr: Union[None, str, 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. :warn: may deadlock if output or error pipes are used and not handled separately. :raise GitCommandError: if the return status is not 0""" if stderr is None: - stderr = b'' - stderr = force_bytes(data=stderr, encoding='utf-8') + stderr_b = b'' + stderr_b = force_bytes(data=stderr, encoding='utf-8') if self.proc is not None: status = self.proc.wait() @@ -437,11 +437,11 @@ def wait(self, stderr: Union[None, bytes] = b'') -> int: def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: if stream: try: - return stderr + force_bytes(stream.read()) + return stderr_b + force_bytes(stream.read()) except ValueError: - return stderr or b'' + return stderr_b or b'' else: - return stderr or b'' + return stderr_b or b'' if status != 0: errstr = read_all_from_possibly_closed_stream(self.proc.stderr) @@ -575,8 +575,8 @@ def __init__(self, working_dir: Union[None, PathLike] = None): self._environment: Dict[str, str] = {} # cached command slots - self.cat_file_header = None - self.cat_file_all = None + self.cat_file_header: Union[None, TBD] = None + self.cat_file_all: Union[None, TBD] = None def __getattr__(self, name: str) -> Any: """A convenience method as it allows to call the command as if it was @@ -1012,17 +1012,14 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any @classmethod def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]: - if not isinstance(arg_list, (list, tuple)): - return [str(arg_list)] outlist = [] - for arg in arg_list: - if isinstance(arg_list, (list, tuple)): + if isinstance(arg_list, (list, tuple)): + for arg in arg_list: outlist.extend(cls.__unpack_args(arg)) - # END recursion - else: - outlist.append(str(arg)) - # END for each arg + else: + outlist.append(str(arg_list)) + return outlist def __call__(self, **kwargs: Any) -> 'Git': diff --git a/git/compat.py b/git/compat.py index 7a0a15d23..988c04eff 100644 --- a/git/compat.py +++ b/git/compat.py @@ -29,8 +29,6 @@ Union, overload, ) -from git.types import TBD - # --------------------------------------------------------------------------- @@ -97,19 +95,3 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: elif s is not None: raise TypeError('Expected bytes or text, but got %r' % (s,)) return None - - -# 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 - __call__ = type.__call__ - __init__ = type.__init__ # type: ignore - - 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, {}) # type: ignore diff --git a/git/config.py b/git/config.py index 011d0e0b1..293281d21 100644 --- a/git/config.py +++ b/git/config.py @@ -33,7 +33,7 @@ from typing import (Any, Callable, Generic, IO, List, Dict, Sequence, TYPE_CHECKING, Tuple, TypeVar, Union, cast, overload) -from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, TBD, assert_never, _T +from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T if TYPE_CHECKING: from git.repo.base import Repo @@ -44,10 +44,10 @@ if sys.version_info[:3] < (3, 7, 2): # typing.Ordereddict not added until py 3.7.2 - from collections import OrderedDict # type: ignore # until 3.6 dropped - OrderedDict_OMD = OrderedDict # type: ignore # until 3.6 dropped + from collections import OrderedDict + OrderedDict_OMD = OrderedDict else: - from typing import OrderedDict # type: ignore # until 3.6 dropped + from typing import OrderedDict OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] # ------------------------------------------------------------- @@ -72,7 +72,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: Dict[str, Any]) -> TBD: + def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': """ 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.""" @@ -177,7 +177,7 @@ def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> class _OMD(OrderedDict_OMD): """Ordered multi-dict.""" - def __setitem__(self, key: str, value: _T) -> None: # type: ignore[override] + def __setitem__(self, key: str, value: _T) -> None: super(_OMD, self).__setitem__(key, [value]) def add(self, key: str, value: Any) -> None: @@ -203,8 +203,8 @@ def setlast(self, key: str, value: Any) -> None: prior = super(_OMD, self).__getitem__(key) prior[-1] = value - def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: # type: ignore - return super(_OMD, self).get(key, [default])[-1] # type: ignore + def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: + return super(_OMD, self).get(key, [default])[-1] def getall(self, key: str) -> List[_T]: return super(_OMD, self).__getitem__(key) @@ -236,7 +236,8 @@ def get_config_path(config_level: Lit_config_levels) -> str: raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") else: # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs - assert_never(config_level, ValueError(f"Invalid configuration level: {config_level!r}")) + assert_never(config_level, # type: ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}")) class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): @@ -299,10 +300,10 @@ def __init__(self, file_or_files: Union[None, PathLike, 'BytesIO', Sequence[Unio :param repo: Reference to repository to use if [includeIf] sections are found in configuration files. """ - cp.RawConfigParser.__init__(self, dict_type=_OMD) # type: ignore[arg-type] - self._dict: Callable[..., _OMD] # type: ignore[assignment] # mypy/typeshed bug - self._defaults: _OMD # type: ignore[assignment] # mypy/typeshed bug - self._sections: _OMD # type: ignore[assignment] # mypy/typeshed bug + cp.RawConfigParser.__init__(self, dict_type=_OMD) + self._dict: Callable[..., _OMD] # type: ignore # mypy/typeshed bug? + self._defaults: _OMD + self._sections: _OMD # type: ignore # mypy/typeshed bug? # Used in python 3, needs to stay in sync with sections for underlying implementation to work if not hasattr(self, '_proxies'): @@ -617,12 +618,12 @@ def _write(self, fp: IO) -> None: def write_section(name: str, section_dict: _OMD) -> None: fp.write(("[%s]\n" % name).encode(defenc)) - values: Sequence[Union[str, bytes, int, float, bool]] + values: Sequence[str] # runtime only gets str in tests, but should be whatever _OMD stores + v: str for (key, values) in section_dict.items_all(): if key == "__name__": continue - v: Union[str, bytes, int, float, bool] for v in values: fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace('\n', '\n\t'))).encode(defenc)) # END if key is not __name__ @@ -630,7 +631,8 @@ def write_section(name: str, section_dict: _OMD) -> None: if self._defaults: write_section(cp.DEFAULTSECT, self._defaults) - value: TBD + value: _OMD + for name, value in self._sections.items(): write_section(name, value) diff --git a/git/diff.py b/git/diff.py index 74ca0b64d..cea66d7ee 100644 --- a/git/diff.py +++ b/git/diff.py @@ -16,7 +16,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Iterator, List, Match, Optional, Tuple, Type, TypeVar, Union, TYPE_CHECKING, cast -from git.types import PathLike, TBD, Literal +from git.types import PathLike, Literal if TYPE_CHECKING: from .objects.tree import Tree @@ -24,6 +24,7 @@ from git.repo.base import Repo from git.objects.base import IndexObject from subprocess import Popen + from git import Git Lit_change_type = Literal['A', 'D', 'C', 'M', 'R', 'T', 'U'] @@ -442,7 +443,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: Union['Popen', 'Git.AutoInterrupt']) -> 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) @@ -455,8 +456,8 @@ def _index_from_patch_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex: # for now, we have to bake the stream text = b''.join(text_list) index: 'DiffIndex' = DiffIndex() - previous_header = None - header = None + previous_header: Union[Match[bytes], None] = None + header: Union[Match[bytes], None] = 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): diff --git a/git/index/base.py b/git/index/base.py index 6452419c5..102703e6d 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -70,7 +70,7 @@ from typing import (Any, BinaryIO, Callable, Dict, IO, Iterable, Iterator, List, NoReturn, Sequence, TYPE_CHECKING, Tuple, Type, Union) -from git.types import Commit_ish, PathLike, TBD +from git.types import Commit_ish, PathLike if TYPE_CHECKING: from subprocess import Popen @@ -181,7 +181,7 @@ def _deserialize(self, stream: IO) -> 'IndexFile': self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream) return self - def _entries_sorted(self) -> List[TBD]: + def _entries_sorted(self) -> List[IndexEntry]: """: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)) @@ -427,8 +427,8 @@ def raise_exc(e: Exception) -> NoReturn: # END path exception handling # END for each path - def _write_path_to_stdin(self, proc: 'Popen', filepath: PathLike, item: TBD, fmakeexc: Callable[..., GitError], - fprogress: Callable[[PathLike, bool, TBD], None], + def _write_path_to_stdin(self, proc: 'Popen', filepath: PathLike, item: PathLike, fmakeexc: Callable[..., GitError], + fprogress: Callable[[PathLike, bool, PathLike], None], read_from_stdout: bool = True) -> Union[None, str]: """Write path to proc.stdin and make sure it processes the item, including progress. @@ -492,12 +492,13 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: are at stage 3 will not have a stage 3 entry. """ is_unmerged_blob = lambda t: t[0] != 0 - path_map: Dict[PathLike, List[Tuple[TBD, Blob]]] = {} + path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {} for stage, blob in self.iter_blobs(is_unmerged_blob): path_map.setdefault(blob.path, []).append((stage, blob)) # END for each unmerged blob for line in path_map.values(): line.sort() + return path_map @ classmethod @@ -1201,7 +1202,6 @@ def handle_stderr(proc: 'Popen[bytes]', iter_checked_out_files: Iterable[PathLik handle_stderr(proc, checked_out_files) return checked_out_files # END paths handling - assert "Should not reach this point" @ default_index def reset(self, commit: Union[Commit, 'Reference', str] = 'HEAD', working_tree: bool = False, diff --git a/git/objects/fun.py b/git/objects/fun.py index d6cdafe1e..19b4e525a 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -51,7 +51,7 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[['ReadableBuffer if isinstance(name, str): name_bytes = name.encode(defenc) else: - name_bytes = name + name_bytes = name # type: ignore[unreachable] # check runtime types - is always str? write(b''.join((mode_str, b' ', name_bytes, b'\0', binsha))) # END for each item diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 559d2585e..d306c91d4 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -379,6 +379,7 @@ def add(cls, repo: 'Repo', name: str, path: PathLike, url: Union[str, None] = No :return: The newly created submodule instance :note: works atomically, such that no change will be done if the repository update fails for instance""" + if repo.bare: raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") # END handle bare repos @@ -434,7 +435,7 @@ def add(cls, repo: 'Repo', name: str, path: PathLike, url: Union[str, None] = No url = urls[0] else: # clone new repo - kwargs: Dict[str, Union[bool, int, Sequence[TBD]]] = {'n': no_checkout} + kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {'n': no_checkout} if not branch_is_default: kwargs['b'] = br.name # END setup checkout-branch diff --git a/git/objects/tree.py b/git/objects/tree.py index 0cceb59ac..22531895e 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -215,7 +215,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', + def _get_intermediate_items(cls, index_object: IndexObjUnion, ) -> Union[Tuple['Tree', ...], Tuple[()]]: if index_object.type == "tree": return tuple(index_object._iter_convert_to_object(index_object._cache)) diff --git a/git/objects/util.py b/git/objects/util.py index f627211ec..16d4c0ac8 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -167,7 +167,7 @@ def from_timestamp(timestamp: float, tz_offset: float) -> datetime: return utc_dt -def parse_date(string_date: str) -> Tuple[int, int]: +def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: """ Parse the given date as one of the following @@ -181,9 +181,13 @@ def parse_date(string_date: str) -> Tuple[int, int]: :raise ValueError: If the format could not be understood :note: Date can also be YYYY.MM.DD, MM/DD/YYYY and DD.MM.YYYY. """ - if isinstance(string_date, datetime) and string_date.tzinfo: - offset = -int(string_date.utcoffset().total_seconds()) - return int(string_date.astimezone(utc).timestamp()), offset + if isinstance(string_date, datetime): + if string_date.tzinfo: + utcoffset = cast(timedelta, string_date.utcoffset()) # typeguard, if tzinfoand is not None + offset = -int(utcoffset.total_seconds()) + return int(string_date.astimezone(utc).timestamp()), offset + else: + raise ValueError(f"string_date datetime object without tzinfo, {string_date}") # git time try: @@ -245,7 +249,7 @@ def parse_date(string_date: str) -> Tuple[int, int]: raise ValueError("no format matched") # END handle format except Exception as e: - raise ValueError("Unsupported date format: %s" % string_date) from e + raise ValueError(f"Unsupported date format or type: {string_date}, type={type(string_date)}") from e # END handle exceptions @@ -338,7 +342,7 @@ def _list_traverse(self, as_edge: bool = False, *args: Any, **kwargs: Any """ # Commit and Submodule have id.__attribute__ as IterableObj # Tree has id.__attribute__ inherited from IndexObject - if isinstance(self, (TraversableIterableObj, Has_id_attribute)): + if isinstance(self, Has_id_attribute): id = self._id_attribute_ else: id = "" # shouldn't reach here, unless Traversable subclass created with no _id_attribute_ @@ -346,7 +350,7 @@ def _list_traverse(self, as_edge: bool = False, *args: Any, **kwargs: Any if not as_edge: out: IterableList[Union['Commit', 'Submodule', 'Tree', 'Blob']] = IterableList(id) - out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) # type: ignore + out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) return out # overloads in subclasses (mypy does't allow typing self: subclass) # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]] diff --git a/git/refs/reference.py b/git/refs/reference.py index a3647fb3b..2a33fbff0 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -8,7 +8,7 @@ # typing ------------------------------------------------------------------ from typing import Any, Callable, Iterator, Type, Union, TYPE_CHECKING # NOQA -from git.types import Commit_ish, PathLike, TBD, Literal, _T # NOQA +from git.types import Commit_ish, PathLike, _T # NOQA if TYPE_CHECKING: from git.repo import Repo diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index b4a933aa7..0c0fa4045 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -21,8 +21,8 @@ # typing ------------------------------------------------------------------ -from typing import Any, Iterator, List, Match, Optional, Tuple, Type, TypeVar, Union, TYPE_CHECKING, cast # NOQA -from git.types import Commit_ish, PathLike, TBD, Literal # NOQA +from typing import Any, Iterator, List, Tuple, Type, TypeVar, Union, TYPE_CHECKING, cast # NOQA +from git.types import Commit_ish, PathLike # NOQA if TYPE_CHECKING: from git.repo import Repo @@ -72,12 +72,13 @@ def __str__(self) -> str: def __repr__(self) -> str: return '<git.%s "%s">' % (self.__class__.__name__, self.path) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if hasattr(other, 'path'): + other = cast(SymbolicReference, other) return self.path == other.path return False - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: return not (self == other) def __hash__(self) -> int: @@ -284,7 +285,7 @@ def set_object(self, object: Union[Commit_ish, 'SymbolicReference', str], logmsg commit = property(_get_commit, set_commit, doc="Query or set commits directly") # type: ignore object = property(_get_object, set_object, doc="Return the object our ref currently refers to") # type: ignore - def _get_reference(self) -> 'Reference': + def _get_reference(self) -> 'SymbolicReference': """:return: Reference Object we point to :raise TypeError: If this symbolic reference is detached, hence it doesn't point to a reference, but to a commit""" @@ -364,8 +365,9 @@ def set_reference(self, ref: Union[Commit_ish, 'SymbolicReference', str], return self # aliased reference + reference: Union['Head', 'TagReference', 'RemoteReference', 'Reference'] reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") # type: ignore - ref: Union['Head', 'TagReference', 'RemoteReference', 'Reference'] = reference # type: ignore + ref = reference def is_valid(self) -> bool: """ @@ -681,7 +683,7 @@ def iter_items(cls: Type[T_References], repo: 'Repo', common_path: Union[PathLik return (r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached) @classmethod - def from_path(cls, repo: 'Repo', path: PathLike) -> Union['Head', 'TagReference', 'Reference']: + def from_path(cls: Type[T_References], repo: 'Repo', path: PathLike) -> T_References: """ :param path: full .git-directory-relative path name to the Reference to instantiate :note: use to_full_path() if you only have a partial path of a known Reference Type @@ -696,10 +698,13 @@ def from_path(cls, repo: 'Repo', path: PathLike) -> Union['Head', 'TagReference' from . import HEAD, Head, RemoteReference, TagReference, Reference for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): try: + instance: T_References instance = ref_type(repo, path) if instance.__class__ == SymbolicReference and instance.is_detached: raise ValueError("SymbolRef was detached, we drop it") - return instance + else: + return instance + except ValueError: pass # END exception handling diff --git a/git/remote.py b/git/remote.py index 11007cb68..3888506fd 100644 --- a/git/remote.py +++ b/git/remote.py @@ -37,10 +37,10 @@ # typing------------------------------------------------------- -from typing import (Any, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, # NOQA[TC002] +from typing import (Any, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, TYPE_CHECKING, Type, Union, cast, overload) -from git.types import PathLike, Literal, TBD, Commit_ish # NOQA[TC002] +from git.types import PathLike, Literal, Commit_ish if TYPE_CHECKING: from git.repo.base import Repo @@ -50,7 +50,6 @@ flagKeyLiteral = Literal[' ', '!', '+', '-', '*', '=', 't', '?'] - # def is_flagKeyLiteral(inp: str) -> TypeGuard[flagKeyLiteral]: # return inp in [' ', '!', '+', '-', '=', '*', 't', '?'] @@ -633,7 +632,7 @@ def stale_refs(self) -> IterableList[Reference]: as well. This is a fix for the issue described here: https://github.com/gitpython-developers/GitPython/issues/260 """ - out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) + out_refs: IterableList[Reference] = 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 @@ -643,7 +642,7 @@ def stale_refs(self) -> IterableList[Reference]: 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 + '/'): - out_refs.append(SymbolicReference.from_path(self.repo, ref_name)) + out_refs.append(Reference.from_path(self.repo, ref_name)) else: fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name) out_refs.append(RemoteReference(self.repo, fqhn)) @@ -707,9 +706,10 @@ def update(self, **kwargs: Any) -> 'Remote': self.repo.git.remote(scmd, self.name, **kwargs) return self - def _get_fetch_info_from_stderr(self, proc: TBD, + def _get_fetch_info_from_stderr(self, proc: 'Git.AutoInterrupt', 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 @@ -768,7 +768,7 @@ def _get_fetch_info_from_stderr(self, proc: TBD, log.warning("Git informed while fetching: %s", err_line.strip()) return output - def _get_push_info(self, proc: TBD, + def _get_push_info(self, proc: 'Git.AutoInterrupt', progress: Union[Callable[..., Any], RemoteProgress, None]) -> IterableList[PushInfo]: progress = to_progress_instance(progress) diff --git a/git/repo/__init__.py b/git/repo/__init__.py index 712df60de..23c18db85 100644 --- a/git/repo/__init__.py +++ b/git/repo/__init__.py @@ -1,3 +1,3 @@ """Initialize the Repo package""" # flake8: noqa -from .base import * +from .base import Repo as Repo diff --git a/git/repo/base.py b/git/repo/base.py index 5581233ba..c0229a844 100644 --- a/git/repo/base.py +++ b/git/repo/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 __future__ import annotations import logging import os import re @@ -37,13 +38,13 @@ # typing ------------------------------------------------------ -from git.types import TBD, PathLike, Lit_config_levels, Commit_ish, Tree_ish +from git.types import TBD, PathLike, Lit_config_levels, Commit_ish, Tree_ish, assert_never from typing import (Any, BinaryIO, Callable, Dict, Iterator, List, Mapping, Optional, Sequence, TextIO, Tuple, Type, Union, NamedTuple, cast, TYPE_CHECKING) -from git.types import ConfigLevels_Tup +from git.types import ConfigLevels_Tup, TypedDict if TYPE_CHECKING: from git.util import IterableList @@ -52,7 +53,6 @@ from git.objects.submodule.base import UpdateProgress from git.remote import RemoteProgress - # ----------------------------------------------------------- log = logging.getLogger(__name__) @@ -200,7 +200,6 @@ def __init__(self, path: Optional[PathLike] = None, odbt: Type[LooseObjectDB] = # END while curpath if self.git_dir is None: - self.git_dir = cast(PathLike, self.git_dir) raise InvalidGitRepositoryError(epath) self._bare = False @@ -235,7 +234,7 @@ def __init__(self, path: Optional[PathLike] = None, odbt: Type[LooseObjectDB] = def __enter__(self) -> 'Repo': return self - def __exit__(self, exc_type: TBD, exc_value: TBD, traceback: TBD) -> None: + def __exit__(self, *args: Any) -> None: self.close() def __del__(self) -> None: @@ -385,13 +384,13 @@ def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule: :return: created submodules""" return Submodule.add(self, *args, **kwargs) - def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator: + def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: """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: Any, **kwargs: Any) -> Iterator: + def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: """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""" @@ -445,7 +444,7 @@ def create_tag(self, path: PathLike, ref: str = 'HEAD', :return: TagReference object """ return TagReference.create(self, path, ref, message, force, **kwargs) - def delete_tag(self, *tags: TBD) -> None: + def delete_tag(self, *tags: TagReference) -> None: """Delete the given tag references""" return TagReference.delete(self, *tags) @@ -481,10 +480,12 @@ def _get_config_path(self, config_level: Lit_config_levels) -> str: raise NotADirectoryError else: return osp.normpath(osp.join(repo_dir, "config")) + else: - raise ValueError("Invalid configuration level: %r" % config_level) + assert_never(config_level, # type:ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}")) - def config_reader(self, config_level: Optional[Lit_config_levels] = None + def config_reader(self, config_level: Optional[Lit_config_levels] = None, ) -> GitConfigParser: """ :return: @@ -775,7 +776,7 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: finalize_process(proc) return untracked_files - def ignored(self, *paths: PathLike) -> List[PathLike]: + def ignored(self, *paths: PathLike) -> List[str]: """Checks if paths are ignored via .gitignore Doing so using the "git check-ignore" method. @@ -783,7 +784,7 @@ def ignored(self, *paths: PathLike) -> List[PathLike]: :return: subset of those paths which are ignored """ try: - proc = self.git.check_ignore(*paths) + proc: str = self.git.check_ignore(*paths) except GitCommandError: return [] return proc.replace("\\\\", "\\").replace('"', "").split("\n") @@ -795,7 +796,7 @@ def active_branch(self) -> Head: # reveal_type(self.head.reference) # => Reference return self.head.reference - def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iterator['BlameEntry']]: + def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> 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 @@ -809,8 +810,9 @@ def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iter If you combine all line number ranges outputted by this command, you 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: Dict[str, Commit] = {} + + data: bytes = self.git.blame(rev, '--', file, p=True, incremental=True, stdout_as_string=False, **kwargs) + commits: Dict[bytes, Commit] = {} stream = (line for line in data.split(b'\n') if line) while True: @@ -818,15 +820,15 @@ 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 - split_line: Tuple[str, str, str, str] = line.split() - 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) + split_line = line.split() + hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line + lineno = int(lineno_b) + num_lines = int(num_lines_b) + orig_lineno = int(orig_lineno_b) if hexsha not in commits: # Now read the next few lines and build up a dict of properties # for this commit - props = {} + props: Dict[bytes, bytes] = {} while True: try: line = next(stream) @@ -870,8 +872,8 @@ def blame_incremental(self, rev: TBD, file: TBD, **kwargs: Any) -> Optional[Iter safe_decode(orig_filename), range(orig_lineno, orig_lineno + num_lines)) - def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any - ) -> Union[List[List[Union[Optional['Commit'], List[str]]]], Optional[Iterator[BlameEntry]]]: + def blame(self, rev: Union[str, HEAD], file: str, incremental: bool = False, **kwargs: Any + ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: """The blame information for the given file at the given revision. :param rev: revision specifier, see git-rev-parse for viable options. @@ -883,25 +885,38 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any if incremental: return self.blame_incremental(rev, file, **kwargs) - data = self.git.blame(rev, '--', file, p=True, stdout_as_string=False, **kwargs) - commits: Dict[str, TBD] = {} - blames: List[List[Union[Optional['Commit'], List[str]]]] = [] - - info: Dict[str, TBD] = {} # use Any until TypedDict available + data: bytes = self.git.blame(rev, '--', file, p=True, stdout_as_string=False, **kwargs) + commits: Dict[str, Commit] = {} + blames: List[List[Commit | List[str | bytes] | None]] = [] + + class InfoTD(TypedDict, total=False): + sha: str + id: str + filename: str + summary: str + author: str + author_email: str + author_date: int + committer: str + committer_email: str + committer_date: int + + info: InfoTD = {} keepends = True - for line in data.splitlines(keepends): + for line_bytes in data.splitlines(keepends): try: - line = line.rstrip().decode(defenc) + line_str = line_bytes.rstrip().decode(defenc) except UnicodeDecodeError: firstpart = '' + parts = [] is_binary = True else: # As we don't have an idea when the binary data ends, as it could contain multiple newlines # in the process. So we rely on being able to decode to tell us what is is. # This can absolutely fail even on text files, but even if it does, we should be fine treating it # as binary instead - parts = self.re_whitespace.split(line, 1) + parts = self.re_whitespace.split(line_str, 1) firstpart = parts[0] is_binary = False # end handle decode of line @@ -932,12 +947,20 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any # committer-time 1192271832 # committer-tz -0700 - IGNORED BY US role = m.group(0) - if firstpart.endswith('-mail'): - info["%s_email" % role] = parts[-1] - elif firstpart.endswith('-time'): - info["%s_date" % role] = int(parts[-1]) - elif role == firstpart: - info[role] = parts[-1] + if role == 'author': + if firstpart.endswith('-mail'): + info["author_email"] = parts[-1] + elif firstpart.endswith('-time'): + info["author_date"] = int(parts[-1]) + elif role == firstpart: + info["author"] = parts[-1] + elif role == 'committer': + if firstpart.endswith('-mail'): + info["committer_email"] = parts[-1] + elif firstpart.endswith('-time'): + info["committer_date"] = int(parts[-1]) + elif role == firstpart: + info["committer"] = parts[-1] # END distinguish mail,time,name else: # handle @@ -954,26 +977,29 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any c = commits.get(sha) if c is None: c = Commit(self, hex_to_bin(sha), - author=Actor._from_string(info['author'] + ' ' + info['author_email']), + author=Actor._from_string(f"{info['author']} {info['author_email']}"), authored_date=info['author_date'], committer=Actor._from_string( - info['committer'] + ' ' + info['committer_email']), + f"{info['committer']} {info['committer_email']}"), committed_date=info['committer_date']) commits[sha] = c - # END if commit objects needs initial creation - if not is_binary: - if line and line[0] == '\t': - line = line[1:] - else: - # NOTE: We are actually parsing lines out of binary data, which can lead to the - # binary being split up along the newline separator. We will append this to the blame - # we are currently looking at, even though it should be concatenated with the last line - # we have seen. - pass - # end handle line contents blames[-1][0] = c + # END if commit objects needs initial creation + if blames[-1][1] is not None: + line: str | bytes + if not is_binary: + if line_str and line_str[0] == '\t': + line_str = line_str[1:] + line = line_str + else: + line = line_bytes + # NOTE: We are actually parsing lines out of binary data, which can lead to the + # binary being split up along the newline separator. We will append this to the + # blame we are currently looking at, even though it should be concatenated with + # the last line we have seen. blames[-1][1].append(line) + info = {'id': sha} # END if we collected commit info # END distinguish filename,summary,rest @@ -981,7 +1007,7 @@ def blame(self, rev: TBD, file: TBD, incremental: bool = False, **kwargs: Any # END distinguish hexsha vs other information return blames - @classmethod + @ classmethod def init(cls, path: Union[PathLike, None] = 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 @@ -1020,7 +1046,7 @@ def init(cls, path: Union[PathLike, None] = None, mkdir: bool = True, odbt: Type git.init(**kwargs) return cls(path, odbt=odbt) - @classmethod + @ classmethod def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Type[GitCmdObjectDB], progress: Union['RemoteProgress', 'UpdateProgress', Callable[..., 'RemoteProgress'], None] = None, multi_options: Optional[List[str]] = None, **kwargs: Any @@ -1098,9 +1124,9 @@ def clone(self, path: PathLike, progress: Optional[Callable] = None, :return: ``git.Repo`` (the newly cloned repo)""" return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs) - @classmethod + @ classmethod def clone_from(cls, url: PathLike, to_path: PathLike, progress: Optional[Callable] = None, - env: Optional[Mapping[str, Any]] = None, + env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None, **kwargs: Any) -> 'Repo': """Create a clone from the given URL @@ -1122,7 +1148,7 @@ def clone_from(cls, url: PathLike, to_path: PathLike, progress: Optional[Callabl return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) def archive(self, ostream: Union[TextIO, BinaryIO], treeish: Optional[str] = None, - prefix: Optional[str] = None, **kwargs: Any) -> 'Repo': + 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 @@ -1169,7 +1195,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_ish, None]: + def currently_rebasing_on(self) -> Commit | None: """ :return: The commit which is currently being replayed while rebasing. diff --git a/git/repo/fun.py b/git/repo/fun.py index 7d5c78237..1a83dd3dc 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -1,4 +1,5 @@ """Package with general repository related functions""" +from __future__ import annotations import os import stat from string import digits @@ -18,12 +19,13 @@ # Typing ---------------------------------------------------------------------- from typing import Union, Optional, cast, TYPE_CHECKING - +from git.types import Commit_ish if TYPE_CHECKING: from git.types import PathLike from .base import Repo from git.db import GitCmdObjectDB + from git.refs.reference import Reference from git.objects import Commit, TagObject, Blob, Tree from git.refs.tag import Tag @@ -202,7 +204,7 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: raise NotImplementedError("commit by message search ( regex )") # END handle search - obj = cast(Object, None) # not ideal. Should use guards + obj: Union[Commit_ish, 'Reference', None] = None ref = None output_type = "commit" start = 0 @@ -222,14 +224,16 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: ref = repo.head.ref else: if token == '@': - ref = name_to_object(repo, rev[:start], return_ref=True) + ref = cast('Reference', name_to_object(repo, rev[:start], return_ref=True)) else: - obj = name_to_object(repo, rev[:start]) + obj = cast(Commit_ish, name_to_object(repo, rev[:start])) # END handle token # END handle refname + else: + assert obj is not None if ref is not None: - obj = ref.commit + obj = cast('Commit', ref.commit) # END handle ref # END initialize obj on first token @@ -247,11 +251,13 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: pass # default elif output_type == 'tree': try: + obj = cast(Commit_ish, obj) obj = to_commit(obj).tree except (AttributeError, ValueError): pass # error raised later # END exception handling elif output_type in ('', 'blob'): + obj = cast('TagObject', obj) if obj and obj.type == 'tag': obj = deref_tag(obj) else: @@ -280,13 +286,13 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha)) # make it pass the following checks - output_type = None + output_type = '' else: raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) # END handle output type # empty output types don't require any specific type, its just about dereferencing tags - if output_type and obj.type != output_type: + if output_type and obj and obj.type != output_type: raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type)) # END verify output type @@ -319,6 +325,7 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: parsed_to = start # handle hierarchy walk try: + obj = cast(Commit_ish, obj) if token == "~": obj = to_commit(obj) for _ in range(num): @@ -340,14 +347,14 @@ def rev_parse(repo: 'Repo', rev: str) -> Union['Commit', 'Tag', 'Tree', 'Blob']: # END end handle tag except (IndexError, AttributeError) as e: raise BadName( - "Invalid revision spec '%s' - not enough " - "parent commits to reach '%s%i'" % (rev, token, num)) from e + f"Invalid revision spec '{rev}' - not enough " + f"parent commits to reach '{token}{int(num)}'") from e # END exception handling # END parse loop # still no obj ? Its probably a simple name if obj is None: - obj = name_to_object(repo, rev) + obj = cast(Commit_ish, name_to_object(repo, rev)) parsed_to = lr # END handle simple name diff --git a/git/types.py b/git/types.py index ccaffef3e..64bf3d96d 100644 --- a/git/types.py +++ b/git/types.py @@ -23,7 +23,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]'] # forward ref as pylance complains unless editing with py3.9+ + PathLike = Union[str, os.PathLike] if TYPE_CHECKING: from git.repo import Repo diff --git a/git/util.py b/git/util.py index 92d95379e..4f82219e6 100644 --- a/git/util.py +++ b/git/util.py @@ -38,6 +38,7 @@ from git.remote import Remote from git.repo.base import Repo from git.config import GitConfigParser, SectionConstraint + from git import Git # from git.objects.base import IndexObject @@ -69,7 +70,7 @@ # Most of these are unused here, but are for use by git-python modules so these # don't see gitdb all the time. Flake of course doesn't like it. __all__ = ["stream_copy", "join_path", "to_native_path_linux", - "join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList", + "join_path_native", "Stats", "IndexFileSHA1Writer", "IterableObj", "IterableList", "BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists', 'RemoteProgress', 'CallableRemoteProgress', 'rmtree', 'unbare_repo', 'HIDE_WINDOWS_KNOWN_ERRORS'] @@ -379,7 +380,7 @@ def get_user_id() -> str: return "%s@%s" % (getpass.getuser(), platform.node()) -def finalize_process(proc: subprocess.Popen, **kwargs: Any) -> None: +def finalize_process(proc: Union[subprocess.Popen, 'Git.AutoInterrupt'], **kwargs: Any) -> None: """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" # TODO: No close proc-streams?? proc.wait(**kwargs) @@ -403,7 +404,7 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ p = osp.expanduser(p) # type: ignore if expand_vars: p = osp.expandvars(p) # type: ignore - return osp.normpath(osp.abspath(p)) # type: ignore + return osp.normpath(osp.abspath(p)) # type: ignore except Exception: return None @@ -1033,7 +1034,7 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None: class IterableClassWatcher(type): """ Metaclass that watches """ - def __init__(cls, name: str, bases: List, clsdict: Dict) -> None: + def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: for base in bases: if type(base) == IterableClassWatcher: warnings.warn(f"GitPython Iterable subclassed by {name}. " diff --git a/pyproject.toml b/pyproject.toml index 4751ffcb9..102b6fdc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,11 @@ filterwarnings = 'ignore::DeprecationWarning' disallow_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true -# warn_unused_ignores = True -# warn_unreachable = True +# warn_unused_ignores = true +warn_unreachable = true show_error_codes = true +implicit_reexport = true +# strict = true # TODO: remove when 'gitdb' is fully annotated [[tool.mypy.overrides]] diff --git a/setup.py b/setup.py index 215590710..ae6319f9e 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +from typing import Sequence from setuptools import setup, find_packages from setuptools.command.build_py import build_py as _build_py from setuptools.command.sdist import sdist as _sdist @@ -18,7 +19,7 @@ class build_py(_build_py): - def run(self): + def run(self) -> None: init = path.join(self.build_lib, 'git', '__init__.py') if path.exists(init): os.unlink(init) @@ -29,7 +30,7 @@ def run(self): class sdist(_sdist): - def make_release_tree(self, base_dir, files): + def make_release_tree(self, base_dir: str, files: Sequence) -> None: _sdist.make_release_tree(self, base_dir, files) orig = path.join('git', '__init__.py') assert path.exists(orig), orig @@ -40,7 +41,7 @@ def make_release_tree(self, base_dir, files): _stamp_version(dest) -def _stamp_version(filename): +def _stamp_version(filename: str) -> None: found, out = False, [] try: with open(filename, 'r') as f: @@ -59,7 +60,7 @@ def _stamp_version(filename): print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr) -def build_py_modules(basedir, excludes=()): +def build_py_modules(basedir: str, excludes: Sequence = ()) -> Sequence: # create list of py_modules from tree res = set() _prefix = os.path.basename(basedir) @@ -90,7 +91,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.7', install_requires=requirements, tests_require=requirements + test_requirements, zip_safe=False, @@ -112,11 +113,12 @@ def build_py_modules(basedir, excludes=()): "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", + "Typing:: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.10" ] )