interfaces: add the optional `bdiff.xdiffblocks()` method
authorMatt Harbison <matt_harbison@yahoo.com>
Sun, 29 Sep 2024 02:03:20 -0400
changeset 51934 09f3a6790e56
parent 51933 f2832de2a46c
child 51935 77e2994bd617
interfaces: add the optional `bdiff.xdiffblocks()` method PyCharm flagged where this was called on the protocol class in `mdiff.py` in the previous commit, but pytype completely missed it. PyCharm is correct here, but I'm committing this separately to highlight this potential problem- some of the implementations don't implement _all_ of the methods the others do, and there's not a great way to indicate on a protocol class that a method or attribute is optional- that's kinda the opposite of what static typing is about. Making the method an `Optional[Callable]` attribute works here, and keeps both PyCharm and pytype happy, and the generated `mdiff.pyi` and `modules.pyi` look reasonable. We might be getting a little lucky, because the method isn't invoked directly- it is returned from another method that selects which block function to use. Except since it is declared on the protocol class, every module needs this attribute (in theory, but in practice this doesn't seem to be checked), so the check for it on the module has to change from `hasattr()` to `getattr(..., None)`. We defer defining the optional attrs to the type checking phase as an extra precaution- that way it isn't an attr with a `None` value at runtime if someone is still using `hasattr()`. As to why pytype missed this, I have no clue. The generated `mdiff.pyi` even has the global variable typed as `bdiff: intmod.BDiff`, so uses of it really should comply with what is on the class, protocol class or not.
mercurial/cffi/bdiff.py
mercurial/interfaces/modules.py
mercurial/mdiff.py
mercurial/pure/bdiff.py
--- a/mercurial/cffi/bdiff.py	Sat Sep 28 19:12:18 2024 -0400
+++ b/mercurial/cffi/bdiff.py	Sun Sep 29 02:03:20 2024 -0400
@@ -8,13 +8,20 @@
 from __future__ import annotations
 
 import struct
+import typing
 
 from typing import (
     List,
+    Optional,
     Tuple,
 )
 
 from ..pure.bdiff import *
+
+from ..interfaces import (
+    modules as intmod,
+)
+
 from . import _bdiff  # pytype: disable=import-error
 
 ffi = _bdiff.ffi
@@ -87,3 +94,11 @@
         lib.free(b[0])
         lib.bdiff_freehunks(l.next)
     return b"".join(rl)
+
+
+# In order to adhere to the module protocol, these functions must be visible to
+# the type checker, though they aren't actually implemented by this
+# implementation of the module protocol.  Callers are responsible for
+# checking that the implementation is available before using them.
+if typing.TYPE_CHECKING:
+    xdiffblocks: Optional[intmod.BDiffBlocksFnc] = None
--- a/mercurial/interfaces/modules.py	Sat Sep 28 19:12:18 2024 -0400
+++ b/mercurial/interfaces/modules.py	Sun Sep 29 02:03:20 2024 -0400
@@ -8,14 +8,19 @@
 import typing
 
 from typing import (
+    Callable,
     List,
+    Optional,
     Protocol,
     Tuple,
 )
 
 if typing.TYPE_CHECKING:
     BDiffBlock = Tuple[int, int, int, int]
-    """An entry in the list returned by bdiff.blocks()."""
+    """An entry in the list returned by bdiff.{xdiff,}blocks()."""
+
+    BDiffBlocksFnc = Callable[[bytes, bytes], List[BDiffBlock]]
+    """The signature of `bdiff.blocks()` and `bdiff.xdiffblocks()`."""
 
 
 class BDiff(Protocol):
@@ -32,3 +37,6 @@
 
     def fixws(self, text: bytes, allws: bool) -> bytes:
         ...
+
+    xdiffblocks: Optional[BDiffBlocksFnc]
+    """This method is currently only available in the ``cext`` module."""
--- a/mercurial/mdiff.py	Sat Sep 28 19:12:18 2024 -0400
+++ b/mercurial/mdiff.py	Sun Sep 29 02:03:20 2024 -0400
@@ -212,7 +212,11 @@
 
 
 def chooseblocksfunc(opts=None):
-    if opts is None or not opts.xdiff or not hasattr(bdiff, 'xdiffblocks'):
+    if (
+        opts is None
+        or not opts.xdiff
+        or not getattr(bdiff, 'xdiffblocks', None)
+    ):
         return bdiff.blocks
     else:
         return bdiff.xdiffblocks
--- a/mercurial/pure/bdiff.py	Sat Sep 28 19:12:18 2024 -0400
+++ b/mercurial/pure/bdiff.py	Sun Sep 29 02:03:20 2024 -0400
@@ -10,12 +10,18 @@
 import difflib
 import re
 import struct
+import typing
 
 from typing import (
     List,
+    Optional,
     Tuple,
 )
 
+from ..interfaces import (
+    modules as intmod,
+)
+
 
 def splitnewlines(text: bytes) -> List[bytes]:
     '''like str.splitlines, but only split on newlines.'''
@@ -107,3 +113,11 @@
         text = re.sub(b'[ \t\r]+', b' ', text)
         text = text.replace(b' \n', b'\n')
     return text
+
+
+# In order to adhere to the module protocol, these functions must be visible to
+# the type checker, though they aren't actually implemented by this
+# implementation of the module protocol.  Callers are responsible for
+# checking that the implementation is available before using them.
+if typing.TYPE_CHECKING:
+    xdiffblocks: Optional[intmod.BDiffBlocksFnc] = None