From bf7af69306ed8f14b33528165473ca3591a76246 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 21:29:33 -0400 Subject: [PATCH 1/5] Upgrade flake8 in pre-commit and fix new warnings Upgrading flake8 from 6.0.0 to 6.1.0 causes its pycodestyle dependency to be upgraded from 2.10.* to 2.11.*, which is desirable because: - Spurious "E231 missing whitespace after ':'" warnings on 3.12 due to the lack of full compatibility with Python 3.12 are gone. - New warnings appear, at least one of which, "E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`", does identify something we can improve. This upgrades flake8 in pre-commit and fixes the new warnings. --- .pre-commit-config.yaml | 2 +- git/objects/submodule/base.py | 2 +- git/refs/log.py | 2 +- git/util.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 581cb69b2..888e62d91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 0d20305c6..c7e7856f0 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -1403,7 +1403,7 @@ def iter_items( # END handle critical error # Make sure we are looking at a submodule object - if type(sm) != git.objects.submodule.base.Submodule: + if type(sm) is not git.objects.submodule.base.Submodule: continue # fill in remaining info - saves time as it doesn't have to be parsed again diff --git a/git/refs/log.py b/git/refs/log.py index 1f86356a4..1c2a2c470 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -244,7 +244,7 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry": for i in range(index + 1): line = fp.readline() if not line: - raise IndexError(f"Index file ended at line {i+1}, before given index was reached") + raise IndexError(f"Index file ended at line {i + 1}, before given index was reached") # END abort on eof # END handle runup diff --git a/git/util.py b/git/util.py index 638807a36..48901ba0c 100644 --- a/git/util.py +++ b/git/util.py @@ -1136,7 +1136,7 @@ class IterableClassWatcher(type): def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: for base in bases: - if type(base) == IterableClassWatcher: + if type(base) is IterableClassWatcher: warnings.warn( f"GitPython Iterable subclassed by {name}. " "Iterable is deprecated due to naming clash since v3.1.18" From c1ec9cbd2a995585e53c90d61ae30102aaaff2a5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 22:25:40 -0400 Subject: [PATCH 2/5] Update flake8 additional dependencies, fix warning This bumps the versions of the flake8 plugins specified with pinned versions as additional dependencies of flake8 for pre-commit. Doing so gains a warning about a call to warnings.warn with no stacklevel argument. This appears to be the uncommon case where the implifit effect of stacklevel=1 is intended, so I have made that explicit, which clarifies this intent and dismisses the warning. --- .pre-commit-config.yaml | 4 ++-- git/repo/base.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 888e62d91..4c92656e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,8 @@ repos: - id: flake8 additional_dependencies: [ - flake8-bugbear==22.12.6, - flake8-comprehensions==3.10.1, + flake8-bugbear==23.9.16, + flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] exclude: ^doc|^git/ext/|^test/ diff --git a/git/repo/base.py b/git/repo/base.py index fda3cdc88..bc1b8876d 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -206,7 +206,8 @@ def __init__( if expand_vars and re.search(self.re_envvars, epath): warnings.warn( "The use of environment variables in paths is deprecated" - + "\nfor security reasons and may be removed in the future!!" + + "\nfor security reasons and may be removed in the future!!", + stacklevel=1, ) epath = expand_path(epath, expand_vars) if epath is not None: From 48441a94189494789f751e75f9e1919b9b700b13 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Wed, 20 Sep 2023 23:40:44 -0400 Subject: [PATCH 3/5] Lint test/ (not just git/), fix warnings and a bug This expands flake8 linting to include the test suite, and fixes the resulting warnings. Four code changes are especially notable: - Unit tests from which documentation is automatically generated contain a few occurrences of "# @NoEffect". These suppressions are extended to "# noqa: B015 # @NoEffect" so the corresponding suppression is also applied for flake8. This is significant because it actually changes what appears in the code examples in the generated documentation. However, since the "@NoEffect" annotation was considered acceptable, this may be okay too. The resulting examples did not become excessively long. - Expressions like "[c for c in commits_for_file_generator]" appear in some unit tests from which documentation is automatically generated. The simpler form "list(commits_for_file_generator)" can replace them. This changes the examples in the documentation, but for the better, since that form is simpler (and also a better practice in general, thus a better thing to show readers). So I made those substitutions. - In test_repo.TestRepo.test_git_work_tree_env, the code intended to unpatch environment variables after the test was ineffective, instead causing os.environ to refer to an ordinary dict object that does not affect environment variables when written to. This uses unittest.mock.patch.dict instead, so the variables are unpatched and subsequent writes to environment variables in the test process are effective. - In test_submodule.TestSubmodule._do_base_tests, the expression statement "csm.module().head.ref.tracking_branch() is not None" appeared to be intended as an assertion, in spite of having been annoated @NoEffect. This is in light of the context and because, if the goal were only to exercise the function call, the "is not None" part would be superfluous. So I made it an assertion. --- .flake8 | 2 +- .pre-commit-config.yaml | 2 +- test/test_commit.py | 14 +++++++------- test/test_diff.py | 1 - test/test_docs.py | 8 ++++---- test/test_quick_doc.py | 14 ++++++-------- test/test_remote.py | 2 +- test/test_repo.py | 16 +++++++--------- test/test_submodule.py | 9 +++++---- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/.flake8 b/.flake8 index 08001ffac..ed5d036bf 100644 --- a/.flake8 +++ b/.flake8 @@ -26,7 +26,7 @@ ignore = E265,E266,E731,E704, D, RST, RST3 -exclude = .tox,.venv,build,dist,doc,git/ext/,test +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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c92656e4..5a34b8af0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: flake8-comprehensions==3.14.0, flake8-typing-imports==1.14.0, ] - exclude: ^doc|^git/ext/|^test/ + exclude: ^doc|^git/ext/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/test/test_commit.py b/test/test_commit.py index f6fb49d50..527aea334 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -277,7 +277,7 @@ def __init__(self, *args, **kwargs): super(Child, self).__init__(*args, **kwargs) child_commits = list(Child.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS"))) - assert type(child_commits[0]) == Child + assert type(child_commits[0]) is Child def test_iter_items(self): # pretty not allowed @@ -525,12 +525,12 @@ def test_trailers(self): # check that trailer stays empty for multiple msg combinations msgs = [ - f"Subject\n", - f"Subject\n\nBody with some\nText\n", - f"Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", - f"Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", - f"Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", - f"Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n", + "Subject\n", + "Subject\n\nBody with some\nText\n", + "Subject\n\nBody with\nText\n\nContinuation but\n doesn't contain colon\n", + "Subject\n\nBody with\nText\n\nContinuation but\n only contains one :\n", + "Subject\n\nBody with\nText\n\nKey: Value\nLine without colon\n", + "Subject\n\nBody with\nText\n\nLine without colon\nKey: Value\n", ] for msg in msgs: diff --git a/test/test_diff.py b/test/test_diff.py index 9c3888f03..5aa4408bf 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -7,7 +7,6 @@ import ddt import shutil import tempfile -import unittest from git import ( Repo, GitCommandError, diff --git a/test/test_docs.py b/test/test_docs.py index 4c23e9f81..d6b88855f 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -263,9 +263,9 @@ def test_references_and_objects(self, rw_dir): # [8-test_references_and_objects] hc = repo.head.commit hct = hc.tree - hc != hct # @NoEffect - hc != repo.tags[0] # @NoEffect - hc == repo.head.reference.commit # @NoEffect + hc != hct # noqa: B015 # @NoEffect + hc != repo.tags[0] # noqa: B015 # @NoEffect + hc == repo.head.reference.commit # noqa: B015 # @NoEffect # ![8-test_references_and_objects] # [9-test_references_and_objects] @@ -369,7 +369,7 @@ def test_references_and_objects(self, rw_dir): # The index contains all blobs in a flat list assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == "blob"]) # Access blob objects - for (_path, _stage), entry in index.entries.items(): + for (_path, _stage), _entry in index.entries.items(): pass new_file_path = os.path.join(repo.working_tree_dir, "new-file-name") open(new_file_path, "w").close() diff --git a/test/test_quick_doc.py b/test/test_quick_doc.py index 9dc7b8d2e..342a7f293 100644 --- a/test/test_quick_doc.py +++ b/test/test_quick_doc.py @@ -1,6 +1,3 @@ -import pytest - - from test.lib import TestBase from test.lib.helper import with_rw_directory @@ -25,10 +22,11 @@ def test_init_repo_object(self, path_to_dir): repo = Repo(path_to_dir) # ![2-test_init_repo_object] + del repo # Avoids "assigned to but never used" warning. Doesn't go in the docs. + @with_rw_directory def test_cloned_repo_object(self, local_dir): from git import Repo - import git # code to clone from url # [1-test_cloned_repo_object] @@ -72,7 +70,7 @@ def test_cloned_repo_object(self, local_dir): # [6-test_cloned_repo_object] commits_for_file_generator = repo.iter_commits(all=True, max_count=10, paths=update_file) - commits_for_file = [c for c in commits_for_file_generator] + commits_for_file = list(commits_for_file_generator) commits_for_file # Outputs: [, @@ -136,7 +134,7 @@ def test_cloned_repo_object(self, local_dir): # Compare commit to commit # [11.3-test_cloned_repo_object] - first_commit = [c for c in repo.iter_commits(all=True)][-1] + first_commit = list(repo.iter_commits(all=True))[-1] diffs = repo.head.commit.diff(first_commit) for d in diffs: print(d.a_path) @@ -154,7 +152,7 @@ def test_cloned_repo_object(self, local_dir): # Previous commit tree # [13-test_cloned_repo_object] - prev_commits = [c for c in repo.iter_commits(all=True, max_count=10)] # last 10 commits from all branches + prev_commits = list(repo.iter_commits(all=True, max_count=10)) # last 10 commits from all branches tree = prev_commits[0].tree # ![13-test_cloned_repo_object] @@ -210,7 +208,7 @@ def print_files_from_git(root, level=0): # print previous tree # [18.1-test_cloned_repo_object] - commits_for_file = [c for c in repo.iter_commits(all=True, paths=print_file)] + commits_for_file = list(repo.iter_commits(all=True, paths=print_file)) tree = commits_for_file[-1].tree # gets the first commit tree blob = tree[print_file] diff --git a/test/test_remote.py b/test/test_remote.py index e0dcb4131..7144b2791 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -160,7 +160,7 @@ def _do_test_push_result(self, results, remote): # END error checking # END for each info - if any([info.flags & info.ERROR for info in results]): + if any(info.flags & info.ERROR for info in results): self.assertRaises(GitCommandError, results.raise_if_error) else: # No errors, so this should do nothing diff --git a/test/test_repo.py b/test/test_repo.py index 6432b8c6f..d3bc864cd 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -252,7 +252,8 @@ def test_clone_from_with_path_contains_unicode(self): @with_rw_directory @skip( - "the referenced repository was removed, and one needs to setup a new password controlled repo under the orgs control" + """The referenced repository was removed, and one needs to set up a new + password controlled repo under the org's control.""" ) def test_leaking_password_in_clone_logs(self, rw_dir): password = "fakepassword1234" @@ -758,9 +759,9 @@ def test_blame_complex_revision(self, git): @mock.patch.object(Git, "_call_process") def test_blame_accepts_rev_opts(self, git): - res = self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] boilerplate_kwargs = {"p": True, "stdout_as_string": False} + self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) git.assert_called_once_with(*expected_args, **boilerplate_kwargs) @skipIf( @@ -971,7 +972,7 @@ def _assert_rev_parse(self, name): # history with number ni = 11 history = [obj.parents[0]] - for pn in range(ni): + for _ in range(ni): history.append(history[-1].parents[0]) # END get given amount of commits @@ -1329,6 +1330,7 @@ def test_git_work_tree_env(self, rw_dir): # move .git directory to a subdirectory # set GIT_DIR and GIT_WORK_TREE appropriately # check that repo.working_tree_dir == rw_dir + self.rorepo.clone(join_path_native(rw_dir, "master_repo")) repo_dir = join_path_native(rw_dir, "master_repo") @@ -1338,16 +1340,12 @@ def test_git_work_tree_env(self, rw_dir): os.mkdir(new_subdir) os.rename(old_git_dir, new_git_dir) - oldenv = os.environ.copy() - os.environ["GIT_DIR"] = new_git_dir - os.environ["GIT_WORK_TREE"] = repo_dir + to_patch = {"GIT_DIR": new_git_dir, "GIT_WORK_TREE": repo_dir} - try: + with mock.patch.dict(os.environ, to_patch): r = Repo() self.assertEqual(r.working_tree_dir, repo_dir) self.assertEqual(r.working_dir, repo_dir) - finally: - os.environ = oldenv @with_rw_directory def test_rebasing(self, rw_dir): diff --git a/test/test_submodule.py b/test/test_submodule.py index 88717e220..35ff0d7a8 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -111,7 +111,7 @@ def _do_base_tests(self, rwrepo): # force it to reread its information del smold._url - smold.url == sm.url # @NoEffect + smold.url == sm.url # noqa: B015 # @NoEffect # test config_reader/writer methods sm.config_reader() @@ -248,7 +248,7 @@ def _do_base_tests(self, rwrepo): assert csm.module_exists() # tracking branch once again - csm.module().head.ref.tracking_branch() is not None # @NoEffect + assert csm.module().head.ref.tracking_branch() is not None # this flushed in a sub-submodule assert len(list(rwrepo.iter_submodules())) == 2 @@ -480,8 +480,9 @@ def test_base_bare(self, rwrepo): File "C:\\projects\\gitpython\\git\\cmd.py", line 559, in execute raise GitCommandNotFound(command, err) git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid') - cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module""", - ) # noqa E501 + cmdline: git clone -n --shared -v C:\\projects\\gitpython\\.git Users\\appveyor\\AppData\\Local\\Temp\\1\\tmplyp6kr_rnon_bare_test_root_module + """, # noqa E501 + ) @with_rw_repo(k_subm_current, bare=False) def test_root_module(self, rwrepo): # Can query everything without problems From 98877c58cd402ea94d4235ab9d4074db76dca74d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 02:46:48 -0400 Subject: [PATCH 4/5] Refactor "finally" cleanup in tests, fix minor bug This refactors code in the test suite that uses try-finally, to simplify and clarify what it is doing. An exception to this being a refactoring is that a possible bug is fixed where a throwaway environment variable "FOO" was patched for a test and never unpatched. There are two patterns refactored here: - try-(try-except)-finally to try-except-finally. When the entire suite of the try-block in try-finally is itself a try-except, that has the same effect as try-except-finally. (Python always attempts to run the finally suite in either case, and does so after any applicable except suite is run.) - Replacing try-finally with an appropriate context manager. (These changes do not fix, nor introduce, any flake8 errors, but they were found by manual search for possible problems related to recent flake8 findings but outside of what flake8 can catch. To limit scope, this only refactors try-finally in the test suite.) --- test/lib/helper.py | 32 +++++++++++++++----------------- test/test_git.py | 17 ++++++----------- test/test_repo.py | 9 ++------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/test/lib/helper.py b/test/lib/helper.py index 64c70a8f4..e8464b7d4 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -94,17 +94,16 @@ def wrapper(self): os.mkdir(path) keep = False try: - try: - return func(self, path) - except Exception: - log.info( - "Test %s.%s failed, output is at %r\n", - type(self).__name__, - func.__name__, - path, - ) - keep = True - raise + return func(self, path) + except Exception: + log.info( + "Test %s.%s failed, output is at %r\n", + type(self).__name__, + func.__name__, + path, + ) + keep = True + raise finally: # Need to collect here to be sure all handles have been closed. It appears # a windows-only issue. In fact things should be deleted, as well as @@ -147,12 +146,11 @@ def repo_creator(self): prev_cwd = os.getcwd() os.chdir(rw_repo.working_dir) try: - try: - return func(self, rw_repo) - except: # noqa E722 - log.info("Keeping repo after failure: %s", repo_dir) - repo_dir = None - raise + return func(self, rw_repo) + except: # noqa E722 + log.info("Keeping repo after failure: %s", repo_dir) + repo_dir = None + raise finally: os.chdir(prev_cwd) rw_repo.git.clear_cache() diff --git a/test/test_git.py b/test/test_git.py index 2c392155a..31065d8fd 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -195,17 +195,12 @@ def test_version(self): # END verify number types def test_cmd_override(self): - prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE - exc = GitCommandNotFound - try: - # set it to something that doesn't exist, assure it raises - type(self.git).GIT_PYTHON_GIT_EXECUTABLE = osp.join( - "some", "path", "which", "doesn't", "exist", "gitbinary" - ) - self.assertRaises(exc, self.git.version) - finally: - type(self.git).GIT_PYTHON_GIT_EXECUTABLE = prev_cmd - # END undo adjustment + with mock.patch.object( + type(self.git), + "GIT_PYTHON_GIT_EXECUTABLE", + osp.join("some", "path", "which", "doesn't", "exist", "gitbinary"), + ): + self.assertRaises(GitCommandNotFound, self.git.version) def test_refresh(self): # test a bad git path refresh diff --git a/test/test_repo.py b/test/test_repo.py index d3bc864cd..15899ec50 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -847,18 +847,13 @@ def test_comparison_and_hash(self): @with_rw_directory def test_tilde_and_env_vars_in_repo_path(self, rw_dir): - ph = os.environ.get("HOME") - try: + with mock.patch.dict(os.environ, {"HOME": rw_dir}): os.environ["HOME"] = rw_dir Repo.init(osp.join("~", "test.git"), bare=True) + with mock.patch.dict(os.environ, {"FOO": rw_dir}): os.environ["FOO"] = rw_dir Repo.init(osp.join("$FOO", "test.git"), bare=True) - finally: - if ph: - os.environ["HOME"] = ph - del os.environ["FOO"] - # end assure HOME gets reset to what it was def test_git_cmd(self): # test CatFileContentStream, just to be very sure we have no fencepost errors From c5693204f772102f289c2e4269a399370a38d82b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Thu, 21 Sep 2023 06:40:03 -0400 Subject: [PATCH 5/5] Make an old mock.patch.dict on os.environ clearer This clarifies that the object's contents are patched, rather than patching the attribute used to get the object in the first place. It is also in keeping with the style of patching os.environ elsewhere in the test suite. --- test/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_git.py b/test/test_git.py index 31065d8fd..481309538 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -245,7 +245,7 @@ def test_insert_after_kwarg_raises(self): def test_env_vars_passed_to_git(self): editor = "non_existent_editor" - with mock.patch.dict("os.environ", {"GIT_EDITOR": editor}): # @UndefinedVariable + with mock.patch.dict(os.environ, {"GIT_EDITOR": editor}): self.assertEqual(self.git.var("GIT_EDITOR"), editor) @with_rw_directory