diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1cc049a69..000000000 --- a/.flake8 +++ /dev/null @@ -1,26 +0,0 @@ -[flake8] - -show-source = True -count = True -statistics = True - -# E266 = too many leading '#' for block comment -# E731 = do not assign a lambda expression, use a def -# TC002 = move third party import to TYPE_CHECKING -# TC, TC2 = flake8-type-checking - -# select = C,E,F,W ANN, TC, TC2 # to enable code. Disabled if not listed, including builtin codes -enable-extensions = TC, TC2 # only needed for extensions not enabled by default - -ignore = E266, E731 - -exclude = .tox, .venv, build, dist, doc, git/ext/ - -rst-roles = # for flake8-RST-docstrings - attr, class, func, meth, mod, obj, ref, term, var # used by sphinx - -min-python-version = 3.7.0 - -# for `black` compatibility -max-line-length = 120 -extend-ignore = E203, W503 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d805124fc..a32fb6c4e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,5 +16,3 @@ jobs: - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files --hook-stage manual - env: - SKIP: black-format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ac5baa00..60bbe3518 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,12 @@ repos: -- repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 - hooks: - - id: black - alias: black-check - name: black (check) - args: [--check, --diff] - exclude: ^git/ext/ - stages: [manual] - - id: black - alias: black-format - name: black (format) - exclude: ^git/ext/ - -- repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.0 hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.9.16 - - flake8-comprehensions==3.14.0 - - flake8-typing-imports==1.14.0 + - id: ruff-format + exclude: ^git/ext/ + - id: ruff + args: ["--fix"] exclude: ^doc|^git/ext/ - repo: https://github.com/shellcheck-py/shellcheck-py diff --git a/Makefile b/Makefile index 839dc9f78..d4f9acf87 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,8 @@ -.PHONY: all lint clean release force_release +.PHONY: all clean release force_release all: @awk -F: '/^[[:alpha:]].*:/ && !/^all:/ {print $$1}' Makefile -lint: - SKIP=black-format pre-commit run --all-files --hook-stage manual - clean: rm -rf build/ dist/ .eggs/ .tox/ diff --git a/README.md b/README.md index 7faeae23b..30af532db 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ probably the skills to scratch that itch of mine: implement `git` in a way that If you like the idea and want to learn more, please head over to [gitoxide](https://github.com/Byron/gitoxide), an implementation of 'git' in [Rust](https://www.rust-lang.org). -*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required)* +*(Please note that `gitoxide` is not currently available for use in Python, and that Rust is required.)* ## GitPython @@ -39,9 +39,9 @@ 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 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=` environment variable. +GitPython needs the `git` executable to be installed on the system and available 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=` environment variable. - Git (1.7.x or newer) - Python >= 3.7 @@ -57,7 +57,7 @@ GitPython and its required package dependencies can be installed in any of the f To obtain and install a copy [from PyPI](https://pypi.org/project/GitPython/), run: -```bash +```sh pip install GitPython ``` @@ -67,7 +67,7 @@ pip install GitPython If you have downloaded the source code, run this from inside the unpacked `GitPython` directory: -```bash +```sh pip install . ``` @@ -75,7 +75,7 @@ pip install . To clone the [the GitHub repository](https://github.com/gitpython-developers/GitPython) from source to work on the code, you can do it like so: -```bash +```sh git clone https://github.com/gitpython-developers/GitPython cd GitPython ./init-tests-after-clone.sh @@ -85,7 +85,7 @@ On Windows, `./init-tests-after-clone.sh` can be run in a Git Bash shell. If you are cloning [your own fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks), then replace the above `git clone` command with one that gives the URL of your fork. Or use this [`gh`](https://cli.github.com/) command (assuming you have `gh` and your fork is called `GitPython`): -```bash +```sh gh repo clone GitPython ``` @@ -93,7 +93,7 @@ Having cloned the repo, create and activate your [virtual environment](https://d Then make an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): -```bash +```sh pip install -e ".[test]" ``` @@ -105,7 +105,7 @@ In rare cases, you may want to work on GitPython and one or both of its [gitdb]( If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way: -```bash +```sh pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap ``` @@ -141,42 +141,38 @@ you will encounter test failures. Ensure testing libraries are installed. This is taken care of already if you installed with: -```bash +```sh pip install -e ".[test]" ``` -Otherwise, you can run: - -```bash -pip install -r test-requirements.txt -``` +If you had installed with a command like `pip install -e .` instead, you can still run +the above command to add the testing dependencies. #### Test commands To test, run: -```bash +```sh pytest ``` -To lint, and apply automatic code formatting, run: +To lint, and apply some linting fixes as well as automatic code formatting, run: -```bash +```sh pre-commit run --all-files ``` -- Linting without modifying code can be done with: `make lint` -- Auto-formatting without other lint checks can be done with: `black .` +This includes the linting and autoformatting done by Ruff, as well as some other checks. To typecheck, run: -```bash +```sh mypy -p git ``` #### CI (and tox) -The same linting, and running tests on all the different supported Python versions, will be performed: +Style and formatting checks, and running tests on all the different supported Python versions, will be performed: - Upon submitting a pull request. - On each push, *if* you have a fork with GitHub Actions enabled. @@ -184,10 +180,12 @@ The same linting, and running tests on all the different supported Python versio #### Configuration files -Specific tools: +Specific tools are all configured in the `./pyproject.toml` file: -- Configurations for `mypy`, `pytest`, `coverage.py`, and `black` are in `./pyproject.toml`. -- Configuration for `flake8` is in the `./.flake8` file. +- `pytest` (test runner) +- `coverage.py` (code coverage) +- `ruff` (linter and formatter) +- `mypy` (type checker) Orchestration tools: diff --git a/git/cmd.py b/git/cmd.py index 74693b27a..ab2688a25 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -501,9 +501,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: if mode in quiet: pass elif mode in warn or mode in error: - err = ( - dedent( - """\ + err = dedent( + """\ %s All git commands will error until this is rectified. @@ -516,16 +515,14 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: Example: export %s=%s """ - ) - % ( - err, - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - cls._refresh_env_var, - quiet[0], - ) + ) % ( + err, + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + cls._refresh_env_var, + quiet[0], ) if mode in warn: @@ -533,9 +530,8 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: else: raise ImportError(err) else: - err = ( - dedent( - """\ + err = dedent( + """\ %s environment variable has been set but it has been set with an invalid value. Use only the following values: @@ -543,13 +539,11 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: - %s: for a warning message (logging level CRITICAL, displayed by default) - %s: for a raised exception """ - ) - % ( - cls._refresh_env_var, - "|".join(quiet), - "|".join(warn), - "|".join(error), - ) + ) % ( + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), ) raise ImportError(err) @@ -571,13 +565,11 @@ def is_cygwin(cls) -> bool: @overload @classmethod - def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: - ... + def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: ... @overload @classmethod - def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: - ... + def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: ... @classmethod def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: @@ -755,7 +747,7 @@ class CatFileContentStream: rest to ensure the underlying stream continues to work. """ - __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size") + __slots__ = ("_stream", "_nbr", "_size") def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream @@ -852,14 +844,14 @@ def __del__(self) -> None: self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir: Union[None, PathLike] = None): + def __init__(self, working_dir: Union[None, PathLike] = None) -> None: """Initialize this instance with: :param working_dir: - Git directory we should work in. If ``None``, we always work in the current - directory as returned by :func:`os.getcwd`. - This is meant to be the working tree directory if available, or the - ``.git`` directory in case of bare repositories. + Git directory we should work in. If ``None``, we always work in the current + directory as returned by :func:`os.getcwd`. + This is meant to be the working tree directory if available, or the + ``.git`` directory in case of bare repositories. """ super().__init__() self._working_dir = expand_path(working_dir) @@ -938,8 +930,7 @@ def execute( command: Union[str, Sequence[Any]], *, as_process: Literal[True], - ) -> "AutoInterrupt": - ... + ) -> "AutoInterrupt": ... @overload def execute( @@ -948,8 +939,7 @@ def execute( *, as_process: Literal[False] = False, stdout_as_string: Literal[True], - ) -> Union[str, Tuple[int, str, str]]: - ... + ) -> Union[str, Tuple[int, str, str]]: ... @overload def execute( @@ -958,8 +948,7 @@ def execute( *, as_process: Literal[False] = False, stdout_as_string: Literal[False] = False, - ) -> Union[bytes, Tuple[int, bytes, str]]: - ... + ) -> Union[bytes, Tuple[int, bytes, str]]: ... @overload def execute( @@ -969,8 +958,7 @@ def execute( with_extended_output: Literal[False], as_process: Literal[False], stdout_as_string: Literal[True], - ) -> str: - ... + ) -> str: ... @overload def execute( @@ -980,8 +968,7 @@ def execute( with_extended_output: Literal[False], as_process: Literal[False], stdout_as_string: Literal[False], - ) -> bytes: - ... + ) -> bytes: ... def execute( self, @@ -1109,8 +1096,8 @@ def execute( :raise git.exc.GitCommandError: :note: - If you add additional keyword arguments to the signature of this method, - you must update the ``execute_kwargs`` variable housed in this module. + If you add additional keyword arguments to the signature of this method, you + must update the ``execute_kwargs`` variable housed in this module. """ # Remove password for the command if present. redacted_command = remove_password_if_present(command) @@ -1402,8 +1389,9 @@ def __call__(self, **kwargs: Any) -> "Git": return self @overload - def _call_process(self, method: str, *args: None, **kwargs: None) -> str: - ... # If no args were given, execute the call with all defaults. + def _call_process( + self, method: str, *args: None, **kwargs: None + ) -> str: ... # If no args were given, execute the call with all defaults. @overload def _call_process( @@ -1413,14 +1401,12 @@ def _call_process( as_process: Literal[True], *args: Any, **kwargs: Any, - ) -> "Git.AutoInterrupt": - ... + ) -> "Git.AutoInterrupt": ... @overload def _call_process( self, method: str, *args: Any, **kwargs: Any - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: - ... + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: ... def _call_process( self, method: str, *args: Any, **kwargs: Any @@ -1453,7 +1439,7 @@ def _call_process( turns into:: - git rev-list max-count 10 --header master + git rev-list max-count 10 --header master :return: Same as :meth:`execute`. If no args are given, used :meth:`execute`'s diff --git a/git/compat.py b/git/compat.py index 43b50b287..6f5376c9d 100644 --- a/git/compat.py +++ b/git/compat.py @@ -74,13 +74,11 @@ @overload -def safe_decode(s: None) -> None: - ... +def safe_decode(s: None) -> None: ... @overload -def safe_decode(s: AnyStr) -> str: - ... +def safe_decode(s: AnyStr) -> str: ... def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: @@ -96,13 +94,11 @@ def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: @overload -def safe_encode(s: None) -> None: - ... +def safe_encode(s: None) -> None: ... @overload -def safe_encode(s: AnyStr) -> bytes: - ... +def safe_encode(s: AnyStr) -> bytes: ... def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: @@ -118,13 +114,11 @@ def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: @overload -def win_encode(s: None) -> None: - ... +def win_encode(s: None) -> None: ... @overload -def win_encode(s: AnyStr) -> bytes: - ... +def win_encode(s: AnyStr) -> bytes: ... def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: diff --git a/git/index/base.py b/git/index/base.py index 80fa90ee8..883d086f5 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -235,7 +235,7 @@ def write( # Make sure we have our entries read before getting a write lock. # Otherwise it would be done when streaming. # This can happen if one doesn't change the index, but writes it right away. - self.entries + self.entries # noqa: B018 lfd = LockedFD(file_path or self._file_path) stream = lfd.open(write=True, stream=True) @@ -384,7 +384,7 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile with TemporaryFileSwap(join_path_native(repo.git_dir, "index")): repo.git.read_tree(*arg_list, **kwargs) index = cls(repo, tmp_index) - index.entries # Force it to read the file as we will delete the temp-file. + index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file. return index # END index merge handling @@ -1326,7 +1326,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik # Make sure we have our entries loaded before we start checkout_index, which # will hold a lock on it. We try to get the lock as well during our entries # initialization. - self.entries + self.entries # noqa: B018 args.append("--stdin") kwargs["as_process"] = True @@ -1366,7 +1366,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik self._flush_stdin_and_wait(proc, ignore_stdout=True) except GitCommandError: # Without parsing stdout we don't know what failed. - raise CheckoutError( + raise CheckoutError( # noqa: B904 "Some files could not be checked out from the index, probably because they didn't exist.", failed_files, [], diff --git a/git/index/fun.py b/git/index/fun.py index 60483c1f4..001e8f6f2 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -287,9 +287,9 @@ def read_cache( # 4 bytes length of chunk # Repeated 0 - N times extension_data = stream.read(~0) - assert ( - len(extension_data) > 19 - ), "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data) + assert len(extension_data) > 19, ( + "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(extension_data) + ) content_sha = extension_data[-20:] diff --git a/git/objects/base.py b/git/objects/base.py index 90e20c2b6..f568a4bc5 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -97,7 +97,7 @@ class Object(LazyMixin): See also :class:`~git.types.GitObjectTypeString`. """ - def __init__(self, repo: "Repo", binsha: bytes): + def __init__(self, repo: "Repo", binsha: bytes) -> None: """Initialize an object by identifying it by its binary sha. All keyword arguments will be set on demand if ``None``. diff --git a/git/objects/blob.py b/git/objects/blob.py index b37570c3a..0a8527407 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -6,7 +6,11 @@ from mimetypes import guess_type from . import base -from git.types import Literal + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal __all__ = ("Blob",) diff --git a/git/objects/commit.py b/git/objects/commit.py index 31aaf60f4..f17e3fdf5 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -44,7 +44,12 @@ Dict, ) -from git.types import PathLike, Literal +from git.types import PathLike + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal if TYPE_CHECKING: from git.repo import Repo diff --git a/git/objects/fun.py b/git/objects/fun.py index 22b99cb6b..5bd8a3d62 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -152,13 +152,11 @@ def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: @overload -def _to_full_path(item: None, path_prefix: str) -> None: - ... +def _to_full_path(item: None, path_prefix: str) -> None: ... @overload -def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: - ... +def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: ... def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone: diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index f9e0a8e0f..8c4a7356e 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -54,7 +54,12 @@ cast, ) -from git.types import Commit_ish, Literal, PathLike, TBD +from git.types import Commit_ish, PathLike, TBD + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal if TYPE_CHECKING: from git.index import IndexFile @@ -1455,7 +1460,7 @@ def exists(self) -> bool: try: try: - self.path + self.path # noqa: B018 return True except Exception: return False diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py index f6a8c6807..ae56e5ef4 100644 --- a/git/objects/submodule/root.py +++ b/git/objects/submodule/root.py @@ -56,7 +56,7 @@ class RootModule(Submodule): k_root_name = "__ROOT__" - def __init__(self, repo: "Repo"): + def __init__(self, repo: "Repo") -> None: # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None) super().__init__( repo, diff --git a/git/objects/tag.py b/git/objects/tag.py index fd3cdd630..83cf4ae18 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -16,7 +16,10 @@ from typing import List, TYPE_CHECKING, Union -from git.types import Literal +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal if TYPE_CHECKING: from git.repo import Repo diff --git a/git/objects/tree.py b/git/objects/tree.py index e9c08a7cd..225061bb7 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -31,7 +31,12 @@ TYPE_CHECKING, ) -from git.types import PathLike, Literal +from git.types import PathLike + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal if TYPE_CHECKING: from git.repo import Repo @@ -186,7 +191,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable): _map_id_to_type: Dict[int, Type[IndexObjUnion]] = { commit_id: Submodule, blob_id: Blob, - symlink_id: Blob + symlink_id: Blob, # Tree ID added once Tree is defined. } diff --git a/git/objects/util.py b/git/objects/util.py index 576aca406..7cca05134 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -439,7 +439,7 @@ def _list_traverse( if not as_edge: out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id) - out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) + out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) # noqa: B026 return out # Overloads in subclasses (mypy doesn't allow typing self: subclass). # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]] @@ -620,8 +620,7 @@ def list_traverse(self: T_TIobj, *args: Any, **kwargs: Any) -> IterableList[T_TI return super()._list_traverse(*args, **kwargs) @overload - def traverse(self: T_TIobj) -> Iterator[T_TIobj]: - ... + def traverse(self: T_TIobj) -> Iterator[T_TIobj]: ... @overload def traverse( @@ -633,8 +632,7 @@ def traverse( visit_once: bool, ignore_self: Literal[True], as_edge: Literal[False], - ) -> Iterator[T_TIobj]: - ... + ) -> Iterator[T_TIobj]: ... @overload def traverse( @@ -646,8 +644,7 @@ def traverse( visit_once: bool, ignore_self: Literal[False], as_edge: Literal[True], - ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: - ... + ) -> Iterator[Tuple[Union[T_TIobj, None], T_TIobj]]: ... @overload def traverse( @@ -659,8 +656,7 @@ def traverse( visit_once: bool, ignore_self: Literal[True], as_edge: Literal[True], - ) -> Iterator[Tuple[T_TIobj, T_TIobj]]: - ... + ) -> Iterator[Tuple[T_TIobj, T_TIobj]]: ... def traverse( self: T_TIobj, diff --git a/git/refs/head.py b/git/refs/head.py index 4eff7abb8..aae5767d4 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -47,7 +47,7 @@ class HEAD(SymbolicReference): # TODO: This can be removed once SymbolicReference.commit has static type hints. commit: "Commit" - def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME): + def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None: if path != self._HEAD_NAME: raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) super().__init__(repo, path) diff --git a/git/refs/log.py b/git/refs/log.py index 7aefeb4e6..f98f56f11 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -164,7 +164,7 @@ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog": inst = super().__new__(cls) return inst - def __init__(self, filepath: Union[PathLike, None] = None): + def __init__(self, filepath: Union[PathLike, None] = None) -> None: """Initialize this instance with an optional filepath, from which we will initialize our data. The path is also used to write changes back using the :meth:`write` method.""" diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 1f1daa11b..754e90089 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -73,7 +73,7 @@ class SymbolicReference: _remote_common_path_default = "refs/remotes" _id_attribute_ = "name" - def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False): + def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False) -> None: self.repo = repo self.path = path @@ -511,7 +511,7 @@ def is_valid(self) -> bool: valid object or reference. """ try: - self.object + self.object # noqa: B018 except (OSError, ValueError): return False else: @@ -525,7 +525,7 @@ def is_detached(self) -> bool: instead to another reference. """ try: - self.ref + self.ref # noqa: B018 return False except TypeError: return True diff --git a/git/remote.py b/git/remote.py index e0dc08639..1723216a4 100644 --- a/git/remote.py +++ b/git/remote.py @@ -91,22 +91,19 @@ def add_progress( @overload -def to_progress_instance(progress: None) -> RemoteProgress: - ... +def to_progress_instance(progress: None) -> RemoteProgress: ... @overload -def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: - ... +def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ... @overload -def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: - ... +def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ... def to_progress_instance( - progress: Union[Callable[..., Any], RemoteProgress, None] + progress: Union[Callable[..., Any], RemoteProgress, None], ) -> Union[RemoteProgress, CallableRemoteProgress]: """Given the `progress` return a suitable object derived from :class:`~git.util.RemoteProgress`.""" @@ -323,7 +320,7 @@ class FetchInfo(IterableObj): ERROR, ) = [1 << x for x in range(8)] - _re_fetch_result = re.compile(r"^\s*(.) (\[[\w\s\.$@]+\]|[\w\.$@]+)\s+(.+) -> ([^\s]+)( \(.*\)?$)?") + _re_fetch_result = re.compile(r"^ *(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)( \(.*\)?$)?") _flag_map: Dict[flagKeyLiteral, int] = { "!": ERROR, @@ -895,7 +892,7 @@ def _get_fetch_info_from_stderr( None, progress_handler, finalizer=None, - decode_streams=False, + decode_streams=True, kill_after_timeout=kill_after_timeout, ) @@ -1072,7 +1069,7 @@ def fetch( Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options) proc = self.repo.git.fetch( - "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs + "--", self, *args, as_process=True, with_stdout=False, universal_newlines=False, v=verbose, **kwargs ) res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout) if hasattr(self.repo.odb, "update_cache"): @@ -1126,7 +1123,7 @@ def pull( Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options) proc = self.repo.git.pull( - "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs + "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=False, v=True, **kwargs ) res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout) if hasattr(self.repo.odb, "update_cache"): diff --git a/git/util.py b/git/util.py index 8b60b1cb2..2a9dd10a9 100644 --- a/git/util.py +++ b/git/util.py @@ -472,13 +472,11 @@ def _is_cygwin_git(git_executable: str) -> bool: @overload -def is_cygwin_git(git_executable: None) -> Literal[False]: - ... +def is_cygwin_git(git_executable: None) -> Literal[False]: ... @overload -def is_cygwin_git(git_executable: PathLike) -> bool: - ... +def is_cygwin_git(git_executable: PathLike) -> bool: ... def is_cygwin_git(git_executable: Union[None, PathLike]) -> bool: @@ -503,8 +501,7 @@ def finalize_process(proc: Union[subprocess.Popen, "Git.AutoInterrupt"], **kwarg @overload -def expand_path(p: None, expand_vars: bool = ...) -> None: - ... +def expand_path(p: None, expand_vars: bool = ...) -> None: ... @overload @@ -620,20 +617,6 @@ def _parse_progress_line(self, line: AnyStr) -> None: self.error_lines.append(self._cur_line) return - # Find escape characters and cut them away - regex will not work with - # them as they are non-ASCII. As git might expect a tty, it will send them. - last_valid_index = None - for i, c in enumerate(reversed(line_str)): - if ord(c) < 32: - # its a slice index - last_valid_index = -i - 1 - # END character was non-ASCII - # END for each character in line - if last_valid_index is not None: - line_str = line_str[:last_valid_index] - # END cut away invalid part - line_str = line_str.rstrip() - cur_count, max_count = None, None match = self.re_op_relative.match(line_str) if match is None: @@ -933,7 +916,7 @@ class Stats: __slots__ = ("total", "files") - def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]): + def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]) -> None: self.total = total self.files = files diff --git a/pyproject.toml b/pyproject.toml index ef1189b0c..1770a8393 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ warn_unused_ignores = true warn_unreachable = true implicit_reexport = true # strict = true - # TODO: Remove when 'gitdb' is fully annotated. exclude = ["^git/ext/gitdb"] [[tool.mypy.overrides]] @@ -42,7 +41,37 @@ source = ["git"] include = ["*/git/*"] omit = ["*/git/ext/*"] -[tool.black] +[tool.ruff] +target-version = "py37" line-length = 120 -target-version = ["py37"] -extend-exclude = "git/ext/gitdb" +# Exclude a variety of commonly ignored directories. +exclude = [ + "git/ext/", + "doc", + "build", + "dist", +] +# Enable Pyflakes `E` and `F` codes by default. +lint.select = [ + "E", + "W", # see: https://pypi.org/project/pycodestyle + "F", # see: https://pypi.org/project/pyflakes +# "I", #see: https://pypi.org/project/isort/ +# "S", # see: https://pypi.org/project/flake8-bandit +# "UP", # see: https://docs.astral.sh/ruff/rules/#pyupgrade-up +] +lint.extend-select = [ + #"A", # see: https://pypi.org/project/flake8-builtins + "B", # see: https://pypi.org/project/flake8-bugbear + "C4", # see: https://pypi.org/project/flake8-comprehensions + "TCH004", # see: https://docs.astral.sh/ruff/rules/runtime-import-in-type-checking-block/ +] +lint.ignore = [ + "E203", + "E731", # Do not assign a `lambda` expression, use a `def` +] +lint.ignore-init-module-imports = true +lint.unfixable = ["F401"] + +[tool.ruff.lint.per-file-ignores] +"test/**" = ["B018"] diff --git a/requirements-dev.txt b/requirements-dev.txt index e3030c597..69a79d13d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,5 @@ # libraries for additional local testing/linting - to be added to test-requirements.txt when all pass -flake8-type-checking;python_version>="3.8" # checks for TYPE_CHECKING only imports - pytest-icdiff # pytest-profiling diff --git a/test-requirements.txt b/test-requirements.txt index 7cfb977a1..e1f5e2ed4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,3 @@ -black coverage[toml] ddt >= 1.1.1, != 1.4.3 mock ; python_version < "3.8" diff --git a/test/lib/helper.py b/test/lib/helper.py index 0a6df242c..45a778b7d 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -10,6 +10,7 @@ import logging import os import os.path as osp +import subprocess import sys import tempfile import textwrap @@ -412,6 +413,13 @@ def __init__(self, env_dir, *, with_pip): self._env_dir = env_dir venv.create(self.env_dir, symlinks=True, with_pip=with_pip) + if with_pip: + # The upgrade_deps parameter to venv.create is 3.9+ only, so do it this way. + command = [self.python, "-m", "pip", "install", "--upgrade", "pip"] + if sys.version_info < (3, 12): + command.append("setuptools") + subprocess.check_output(command) + @property def env_dir(self): """The top-level directory of the environment.""" diff --git a/test/test_remote.py b/test/test_remote.py index 5344a7324..f84452deb 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -1002,6 +1002,22 @@ def test_push_unsafe_options_allowed(self, rw_repo): assert tmp_file.exists() tmp_file.unlink() + @with_rw_and_rw_remote_repo("0.1.6") + def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo): + # Create branch with a name containing a NBSP + bad_branch_name = f"branch_with_{chr(160)}_nbsp" + Head.create(remote_repo, bad_branch_name) + + # Fetch and get branches + remote = rw_repo.remote("origin") + branches = remote.fetch() + + # Test for truncated branch name in branches + assert f"origin/{bad_branch_name}" in [b.name for b in branches] + + # Cleanup branch + Head.delete(remote_repo, bad_branch_name) + class TestTimeouts(TestBase): @with_rw_repo("HEAD", bare=False) diff --git a/tox.ini b/tox.ini index f9ac25b78..6e02e5aee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] requires = tox>=4 -env_list = py{37,38,39,310,311,312}, lint, mypy, html +env_list = py{37,38,39,310,311,312}, mypy, html [testenv] description = Run unit tests @@ -12,8 +12,6 @@ commands = pytest --color=yes {posargs} [testenv:lint] description = Lint via pre-commit base_python = py{39,310,311,312,38,37} -set_env = - SKIP = black-format commands = pre-commit run --all-files --hook-stage manual [testenv:mypy]