Преглед изворни кода

Improve type annotations to remove type: ignore comments

Remove type: ignore comments by improving type handling in three categories:
Jelmer Vernooij пре 3 месеци
родитељ
комит
332934efc8
4 измењених фајлова са 103 додато и 58 уклоњено
  1. 20 0
      dulwich/__init__.pyi
  2. 38 11
      dulwich/objects.py
  3. 36 27
      dulwich/porcelain.py
  4. 9 20
      dulwich/repo.py

+ 20 - 0
dulwich/__init__.pyi

@@ -0,0 +1,20 @@
+# Type stubs for dulwich/__init__.py
+
+from typing import Callable, Optional, TypeVar, Union
+
+if True:  # sys.version_info >= (3, 10)
+    from typing import ParamSpec
+else:
+    from typing_extensions import ParamSpec
+
+__version__: tuple[int, int, int]
+
+__all__: list[str]
+
+P = ParamSpec("P")
+R = TypeVar("R")
+
+def replace_me(
+    since: Optional[Union[str, tuple[int, ...]]] = None,
+    remove_in: Optional[Union[str, tuple[int, ...]]] = None,
+) -> Callable[[Callable[P, R]], Callable[P, R]]: ...

+ 38 - 11
dulwich/objects.py

@@ -167,23 +167,50 @@ def hex_to_filename(path: PathT, hex: Union[str, bytes]) -> PathT:
     # os.path.join accepts bytes or unicode, but all args must be of the same
     # type. Make sure that hex which is expected to be bytes, is the same type
     # as path.
-    if type(path) is not type(hex) and isinstance(path, str):
-        hex = hex.decode("ascii")  # type: ignore
-    dir_name = hex[:2]
-    file_name = hex[2:]
-    # Check from object dir
-    return os.path.join(path, dir_name, file_name)  # type: ignore
+    if isinstance(path, str):
+        if isinstance(hex, bytes):
+            hex_str = hex.decode("ascii")
+        else:
+            hex_str = hex
+        dir_name = hex_str[:2]
+        file_name = hex_str[2:]
+        result = os.path.join(path, dir_name, file_name)
+        assert isinstance(result, str)
+        return result
+    else:
+        # path is bytes
+        if isinstance(hex, str):
+            hex_bytes = hex.encode("ascii")
+        else:
+            hex_bytes = hex
+        dir_name_b = hex_bytes[:2]
+        file_name_b = hex_bytes[2:]
+        result_b = os.path.join(path, dir_name_b, file_name_b)
+        assert isinstance(result_b, bytes)
+        return result_b
 
 
 def filename_to_hex(filename: Union[str, bytes]) -> str:
     """Takes an object filename and returns its corresponding hex sha."""
     # grab the last (up to) two path components
-    names = filename.rsplit(os.path.sep, 2)[-2:]  # type: ignore
     errmsg = f"Invalid object filename: {filename!r}"
-    assert len(names) == 2, errmsg
-    base, rest = names
-    assert len(base) == 2 and len(rest) == 38, errmsg
-    hex_bytes = (base + rest).encode("ascii")  # type: ignore
+    if isinstance(filename, str):
+        names = filename.rsplit(os.path.sep, 2)[-2:]
+        assert len(names) == 2, errmsg
+        base, rest = names
+        assert len(base) == 2 and len(rest) == 38, errmsg
+        hex_str = base + rest
+        hex_bytes = hex_str.encode("ascii")
+    else:
+        # filename is bytes
+        sep = (
+            os.path.sep.encode("ascii") if isinstance(os.path.sep, str) else os.path.sep
+        )
+        names_b = filename.rsplit(sep, 2)[-2:]
+        assert len(names_b) == 2, errmsg
+        base_b, rest_b = names_b
+        assert len(base_b) == 2 and len(rest_b) == 38, errmsg
+        hex_bytes = base_b + rest_b
     hex_to_sha(hex_bytes)
     return hex_bytes.decode("ascii")
 

+ 36 - 27
dulwich/porcelain.py

@@ -1051,6 +1051,8 @@ def clean(
         # Reverse file visit order, so that files and subdirectories are
         # removed before containing directory
         for ap, is_dir in reversed(list(paths_in_wd)):
+            # target_dir and r.path are both str, so ap must be str
+            assert isinstance(ap, str)
             if is_dir:
                 # All subdirectories and files have been removed if untracked,
                 # so dir contains no tracked files iff it is empty.
@@ -1061,7 +1063,7 @@ def clean(
                 ip = path_to_tree_path(r.path, ap)
                 is_tracked = ip in index
 
-                rp = os.path.relpath(ap, r.path)  # type: ignore[arg-type]
+                rp = os.path.relpath(ap, r.path)
                 is_ignored = ignore_manager.is_ignored(rp)
 
                 if not is_tracked and not is_ignored:
@@ -2879,7 +2881,11 @@ def get_untracked_paths(
     if untracked_files == "no":
         return
 
-    with open_repo_closing(basepath) as r:
+    # Normalize paths to str
+    frompath_str = os.fsdecode(os.fspath(frompath))
+    basepath_str = os.fsdecode(os.fspath(basepath))
+
+    with open_repo_closing(basepath_str) as r:
         ignore_manager = IgnoreFilterManager.from_repo(r)
 
     ignored_dirs = []
@@ -2907,13 +2913,13 @@ def get_untracked_paths(
     def prune_dirnames(dirpath: str, dirnames: list[str]) -> list[str]:
         for i in range(len(dirnames) - 1, -1, -1):
             path = os.path.join(dirpath, dirnames[i])
-            ip = os.path.join(os.path.relpath(path, basepath), "")  # type: ignore[arg-type]
+            ip = os.path.join(os.path.relpath(path, basepath_str), "")
 
             # Check if directory is ignored
             if ignore_manager.is_ignored(ip) is True:
                 if not exclude_ignored:
                     ignored_dirs.append(
-                        os.path.join(os.path.relpath(path, frompath), "")  # type: ignore[arg-type]
+                        os.path.join(os.path.relpath(path, frompath_str), "")
                     )
                 del dirnames[i]
                 continue
@@ -2921,7 +2927,7 @@ def get_untracked_paths(
             # For "normal" mode, check if the directory is entirely untracked
             if untracked_files == "normal":
                 # Convert directory path to tree path for index lookup
-                dir_tree_path = path_to_tree_path(basepath, path)
+                dir_tree_path = path_to_tree_path(basepath_str, path)
 
                 # Check if any file in this directory is tracked
                 dir_prefix = dir_tree_path + b"/" if dir_tree_path else b""
@@ -2929,8 +2935,10 @@ def get_untracked_paths(
 
                 if not has_tracked_files:
                     # This directory is entirely untracked
-                    rel_path_base = os.path.relpath(path, basepath)  # type: ignore[arg-type]
-                    rel_path_from = os.path.join(os.path.relpath(path, frompath), "")  # type: ignore[arg-type]
+                    rel_path_base = os.path.relpath(path, basepath_str)
+                    rel_path_from = os.path.join(
+                        os.path.relpath(path, frompath_str), ""
+                    )
 
                     # If excluding ignored, check if directory contains any non-ignored files
                     if exclude_ignored:
@@ -2950,39 +2958,43 @@ def get_untracked_paths(
     # For "all" mode, use the original behavior
     if untracked_files == "all":
         for ap, is_dir in _walk_working_dir_paths(
-            frompath, basepath, prune_dirnames=prune_dirnames
+            frompath_str, basepath_str, prune_dirnames=prune_dirnames
         ):
+            # frompath_str and basepath_str are both str, so ap must be str
+            assert isinstance(ap, str)
             if not is_dir:
-                ip = path_to_tree_path(basepath, ap)
+                ip = path_to_tree_path(basepath_str, ap)
                 if ip not in index:
                     if not exclude_ignored or not ignore_manager.is_ignored(
-                        os.path.relpath(ap, basepath)  # type: ignore[arg-type]
+                        os.path.relpath(ap, basepath_str)
                     ):
-                        yield os.path.relpath(ap, frompath)  # type: ignore[arg-type]
+                        yield os.path.relpath(ap, frompath_str)
     else:  # "normal" mode
         # Walk directories, handling both files and directories
         for ap, is_dir in _walk_working_dir_paths(
-            frompath, basepath, prune_dirnames=prune_dirnames
+            frompath_str, basepath_str, prune_dirnames=prune_dirnames
         ):
+            # frompath_str and basepath_str are both str, so ap must be str
+            assert isinstance(ap, str)
             # This part won't be reached for pruned directories
             if is_dir:
                 # Check if this directory is entirely untracked
-                dir_tree_path = path_to_tree_path(basepath, ap)
+                dir_tree_path = path_to_tree_path(basepath_str, ap)
                 dir_prefix = dir_tree_path + b"/" if dir_tree_path else b""
                 has_tracked_files = any(name.startswith(dir_prefix) for name in index)
                 if not has_tracked_files:
                     if not exclude_ignored or not ignore_manager.is_ignored(
-                        os.path.relpath(ap, basepath)  # type: ignore[arg-type]
+                        os.path.relpath(ap, basepath_str)
                     ):
-                        yield os.path.join(os.path.relpath(ap, frompath), "")  # type: ignore[arg-type]
+                        yield os.path.join(os.path.relpath(ap, frompath_str), "")
             else:
                 # Check individual files in directories that contain tracked files
-                ip = path_to_tree_path(basepath, ap)
+                ip = path_to_tree_path(basepath_str, ap)
                 if ip not in index:
                     if not exclude_ignored or not ignore_manager.is_ignored(
-                        os.path.relpath(ap, basepath)  # type: ignore[arg-type]
+                        os.path.relpath(ap, basepath_str)
                     ):
-                        yield os.path.relpath(ap, frompath)  # type: ignore[arg-type]
+                        yield os.path.relpath(ap, frompath_str)
 
         # Yield any untracked directories found during pruning
         yield from untracked_dir_list
@@ -3939,26 +3951,23 @@ def check_ignore(
         ignore_manager = IgnoreFilterManager.from_repo(r)
         for original_path in paths:
             # Convert path to string for consistent handling
-            original_path_str = os.fspath(original_path)
+            original_path_fspath = os.fspath(original_path)
+            # Normalize to str
+            original_path_str = os.fsdecode(original_path_fspath)
 
             if not no_index and path_to_tree_path(r.path, original_path_str) in index:
                 continue
 
             # Preserve whether the original path had a trailing slash
-            if isinstance(original_path_str, bytes):
-                had_trailing_slash = original_path_str.endswith(
-                    (b"/", os.path.sep.encode())
-                )
-            else:
-                had_trailing_slash = original_path_str.endswith(("/", os.path.sep))
+            had_trailing_slash = original_path_str.endswith(("/", os.path.sep))
 
             if os.path.isabs(original_path_str):
-                path = os.path.relpath(original_path_str, r.path)  # type: ignore[arg-type]
+                path = os.path.relpath(original_path_str, r.path)
                 # Normalize Windows paths to use forward slashes
                 if os.path.sep != "/":
                     path = path.replace(os.path.sep, "/")
             else:
-                path = original_path_str  # type: ignore[assignment]
+                path = original_path_str
 
             # Restore trailing slash if it was in the original
             if had_trailing_slash and not path.endswith("/"):

+ 9 - 20
dulwich/repo.py

@@ -100,7 +100,7 @@ from .objects import (
     check_hexsha,
     valid_hexsha,
 )
-from .pack import generate_unpacked_objects
+from .pack import PackHint, generate_unpacked_objects
 from .refs import (
     ANNOTATED_TAG_SUFFIX,  # noqa: F401
     LOCAL_BRANCH_PREFIX,
@@ -608,7 +608,8 @@ class BaseRepo:
             if hasattr(graph_walker, "shallow"):
                 graph_walker.shallow.update(shallow - not_shallow)
                 new_shallow = graph_walker.shallow - current_shallow
-                unshallow = graph_walker.unshallow = not_shallow & current_shallow  # type: ignore[attr-defined]
+                unshallow = not_shallow & current_shallow
+                setattr(graph_walker, "unshallow", unshallow)
                 if hasattr(graph_walker, "update_shallow"):
                     graph_walker.update_shallow(new_shallow, unshallow)
         else:
@@ -622,24 +623,12 @@ class BaseRepo:
                 # Do not send a pack in shallow short-circuit path
                 return None
 
-            class DummyMissingObjectFinder:
-                """Dummy finder that returns no missing objects."""
-
-                def get_remote_has(self) -> None:
-                    """Get remote has (always returns None).
-
-                    Returns:
-                      None
-                    """
-                    return None
-
-                def __len__(self) -> int:
-                    return 0
-
-                def __iter__(self) -> Iterator[tuple[bytes, Optional[bytes]]]:
-                    yield from []
-
-            return DummyMissingObjectFinder()  # type: ignore
+            # Return an actual MissingObjectFinder with empty wants
+            return MissingObjectFinder(
+                self.object_store,
+                haves=[],
+                wants=[],
+            )
 
         # If the graph walker is set up with an implementation that can
         # ACK/NAK to the wire, it will write data to the client through