Browse Source

Upgrade to >= python 3.7

Jelmer Vernooij 1 year ago
parent
commit
2fab3300ab

+ 7 - 7
dulwich/cli.py

@@ -132,7 +132,7 @@ class cmd_fetch(Command):
         refs = client.fetch(path, r, progress=sys.stdout.write)
         refs = client.fetch(path, r, progress=sys.stdout.write)
         print("Remote refs:")
         print("Remote refs:")
         for item in refs.items():
         for item in refs.items():
-            print("%s -> %s" % item)
+            print("{} -> {}".format(*item))
 
 
 
 
 class cmd_fsck(Command):
 class cmd_fsck(Command):
@@ -140,7 +140,7 @@ class cmd_fsck(Command):
         opts, args = getopt(args, "", [])
         opts, args = getopt(args, "", [])
         opts = dict(opts)
         opts = dict(opts)
         for (obj, msg) in porcelain.fsck("."):
         for (obj, msg) in porcelain.fsck("."):
-            print("{}: {}".format(obj, msg))
+            print(f"{obj}: {msg}")
 
 
 
 
 class cmd_log(Command):
 class cmd_log(Command):
@@ -203,9 +203,9 @@ class cmd_dump_pack(Command):
             try:
             try:
                 print("\t%s" % x[name])
                 print("\t%s" % x[name])
             except KeyError as k:
             except KeyError as k:
-                print("\t{}: Unable to resolve base {}".format(name, k))
+                print(f"\t{name}: Unable to resolve base {k}")
             except ApplyDeltaError as e:
             except ApplyDeltaError as e:
-                print("\t{}: Unable to apply delta: {!r}".format(name, e))
+                print(f"\t{name}: Unable to apply delta: {e!r}")
 
 
 
 
 class cmd_dump_index(Command):
 class cmd_dump_index(Command):
@@ -488,7 +488,7 @@ class cmd_status(Command):
             for kind, names in status.staged.items():
             for kind, names in status.staged.items():
                 for name in names:
                 for name in names:
                     sys.stdout.write(
                     sys.stdout.write(
-                        "\t{}: {}\n".format(kind, name.decode(sys.getfilesystemencoding()))
+                        f"\t{kind}: {name.decode(sys.getfilesystemencoding())}\n"
                     )
                     )
             sys.stdout.write("\n")
             sys.stdout.write("\n")
         if status.unstaged:
         if status.unstaged:
@@ -511,7 +511,7 @@ class cmd_ls_remote(Command):
             sys.exit(1)
             sys.exit(1)
         refs = porcelain.ls_remote(args[0])
         refs = porcelain.ls_remote(args[0])
         for ref in sorted(refs):
         for ref in sorted(refs):
-            sys.stdout.write("{}\t{}\n".format(ref, refs[ref]))
+            sys.stdout.write(f"{ref}\t{refs[ref]}\n")
 
 
 
 
 class cmd_ls_tree(Command):
 class cmd_ls_tree(Command):
@@ -635,7 +635,7 @@ class cmd_submodule_list(Command):
         parser = argparse.ArgumentParser()
         parser = argparse.ArgumentParser()
         parser.parse_args(argv)
         parser.parse_args(argv)
         for path, sha in porcelain.submodule_list("."):
         for path, sha in porcelain.submodule_list("."):
-            sys.stdout.write(' {} {}\n'.format(sha, path))
+            sys.stdout.write(f' {sha} {path}\n')
 
 
 
 
 class cmd_submodule_init(Command):
 class cmd_submodule_init(Command):

+ 9 - 26
dulwich/client.py

@@ -49,7 +49,6 @@ from io import BufferedReader, BytesIO
 from typing import (
 from typing import (
     IO,
     IO,
     TYPE_CHECKING,
     TYPE_CHECKING,
-    Any,
     Callable,
     Callable,
     Dict,
     Dict,
     Iterable,
     Iterable,
@@ -407,7 +406,7 @@ class SendPackResult:
         return super().__getattribute__(name)
         return super().__getattribute__(name)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "{}({!r}, {!r})".format(self.__class__.__name__, self.refs, self.agent)
+        return f"{self.__class__.__name__}({self.refs!r}, {self.agent!r})"
 
 
 
 
 def _read_shallow_updates(pkt_seq):
 def _read_shallow_updates(pkt_seq):
@@ -454,12 +453,12 @@ class _v1ReceivePackHeader:
             old_sha1 = old_refs.get(refname, ZERO_SHA)
             old_sha1 = old_refs.get(refname, ZERO_SHA)
             if not isinstance(old_sha1, bytes):
             if not isinstance(old_sha1, bytes):
                 raise TypeError(
                 raise TypeError(
-                    "old sha1 for {!r} is not a bytestring: {!r}".format(refname, old_sha1)
+                    f"old sha1 for {refname!r} is not a bytestring: {old_sha1!r}"
                 )
                 )
             new_sha1 = new_refs.get(refname, ZERO_SHA)
             new_sha1 = new_refs.get(refname, ZERO_SHA)
             if not isinstance(new_sha1, bytes):
             if not isinstance(new_sha1, bytes):
                 raise TypeError(
                 raise TypeError(
-                    "old sha1 for {!r} is not a bytestring {!r}".format(refname, new_sha1)
+                    f"old sha1 for {refname!r} is not a bytestring {new_sha1!r}"
                 )
                 )
 
 
             if old_sha1 != new_sha1:
             if old_sha1 != new_sha1:
@@ -510,9 +509,6 @@ def _handle_upload_pack_head(
       can_read: function that returns a boolean that indicates
       can_read: function that returns a boolean that indicates
     whether there is extra graph data to read on proto
     whether there is extra graph data to read on proto
       depth: Depth for request
       depth: Depth for request
-
-    Returns:
-
     """
     """
     assert isinstance(wants, list) and isinstance(wants[0], bytes)
     assert isinstance(wants, list) and isinstance(wants[0], bytes)
     proto.write_pkt_line(
     proto.write_pkt_line(
@@ -582,9 +578,6 @@ def _handle_upload_pack_tail(
       pack_data: Function to call with pack data
       pack_data: Function to call with pack data
       progress: Optional progress reporting function
       progress: Optional progress reporting function
       rbufsize: Read buffer size
       rbufsize: Read buffer size
-
-    Returns:
-
     """
     """
     pkt = proto.read_pkt_line()
     pkt = proto.read_pkt_line()
     while pkt:
     while pkt:
@@ -866,9 +859,6 @@ class GitClient:
 
 
         Args:
         Args:
           path: Path to the repo to fetch from. (as bytestring)
           path: Path to the repo to fetch from. (as bytestring)
-
-        Returns:
-
         """
         """
         raise NotImplementedError(self.get_refs)
         raise NotImplementedError(self.get_refs)
 
 
@@ -975,9 +965,6 @@ def check_wants(wants, refs):
     Args:
     Args:
       wants: Set of object SHAs to fetch
       wants: Set of object SHAs to fetch
       refs: Refs dictionary to check against
       refs: Refs dictionary to check against
-
-    Returns:
-
     """
     """
     missing = set(wants) - {
     missing = set(wants) - {
         v for (k, v) in refs.items() if not k.endswith(PEELED_TAG_SUFFIX)
         v for (k, v) in refs.items() if not k.endswith(PEELED_TAG_SUFFIX)
@@ -1269,7 +1256,7 @@ class TCPGitClient(TraditionalGitClient):
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM
         )
         )
         s = None
         s = None
-        err = socket.error("no address found for %s" % self._host)
+        err = OSError("no address found for %s" % self._host)
         for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
         for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
             s = socket.socket(family, socktype, proto)
             s = socket.socket(family, socktype, proto)
             s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
             s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -1465,7 +1452,7 @@ class LocalGitClient(GitClient):
                 old_sha1 = old_refs.get(refname, ZERO_SHA)
                 old_sha1 = old_refs.get(refname, ZERO_SHA)
                 if new_sha1 != ZERO_SHA:
                 if new_sha1 != ZERO_SHA:
                     if not target.refs.set_if_equals(refname, old_sha1, new_sha1):
                     if not target.refs.set_if_equals(refname, old_sha1, new_sha1):
-                        msg = "unable to set {} to {}".format(refname, new_sha1)
+                        msg = f"unable to set {refname} to {new_sha1}"
                         progress(msg)
                         progress(msg)
                         ref_status[refname] = msg
                         ref_status[refname] = msg
                 else:
                 else:
@@ -1577,9 +1564,6 @@ class SSHVendor:
           password: Optional ssh password for login or private key
           password: Optional ssh password for login or private key
           key_filename: Optional path to private keyfile
           key_filename: Optional path to private keyfile
           ssh_command: Optional SSH command
           ssh_command: Optional SSH command
-
-        Returns:
-
         """
         """
         raise NotImplementedError(self.run_command)
         raise NotImplementedError(self.run_command)
 
 
@@ -1624,7 +1608,7 @@ class SubprocessSSHVendor(SSHVendor):
             args.extend(["-i", str(key_filename)])
             args.extend(["-i", str(key_filename)])
 
 
         if username:
         if username:
-            host = "{}@{}".format(username, host)
+            host = f"{username}@{host}"
         if host.startswith("-"):
         if host.startswith("-"):
             raise StrangeHostname(hostname=host)
             raise StrangeHostname(hostname=host)
         args.append(host)
         args.append(host)
@@ -1678,7 +1662,7 @@ class PLinkSSHVendor(SSHVendor):
             args.extend(["-i", str(key_filename)])
             args.extend(["-i", str(key_filename)])
 
 
         if username:
         if username:
-            host = "{}@{}".format(username, host)
+            host = f"{username}@{host}"
         if host.startswith("-"):
         if host.startswith("-"):
             raise StrangeHostname(hostname=host)
             raise StrangeHostname(hostname=host)
         args.append(host)
         args.append(host)
@@ -1981,8 +1965,7 @@ class AbstractHttpGitClient(GitClient):
             # Something changed (redirect!), so let's update the base URL
             # Something changed (redirect!), so let's update the base URL
             if not resp.redirect_location.endswith(tail):
             if not resp.redirect_location.endswith(tail):
                 raise GitProtocolError(
                 raise GitProtocolError(
-                    "Redirected from URL %s to URL %s without %s"
+                    f"Redirected from URL {url} to URL {resp.redirect_location} without {tail}"
-                    % (url, resp.redirect_location, tail)
                 )
                 )
             base_url = urljoin(url, resp.redirect_location[: -len(tail)])
             base_url = urljoin(url, resp.redirect_location[: -len(tail)])
 
 
@@ -2374,7 +2357,7 @@ def get_transport_and_path(
     location: str,
     location: str,
     config: Optional[Config] = None,
     config: Optional[Config] = None,
     operation: Optional[str] = None,
     operation: Optional[str] = None,
-    **kwargs: Any
+    **kwargs
 ) -> Tuple[GitClient, str]:
 ) -> Tuple[GitClient, str]:
     """Obtain a git client from a URL.
     """Obtain a git client from a URL.
 
 

+ 4 - 4
dulwich/config.py

@@ -30,7 +30,9 @@ import os
 import sys
 import sys
 from contextlib import suppress
 from contextlib import suppress
 from typing import (
 from typing import (
+    Any,
     BinaryIO,
     BinaryIO,
+    Dict,
     Iterable,
     Iterable,
     Iterator,
     Iterator,
     KeysView,
     KeysView,
@@ -40,8 +42,6 @@ from typing import (
     Tuple,
     Tuple,
     Union,
     Union,
     overload,
     overload,
-    Any,
-    Dict,
 )
 )
 
 
 from .file import GitFile
 from .file import GitFile
@@ -268,7 +268,7 @@ class ConfigDict(Config, MutableMapping[Section, MutableMapping[Name, Value]]):
         self._values = CaseInsensitiveOrderedMultiDict.make(values)
         self._values = CaseInsensitiveOrderedMultiDict.make(values)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "{}({!r})".format(self.__class__.__name__, self._values)
+        return f"{self.__class__.__name__}({self._values!r})"
 
 
     def __eq__(self, other: object) -> bool:
     def __eq__(self, other: object) -> bool:
         return isinstance(other, self.__class__) and other._values == self._values
         return isinstance(other, self.__class__) and other._values == self._values
@@ -688,7 +688,7 @@ class StackedConfig(Config):
         self.writable = writable
         self.writable = writable
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "<{} for {!r}>".format(self.__class__.__name__, self.backends)
+        return f"<{self.__class__.__name__} for {self.backends!r}>"
 
 
     @classmethod
     @classmethod
     def default(cls) -> "StackedConfig":
     def default(cls) -> "StackedConfig":

+ 3 - 5
dulwich/contrib/swift.py

@@ -258,8 +258,7 @@ class SwiftConnector:
         if ret.status_code < 200 or ret.status_code >= 300:
         if ret.status_code < 200 or ret.status_code >= 300:
             raise SwiftException(
             raise SwiftException(
                 "AUTH v1.0 request failed on "
                 "AUTH v1.0 request failed on "
-                + "%s with error code %s (%s)"
+                + "{} with error code {} ({})".format(
-                % (
                     str(auth_httpclient.get_base_url()) + path,
                     str(auth_httpclient.get_base_url()) + path,
                     ret.status_code,
                     ret.status_code,
                     str(ret.items()),
                     str(ret.items()),
@@ -294,8 +293,7 @@ class SwiftConnector:
         if ret.status_code < 200 or ret.status_code >= 300:
         if ret.status_code < 200 or ret.status_code >= 300:
             raise SwiftException(
             raise SwiftException(
                 "AUTH v2.0 request failed on "
                 "AUTH v2.0 request failed on "
-                + "%s with error code %s (%s)"
+                + "{} with error code {} ({})".format(
-                % (
                     str(auth_httpclient.get_base_url()) + path,
                     str(auth_httpclient.get_base_url()) + path,
                     ret.status_code,
                     ret.status_code,
                     str(ret.items()),
                     str(ret.items()),
@@ -495,7 +493,7 @@ class SwiftPackReader:
             self.buff_length = self.buff_length * 2
             self.buff_length = self.buff_length * 2
         offset = self.base_offset
         offset = self.base_offset
         r = min(self.base_offset + self.buff_length, self.pack_length)
         r = min(self.base_offset + self.buff_length, self.pack_length)
-        ret = self.scon.get_object(self.filename, range="{}-{}".format(offset, r))
+        ret = self.scon.get_object(self.filename, range=f"{offset}-{r}")
         self.buff = ret
         self.buff = ret
 
 
     def read(self, length):
     def read(self, length):

+ 1 - 1
dulwich/contrib/test_swift.py

@@ -171,7 +171,7 @@ def create_commit(data, marker=b"Default", blob=None):
 def create_commits(length=1, marker=b"Default"):
 def create_commits(length=1, marker=b"Default"):
     data = []
     data = []
     for i in range(0, length):
     for i in range(0, length):
-        _marker = ("{}_{}".format(marker, i)).encode()
+        _marker = (f"{marker}_{i}").encode()
         blob, tree, tag, cmt = create_commit(data, _marker)
         blob, tree, tag, cmt = create_commit(data, _marker)
         data.extend([blob, tree, tag, cmt])
         data.extend([blob, tree, tag, cmt])
     return data
     return data

+ 3 - 3
dulwich/errors.py

@@ -43,12 +43,12 @@ class ChecksumMismatch(Exception):
         if self.extra is None:
         if self.extra is None:
             Exception.__init__(
             Exception.__init__(
                 self,
                 self,
-                "Checksum mismatch: Expected {}, got {}".format(expected, got),
+                f"Checksum mismatch: Expected {expected}, got {got}",
             )
             )
         else:
         else:
             Exception.__init__(
             Exception.__init__(
                 self,
                 self,
-                "Checksum mismatch: Expected {}, got {}; {}".format(expected, got, extra),
+                f"Checksum mismatch: Expected {expected}, got {got}; {extra}",
             )
             )
 
 
 
 
@@ -64,7 +64,7 @@ class WrongObjectException(Exception):
     type_name: str
     type_name: str
 
 
     def __init__(self, sha, *args, **kwargs) -> None:
     def __init__(self, sha, *args, **kwargs) -> None:
-        Exception.__init__(self, "{} is not a {}".format(sha, self.type_name))
+        Exception.__init__(self, f"{sha} is not a {self.type_name}")
 
 
 
 
 class NotCommitError(WrongObjectException):
 class NotCommitError(WrongObjectException):

+ 3 - 3
dulwich/greenthreads.py

@@ -22,17 +22,17 @@
 
 
 """Utility module for querying an ObjectStore with gevent."""
 """Utility module for querying an ObjectStore with gevent."""
 
 
+from typing import FrozenSet, Optional, Set, Tuple
+
 import gevent
 import gevent
 from gevent import pool
 from gevent import pool
 
 
-from typing import Set, Tuple, Optional, FrozenSet
-
 from .object_store import (
 from .object_store import (
     MissingObjectFinder,
     MissingObjectFinder,
     _collect_ancestors,
     _collect_ancestors,
     _collect_filetree_revs,
     _collect_filetree_revs,
 )
 )
-from .objects import Commit, Tag, ObjectID
+from .objects import Commit, ObjectID, Tag
 
 
 
 
 def _split_commits_and_tags(obj_store, lst, *, ignore_unknown=False, pool=None):
 def _split_commits_and_tags(obj_store, lst, *, ignore_unknown=False, pool=None):

+ 1 - 1
dulwich/ignore.py

@@ -240,7 +240,7 @@ class IgnoreFilter:
     def __repr__(self) -> str:
     def __repr__(self) -> str:
         path = getattr(self, "_path", None)
         path = getattr(self, "_path", None)
         if path is not None:
         if path is not None:
-            return "{}.from_path({!r})".format(type(self).__name__, path)
+            return f"{type(self).__name__}.from_path({path!r})"
         else:
         else:
             return "<%s>" % (type(self).__name__)
             return "<%s>" % (type(self).__name__)
 
 

+ 1 - 1
dulwich/index.py

@@ -330,7 +330,7 @@ class Index:
         return self._filename
         return self._filename
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "{}({!r})".format(self.__class__.__name__, self._filename)
+        return f"{self.__class__.__name__}({self._filename!r})"
 
 
     def write(self) -> None:
     def write(self) -> None:
         """Write current contents of index to disk."""
         """Write current contents of index to disk."""

+ 6 - 6
dulwich/line_ending.py

@@ -17,7 +17,7 @@
 # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
 # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
 # License, Version 2.0.
 # License, Version 2.0.
 #
 #
-"""All line-ending related functions, from conversions to config processing.
+r"""All line-ending related functions, from conversions to config processing.
 
 
 Line-ending normalization is a complex beast. Here is some notes and details
 Line-ending normalization is a complex beast. Here is some notes and details
 about how it seems to work.
 about how it seems to work.
@@ -61,23 +61,23 @@ There is multiple variables that impact the normalization.
 First, a repository can contains a ``.gitattributes`` file (or more than one...)
 First, a repository can contains a ``.gitattributes`` file (or more than one...)
 that can further customize the operation on some file patterns, for example:
 that can further customize the operation on some file patterns, for example:
 
 
-    \\*.txt text
+    \*.txt text
 
 
 Force all ``.txt`` files to be treated as text files and to have their lines
 Force all ``.txt`` files to be treated as text files and to have their lines
 endings normalized.
 endings normalized.
 
 
-    \\*.jpg -text
+    \*.jpg -text
 
 
 Force all ``.jpg`` files to be treated as binary files and to not have their
 Force all ``.jpg`` files to be treated as binary files and to not have their
 lines endings converted.
 lines endings converted.
 
 
-    \\*.vcproj text eol=crlf
+    \*.vcproj text eol=crlf
 
 
 Force all ``.vcproj`` files to be treated as text files and to have their lines
 Force all ``.vcproj`` files to be treated as text files and to have their lines
 endings converted into ``CRLF`` in working directory no matter the native EOL of
 endings converted into ``CRLF`` in working directory no matter the native EOL of
 the platform.
 the platform.
 
 
-    \\*.sh text eol=lf
+    \*.sh text eol=lf
 
 
 Force all ``.sh`` files to be treated as text files and to have their lines
 Force all ``.sh`` files to be treated as text files and to have their lines
 endings converted into ``LF`` in working directory no matter the native EOL of
 endings converted into ``LF`` in working directory no matter the native EOL of
@@ -86,7 +86,7 @@ the platform.
 If the ``eol`` attribute is not defined, Git uses the ``core.eol`` configuration
 If the ``eol`` attribute is not defined, Git uses the ``core.eol`` configuration
 value described later.
 value described later.
 
 
-    \\* text=auto
+    \* text=auto
 
 
 Force all files to be scanned by the text file heuristic detection and to have
 Force all files to be scanned by the text file heuristic detection and to have
 their line endings normalized in case they are detected as text files.
 their line endings normalized in case they are detected as text files.

+ 5 - 5
dulwich/lru_cache.py

@@ -131,31 +131,31 @@ class LRUCache(Generic[K, V]):
                 raise AssertionError(
                 raise AssertionError(
                     "the _most_recently_used entry is not"
                     "the _most_recently_used entry is not"
                     " supposed to have a previous entry"
                     " supposed to have a previous entry"
-                    " %s" % (node,)
+                    " {}".format(node)
                 )
                 )
         while node is not None:
         while node is not None:
             if node.next_key is _null_key:
             if node.next_key is _null_key:
                 if node is not self._least_recently_used:
                 if node is not self._least_recently_used:
                     raise AssertionError(
                     raise AssertionError(
-                        "only the last node should have" " no next value: %s" % (node,)
+                        "only the last node should have" " no next value: {}".format(node)
                     )
                     )
                 node_next = None
                 node_next = None
             else:
             else:
                 node_next = self._cache[node.next_key]
                 node_next = self._cache[node.next_key]
                 if node_next.prev is not node:
                 if node_next.prev is not node:
                     raise AssertionError(
                     raise AssertionError(
-                        "inconsistency found, node.next.prev" " != node: %s" % (node,)
+                        "inconsistency found, node.next.prev" " != node: {}".format(node)
                     )
                     )
             if node.prev is None:
             if node.prev is None:
                 if node is not self._most_recently_used:
                 if node is not self._most_recently_used:
                     raise AssertionError(
                     raise AssertionError(
                         "only the _most_recently_used should"
                         "only the _most_recently_used should"
-                        " not have a previous node: %s" % (node,)
+                        " not have a previous node: {}".format(node)
                     )
                     )
             else:
             else:
                 if node.prev.next_key != node.key:
                 if node.prev.next_key != node.key:
                     raise AssertionError(
                     raise AssertionError(
-                        "inconsistency found, node.prev.next" " != node: %s" % (node,)
+                        "inconsistency found, node.prev.next" " != node: {}".format(node)
                     )
                     )
             yield node
             yield node
             node = node_next
             node = node_next

+ 1 - 1
dulwich/mailmap.py

@@ -20,7 +20,7 @@
 
 
 """Mailmap file reader."""
 """Mailmap file reader."""
 
 
-from typing import Dict, Tuple, Optional
+from typing import Dict, Optional, Tuple
 
 
 
 
 def parse_identity(text):
 def parse_identity(text):

+ 4 - 4
dulwich/object_store.py

@@ -570,7 +570,7 @@ class PackBasedObjectStore(BaseObjectStore):
             sha = name
             sha = name
             hexsha = None
             hexsha = None
         else:
         else:
-            raise AssertionError("Invalid object name {!r}".format(name))
+            raise AssertionError(f"Invalid object name {name!r}")
         for pack in self._iter_cached_packs():
         for pack in self._iter_cached_packs():
             try:
             try:
                 return pack.get_raw(sha)
                 return pack.get_raw(sha)
@@ -653,7 +653,7 @@ class PackBasedObjectStore(BaseObjectStore):
             sha = sha1
             sha = sha1
             hexsha = None
             hexsha = None
         else:
         else:
-            raise AssertionError("Invalid object sha1 {!r}".format(sha1))
+            raise AssertionError(f"Invalid object sha1 {sha1!r}")
         for pack in self._iter_cached_packs():
         for pack in self._iter_cached_packs():
             try:
             try:
                 return pack.get_unpacked_object(sha, include_comp=include_comp)
                 return pack.get_unpacked_object(sha, include_comp=include_comp)
@@ -711,7 +711,7 @@ class DiskObjectStore(PackBasedObjectStore):
         self.pack_compression_level = pack_compression_level
         self.pack_compression_level = pack_compression_level
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "<{}({!r})>".format(self.__class__.__name__, self.path)
+        return f"<{self.__class__.__name__}({self.path!r})>"
 
 
     @classmethod
     @classmethod
     def from_config(cls, path, config):
     def from_config(cls, path, config):
@@ -1005,7 +1005,7 @@ class MemoryObjectStore(BaseObjectStore):
         elif len(sha) == 20:
         elif len(sha) == 20:
             return sha_to_hex(sha)
             return sha_to_hex(sha)
         else:
         else:
-            raise ValueError("Invalid sha {!r}".format(sha))
+            raise ValueError(f"Invalid sha {sha!r}")
 
 
     def contains_loose(self, sha):
     def contains_loose(self, sha):
         """Check if a particular object is present by SHA1 and is loose."""
         """Check if a particular object is present by SHA1 and is loose."""

+ 3 - 3
dulwich/objects.py

@@ -204,7 +204,7 @@ def check_hexsha(hex, error_msg):
       ObjectFormatException: Raised when the string is not valid
       ObjectFormatException: Raised when the string is not valid
     """
     """
     if not valid_hexsha(hex):
     if not valid_hexsha(hex):
-        raise ObjectFormatException("{} {}".format(error_msg, hex))
+        raise ObjectFormatException(f"{error_msg} {hex}")
 
 
 
 
 def check_identity(identity: bytes, error_msg: str) -> None:
 def check_identity(identity: bytes, error_msg: str) -> None:
@@ -559,7 +559,7 @@ class ShaFile:
         return self.sha().hexdigest().encode("ascii")
         return self.sha().hexdigest().encode("ascii")
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "<{} {}>".format(self.__class__.__name__, self.id)
+        return f"<{self.__class__.__name__} {self.id}>"
 
 
     def __ne__(self, other):
     def __ne__(self, other):
         """Check whether this object does not match the other."""
         """Check whether this object does not match the other."""
@@ -1237,7 +1237,7 @@ def parse_timezone(text):
     #  as an integer (using strtol), which could also be negative.
     #  as an integer (using strtol), which could also be negative.
     #  We do the same for compatibility. See #697828.
     #  We do the same for compatibility. See #697828.
     if text[0] not in b"+-":
     if text[0] not in b"+-":
-        raise ValueError("Timezone must start with + or - (%(text)s)" % vars())
+        raise ValueError("Timezone must start with + or - ({text})".format(**vars()))
     sign = text[:1]
     sign = text[:1]
     offset = int(text[1:])
     offset = int(text[1:])
     if sign == b"-":
     if sign == b"-":

+ 2 - 2
dulwich/pack.py

@@ -259,7 +259,7 @@ class UnpackedObject:
         return not (self == other)
         return not (self == other)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        data = ["{}={!r}".format(s, getattr(self, s)) for s in self.__slots__]
+        data = [f"{s}={getattr(self, s)!r}" for s in self.__slots__]
         return "{}({})".format(self.__class__.__name__, ", ".join(data))
         return "{}({})".format(self.__class__.__name__, ", ".join(data))
 
 
 
 
@@ -2346,7 +2346,7 @@ class Pack:
         return len(self.index)
         return len(self.index)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "{}({!r})".format(self.__class__.__name__, self._basename)
+        return f"{self.__class__.__name__}({self._basename!r})"
 
 
     def __iter__(self):
     def __iter__(self):
         """Iterate over all the sha1s of the objects in this pack."""
         """Iterate over all the sha1s of the objects in this pack."""

+ 1 - 1
dulwich/patch.py

@@ -102,7 +102,7 @@ def get_summary(commit):
 
 
 #  Unified Diff
 #  Unified Diff
 def _format_range_unified(start, stop):
 def _format_range_unified(start, stop):
-    'Convert range to the "ed" format.'
+    """Convert range to the "ed" format."""
     # Per the diff spec at http://www.unix.org/single_unix_specification/
     # Per the diff spec at http://www.unix.org/single_unix_specification/
     beginning = start + 1  # lines start numbering with one
     beginning = start + 1  # lines start numbering with one
     length = stop - start
     length = stop - start

+ 1 - 1
dulwich/porcelain.py

@@ -1029,7 +1029,7 @@ def tag_create(
     sign=False,
     sign=False,
     encoding=DEFAULT_ENCODING
     encoding=DEFAULT_ENCODING
 ):
 ):
-    """Creates a tag in git via dulwich calls:
+    """Creates a tag in git via dulwich calls.
 
 
     Args:
     Args:
       repo: Path to repository
       repo: Path to repository

+ 1 - 2
dulwich/protocol.py

@@ -228,8 +228,7 @@ class Protocol:
         else:
         else:
             if len(pkt_contents) + 4 != size:
             if len(pkt_contents) + 4 != size:
                 raise GitProtocolError(
                 raise GitProtocolError(
-                    "Length of pkt read %04x does not match length prefix %04x"
+                    f"Length of pkt read {len(pkt_contents) + 4:04x} does not match length prefix {size:04x}"
-                    % (len(pkt_contents) + 4, size)
                 )
                 )
             return pkt_contents
             return pkt_contents
 
 

+ 3 - 4
dulwich/refs.py

@@ -23,7 +23,7 @@
 import os
 import os
 import warnings
 import warnings
 from contextlib import suppress
 from contextlib import suppress
-from typing import Dict, Optional, Set, Any
+from typing import Any, Dict, Optional, Set
 
 
 from .errors import PackedRefsException, RefFormatError
 from .errors import PackedRefsException, RefFormatError
 from .file import GitFile, ensure_dir_exists
 from .file import GitFile, ensure_dir_exists
@@ -628,7 +628,7 @@ class DiskRefsContainer(RefsContainer):
         self._peeled_refs = None
         self._peeled_refs = None
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
-        return "{}({!r})".format(self.__class__.__name__, self.path)
+        return f"{self.__class__.__name__}({self.path!r})"
 
 
     def subkeys(self, base):
     def subkeys(self, base):
         subkeys = set()
         subkeys = set()
@@ -1279,8 +1279,7 @@ def serialize_refs(store, refs):
             unpeeled, peeled = peel_sha(store, sha)
             unpeeled, peeled = peel_sha(store, sha)
         except KeyError:
         except KeyError:
             warnings.warn(
             warnings.warn(
-                "ref %s points at non-present sha %s"
+                "ref {} points at non-present sha {}".format(ref.decode("utf-8", "replace"), sha.decode("ascii")),
-                % (ref.decode("utf-8", "replace"), sha.decode("ascii")),
                 UserWarning,
                 UserWarning,
             )
             )
             continue
             continue

+ 4 - 4
dulwich/repo.py

@@ -36,6 +36,7 @@ import warnings
 from io import BytesIO
 from io import BytesIO
 from typing import (
 from typing import (
     TYPE_CHECKING,
     TYPE_CHECKING,
+    Any,
     BinaryIO,
     BinaryIO,
     Callable,
     Callable,
     Dict,
     Dict,
@@ -46,7 +47,6 @@ from typing import (
     Set,
     Set,
     Tuple,
     Tuple,
     Union,
     Union,
-    Any
 )
 )
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -649,7 +649,7 @@ class BaseRepo:
                 raise NotTagError(ret)
                 raise NotTagError(ret)
             else:
             else:
                 raise Exception(
                 raise Exception(
-                    "Type invalid: {!r} != {!r}".format(ret.type_name, cls.type_name)
+                    f"Type invalid: {ret.type_name!r} != {cls.type_name!r}"
                 )
                 )
         return ret
         return ret
 
 
@@ -1139,7 +1139,7 @@ class Repo(BaseRepo):
                 bare = True
                 bare = True
             else:
             else:
                 raise NotGitRepository(
                 raise NotGitRepository(
-                    "No git repository was found at %(path)s" % dict(path=root)
+                    "No git repository was found at {path}".format(**dict(path=root))
                 )
                 )
 
 
         self.bare = bare
         self.bare = bare
@@ -1253,7 +1253,7 @@ class Repo(BaseRepo):
             except NotGitRepository:
             except NotGitRepository:
                 path, remaining = os.path.split(path)
                 path, remaining = os.path.split(path)
         raise NotGitRepository(
         raise NotGitRepository(
-            "No git repository was found at %(path)s" % dict(path=start)
+            "No git repository was found at {path}".format(**dict(path=start))
         )
         )
 
 
     def controldir(self):
     def controldir(self):

+ 2 - 2
dulwich/server.py

@@ -189,7 +189,7 @@ class DictBackend(Backend):
             return self.repos[path]
             return self.repos[path]
         except KeyError as exc:
         except KeyError as exc:
             raise NotGitRepository(
             raise NotGitRepository(
-                "No git repository was found at %(path)s" % dict(path=path)
+                "No git repository was found at {path}".format(**dict(path=path))
             ) from exc
             ) from exc
 
 
 
 
@@ -206,7 +206,7 @@ class FileSystemBackend(Backend):
         normcase_abspath = os.path.normcase(abspath)
         normcase_abspath = os.path.normcase(abspath)
         normcase_root = os.path.normcase(self.root)
         normcase_root = os.path.normcase(self.root)
         if not normcase_abspath.startswith(normcase_root):
         if not normcase_abspath.startswith(normcase_root):
-            raise NotGitRepository("Path {!r} not inside root {!r}".format(path, self.root))
+            raise NotGitRepository(f"Path {path!r} not inside root {self.root!r}")
         return Repo(abspath)
         return Repo(abspath)
 
 
 
 

+ 2 - 2
dulwich/tests/compat/server_utils.py

@@ -78,12 +78,12 @@ class ServerTests:
         self._new_repo = self.import_repo("server_new.export")
         self._new_repo = self.import_repo("server_new.export")
 
 
     def url(self, port):
     def url(self, port):
-        return "{}://localhost:{}/".format(self.protocol, port)
+        return f"{self.protocol}://localhost:{port}/"
 
 
     def branch_args(self, branches=None):
     def branch_args(self, branches=None):
         if branches is None:
         if branches is None:
             branches = ["master", "branch"]
             branches = ["master", "branch"]
-        return ["{}:{}".format(b, b) for b in branches]
+        return [f"{b}:{b}" for b in branches]
 
 
     def test_push_to_dulwich(self):
     def test_push_to_dulwich(self):
         self.import_repos()
         self.import_repos()

+ 1 - 1
dulwich/tests/compat/test_client.py

@@ -633,7 +633,7 @@ class HTTPGitServer(http.server.HTTPServer):
         self.server_name = "localhost"
         self.server_name = "localhost"
 
 
     def get_url(self):
     def get_url(self):
-        return "http://{}:{}/".format(self.server_name, self.server_port)
+        return f"http://{self.server_name}:{self.server_port}/"
 
 
 
 
 class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
 class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):

+ 2 - 2
dulwich/tests/compat/utils.py

@@ -92,7 +92,7 @@ def require_git_version(required_version, git_path=_DEFAULT_GIT):
     found_version = git_version(git_path=git_path)
     found_version = git_version(git_path=git_path)
     if found_version is None:
     if found_version is None:
         raise SkipTest(
         raise SkipTest(
-            "Test requires git >= {}, but c git not found".format(required_version)
+            f"Test requires git >= {required_version}, but c git not found"
         )
         )
 
 
     if len(required_version) > _VERSION_LEN:
     if len(required_version) > _VERSION_LEN:
@@ -110,7 +110,7 @@ def require_git_version(required_version, git_path=_DEFAULT_GIT):
         required_version = ".".join(map(str, required_version))
         required_version = ".".join(map(str, required_version))
         found_version = ".".join(map(str, found_version))
         found_version = ".".join(map(str, found_version))
         raise SkipTest(
         raise SkipTest(
-            "Test requires git >= {}, found {}".format(required_version, found_version)
+            f"Test requires git >= {required_version}, found {found_version}"
         )
         )
 
 
 
 

+ 4 - 4
dulwich/tests/test_client.py

@@ -23,9 +23,9 @@ import os
 import shutil
 import shutil
 import sys
 import sys
 import tempfile
 import tempfile
-from typing import Dict
 import warnings
 import warnings
 from io import BytesIO
 from io import BytesIO
+from typing import Dict
 from unittest.mock import patch
 from unittest.mock import patch
 from urllib.parse import quote as urlquote
 from urllib.parse import quote as urlquote
 from urllib.parse import urlparse
 from urllib.parse import urlparse
@@ -1046,7 +1046,7 @@ class HttpGitClientTests(TestCase):
         self.assertEqual(original_password, c._password)
         self.assertEqual(original_password, c._password)
 
 
         basic_auth = c.pool_manager.headers["authorization"]
         basic_auth = c.pool_manager.headers["authorization"]
-        auth_string = "{}:{}".format(original_username, original_password)
+        auth_string = f"{original_username}:{original_password}"
         b64_credentials = base64.b64encode(auth_string.encode("latin1"))
         b64_credentials = base64.b64encode(auth_string.encode("latin1"))
         expected_basic_auth = "Basic %s" % b64_credentials.decode("latin1")
         expected_basic_auth = "Basic %s" % b64_credentials.decode("latin1")
         self.assertEqual(basic_auth, expected_basic_auth)
         self.assertEqual(basic_auth, expected_basic_auth)
@@ -1518,7 +1518,7 @@ class PLinkSSHVendorTests(TestCase):
                 break
                 break
         else:
         else:
             raise AssertionError(
             raise AssertionError(
-                "Expected warning {!r} not in {!r}".format(expected_warning, warnings_list)
+                f"Expected warning {expected_warning!r} not in {warnings_list!r}"
             )
             )
 
 
         args = command.proc.args
         args = command.proc.args
@@ -1563,7 +1563,7 @@ class PLinkSSHVendorTests(TestCase):
                 break
                 break
         else:
         else:
             raise AssertionError(
             raise AssertionError(
-                "Expected warning {!r} not in {!r}".format(expected_warning, warnings_list)
+                f"Expected warning {expected_warning!r} not in {warnings_list!r}"
             )
             )
 
 
         args = command.proc.args
         args = command.proc.args

+ 0 - 1
dulwich/tests/test_graph.py

@@ -1,5 +1,4 @@
 # test_index.py -- Tests for merge
 # test_index.py -- Tests for merge
-# encoding: utf-8
 # Copyright (c) 2020 Kevin B. Hendricks, Stratford Ontario Canada
 # Copyright (c) 2020 Kevin B. Hendricks, Stratford Ontario Canada
 #
 #
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU

+ 3 - 4
dulwich/tests/test_ignore.py

@@ -98,8 +98,7 @@ class TranslateTests(TestCase):
             self.assertEqual(
             self.assertEqual(
                 regex,
                 regex,
                 translate(pattern),
                 translate(pattern),
-                "orig pattern: %r, regex: %r, expected: %r"
+                f"orig pattern: {pattern!r}, regex: {translate(pattern)!r}, expected: {regex!r}",
-                % (pattern, translate(pattern), regex),
             )
             )
 
 
 
 
@@ -133,14 +132,14 @@ class MatchPatternTests(TestCase):
         for (path, pattern) in POSITIVE_MATCH_TESTS:
         for (path, pattern) in POSITIVE_MATCH_TESTS:
             self.assertTrue(
             self.assertTrue(
                 match_pattern(path, pattern),
                 match_pattern(path, pattern),
-                "path: {!r}, pattern: {!r}".format(path, pattern),
+                f"path: {path!r}, pattern: {pattern!r}",
             )
             )
 
 
     def test_no_matches(self):
     def test_no_matches(self):
         for (path, pattern) in NEGATIVE_MATCH_TESTS:
         for (path, pattern) in NEGATIVE_MATCH_TESTS:
             self.assertFalse(
             self.assertFalse(
                 match_pattern(path, pattern),
                 match_pattern(path, pattern),
-                "path: {!r}, pattern: {!r}".format(path, pattern),
+                f"path: {path!r}, pattern: {pattern!r}",
             )
             )
 
 
 
 

+ 1 - 2
dulwich/tests/test_index.py

@@ -1,5 +1,4 @@
 # test_index.py -- Tests for the git index
 # test_index.py -- Tests for the git index
-# encoding: utf-8
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@jelmer.uk>
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@jelmer.uk>
 #
 #
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
@@ -222,7 +221,7 @@ class CommitTreeTests(TestCase):
 
 
 class CleanupModeTests(TestCase):
 class CleanupModeTests(TestCase):
     def assertModeEqual(self, expected, got):
     def assertModeEqual(self, expected, got):
-        self.assertEqual(expected, got, "{:o} != {:o}".format(expected, got))
+        self.assertEqual(expected, got, f"{expected:o} != {got:o}")
 
 
     def test_file(self):
     def test_file(self):
         self.assertModeEqual(0o100644, cleanup_mode(0o100000))
         self.assertModeEqual(0o100644, cleanup_mode(0o100000))

+ 0 - 1
dulwich/tests/test_line_ending.py

@@ -1,5 +1,4 @@
 # test_line_ending.py -- Tests for the line ending functions
 # test_line_ending.py -- Tests for the line ending functions
-# encoding: utf-8
 # Copyright (C) 2018-2019 Boris Feld <boris.feld@comet.ml>
 # Copyright (C) 2018-2019 Boris Feld <boris.feld@comet.ml>
 #
 #
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU

+ 2 - 2
dulwich/tests/test_missing_obj_finder.py

@@ -39,14 +39,14 @@ class MissingObjectFinderTest(TestCase):
             self.assertIn(
             self.assertIn(
                 sha,
                 sha,
                 expected,
                 expected,
-                "({},{}) erroneously reported as missing".format(sha, path)
+                f"({sha},{path}) erroneously reported as missing"
             )
             )
             expected.remove(sha)
             expected.remove(sha)
 
 
         self.assertEqual(
         self.assertEqual(
             len(expected),
             len(expected),
             0,
             0,
-            "some objects are not reported as missing: {}".format(expected),
+            f"some objects are not reported as missing: {expected}",
         )
         )
 
 
 
 

+ 1 - 1
dulwich/tests/test_objects.py

@@ -1133,7 +1133,7 @@ class TagParseTests(ShaFileCheckTests):
 
 
     def test_check_tag_with_overflow_time(self):
     def test_check_tag_with_overflow_time(self):
         """Date with overflow should raise an ObjectFormatException when checked."""
         """Date with overflow should raise an ObjectFormatException when checked."""
-        author = "Some Dude <some@dude.org> {} +0000".format(MAX_TIME + 1)
+        author = f"Some Dude <some@dude.org> {MAX_TIME + 1} +0000"
         tag = Tag.from_string(self.make_tag_text(tagger=(author.encode())))
         tag = Tag.from_string(self.make_tag_text(tagger=(author.encode())))
         with self.assertRaises(ObjectFormatException):
         with self.assertRaises(ObjectFormatException):
             tag.check()
             tag.check()

+ 3 - 3
dulwich/tests/test_repository.py

@@ -450,7 +450,7 @@ class RepositoryRootTests(TestCase):
         o.close()
         o.close()
         bar_path = os.path.join(tmp_dir, 't', 'bar')
         bar_path = os.path.join(tmp_dir, 't', 'bar')
         if sys.platform == 'win32':
         if sys.platform == 'win32':
-            with open(bar_path, 'r') as f:
+            with open(bar_path) as f:
                 self.assertEqual('foo', f.read())
                 self.assertEqual('foo', f.read())
         else:
         else:
             self.assertEqual('foo', os.readlink(bar_path))
             self.assertEqual('foo', os.readlink(bar_path))
@@ -467,7 +467,7 @@ class RepositoryRootTests(TestCase):
         o.do_commit(b"add symlink")
         o.do_commit(b"add symlink")
 
 
         t = o.clone(os.path.join(tmp_dir, "t"), symlinks=False)
         t = o.clone(os.path.join(tmp_dir, "t"), symlinks=False)
-        with open(os.path.join(tmp_dir, "t", 'bar'), 'r') as f:
+        with open(os.path.join(tmp_dir, "t", 'bar')) as f:
             self.assertEqual('foo', f.read())
             self.assertEqual('foo', f.read())
 
 
         t.close()
         t.close()
@@ -846,7 +846,7 @@ exit 1
                 break
                 break
         else:
         else:
             raise AssertionError(
             raise AssertionError(
-                "Expected warning {!r} not in {!r}".format(expected_warning, warnings_list)
+                f"Expected warning {expected_warning!r} not in {warnings_list!r}"
             )
             )
         self.assertEqual([commit_sha], r[commit_sha2].parents)
         self.assertEqual([commit_sha], r[commit_sha2].parents)
 
 

+ 1 - 1
dulwich/walk.py

@@ -24,7 +24,7 @@
 import collections
 import collections
 import heapq
 import heapq
 from itertools import chain
 from itertools import chain
-from typing import Deque, List, Optional, Set, Tuple, Dict
+from typing import Deque, Dict, List, Optional, Set, Tuple
 
 
 from .diff_tree import (
 from .diff_tree import (
     RENAME_CHANGE_TYPES,
     RENAME_CHANGE_TYPES,

+ 1 - 1
examples/clone.py

@@ -19,7 +19,7 @@ _, args = getopt(sys.argv, "", [])
 
 
 
 
 if len(args) < 2:
 if len(args) < 2:
-    print("usage: {} host:path path".format(args[0]))
+    print(f"usage: {args[0]} host:path path")
     sys.exit(1)
     sys.exit(1)
 
 
 elif len(args) < 3:
 elif len(args) < 3:

+ 1 - 1
examples/latest_change.py

@@ -7,7 +7,7 @@ import time
 from dulwich.repo import Repo
 from dulwich.repo import Repo
 
 
 if len(sys.argv) < 2:
 if len(sys.argv) < 2:
-    print("usage: {} filename".format(sys.argv[0]))
+    print(f"usage: {sys.argv[0]} filename")
     sys.exit(1)
     sys.exit(1)
 
 
 r = Repo(".")
 r = Repo(".")

+ 1 - 1
examples/rename-branch.py

@@ -27,4 +27,4 @@ def update_refs(refs):
 
 
 
 
 client.send_pack(path, update_refs, generate_pack_data)
 client.send_pack(path, update_refs, generate_pack_data)
-print("Renamed {} to {}".format(args.old_ref, args.new_ref))
+print(f"Renamed {args.old_ref} to {args.new_ref}")

+ 24 - 1
pyproject.toml

@@ -73,11 +73,34 @@ select = [
     "D",
     "D",
     "E",
     "E",
     "F",
     "F",
-    "I"
+    "I",
+    "UP",
 ]
 ]
 ignore = [
 ignore = [
+    "ANN001",
+    "ANN002",
+    "ANN003",
     "ANN101",  # missing-type-self
     "ANN101",  # missing-type-self
+    "ANN102",
+    "ANN201",
+    "ANN202",
+    "ANN204",
+    "ANN205",
+    "ANN206",
+    "D100",
+    "D101",
+    "D102",
+    "D103",
+    "D104",
+    "D105",
+    "D107",
+    "D204",
+    "D205",
+    "D417",
+    "E501",  # line too long
+    "E741",  # ambiguous variable name
 ]
 ]
+target-version = "py37"
 
 
 [tool.ruff.pydocstyle]
 [tool.ruff.pydocstyle]
 convention = "google"
 convention = "google"

+ 1 - 1
setup.py

@@ -7,7 +7,7 @@ import sys
 
 
 from setuptools import Extension, setup
 from setuptools import Extension, setup
 
 
-if sys.version_info < (3, 6):
+if sys.version_info < (3, 7):
     raise Exception(
     raise Exception(
         'Dulwich only supports Python 3.6 and later. '
         'Dulwich only supports Python 3.6 and later. '
         'For 2.7 support, please install a version prior to 0.20')
         'For 2.7 support, please install a version prior to 0.20')