typing: add a transaction protocol
authorPierre-Yves David <pierre-yves.david@octobus.net>
Fri, 07 Feb 2025 13:48:50 +0100
changeset 52754 a7dcb7c1ff5a
parent 52753 ae2848198462
child 52755 1b7a57a5b47a
typing: add a transaction protocol This allow us to remove the "external" import from mercurial/interfaces/dirstate.py, cutting one of the circular import route.
hgext/git/dirstate.py
mercurial/dirstate.py
mercurial/interfaces/dirstate.py
mercurial/interfaces/transaction.py
mercurial/interfaces/types.py
mercurial/transaction.py
--- a/hgext/git/dirstate.py	Fri Feb 07 16:40:49 2025 +0100
+++ b/hgext/git/dirstate.py	Fri Feb 07 13:48:50 2025 +0100
@@ -13,7 +13,10 @@
     Tuple,
 )
 
-from mercurial.interfaces.types import MatcherT
+from mercurial.interfaces.types import (
+    MatcherT,
+    TransactionT,
+)
 from mercurial.node import sha1nodeconstants
 from mercurial import (
     dirstatemap,
@@ -317,7 +320,7 @@
     ) -> None:
         raise NotImplementedError
 
-    def write(self, tr: Optional[intdirstate.TransactionT]) -> None:
+    def write(self, tr: Optional[TransactionT]) -> None:
         # TODO: call parent change callbacks
 
         if tr:
@@ -456,7 +459,7 @@
         self._plchangecallbacks[category] = callback
 
     def setbranch(
-        self, branch: bytes, transaction: Optional[intdirstate.TransactionT]
+        self, branch: bytes, transaction: Optional[TransactionT]
     ) -> None:
         raise error.Abort(
             b'git repos do not support branches. try using bookmarks'
--- a/mercurial/dirstate.py	Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/dirstate.py	Fri Feb 07 13:48:50 2025 +0100
@@ -24,7 +24,10 @@
 )
 
 from .i18n import _
-from .interfaces.types import MatcherT
+from .interfaces.types import (
+    MatcherT,
+    TransactionT,
+)
 
 from hgdemandimport import tracing
 
@@ -669,7 +672,7 @@
         return self._map.setparents(p1, p2, fold_p2=fold_p2)
 
     def setbranch(
-        self, branch: bytes, transaction: Optional[intdirstate.TransactionT]
+        self, branch: bytes, transaction: Optional[TransactionT]
     ) -> None:
         self.__class__._branch.set(self, encoding.fromlocal(branch))
         if transaction is not None:
@@ -1102,7 +1105,7 @@
             on_abort,
         )
 
-    def write(self, tr: Optional[intdirstate.TransactionT]) -> None:
+    def write(self, tr: Optional[TransactionT]) -> None:
         if not self._dirty:
             return
         # make sure we don't request a write of invalidated content
--- a/mercurial/interfaces/dirstate.py	Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/interfaces/dirstate.py	Fri Feb 07 13:48:50 2025 +0100
@@ -20,13 +20,10 @@
 if typing.TYPE_CHECKING:
     # Almost all mercurial modules are only imported in the type checking phase
     # to avoid circular imports
-    from .. import (
-        transaction as txnmod,
-    )
-
     from . import (
         matcher,
         status as istatus,
+        transaction,
     )
 
     # TODO: finish adding type hints
@@ -55,8 +52,7 @@
     StatusReturnT = Tuple[Any, istatus.Status, Any]
     """The return type of dirstate.status()."""
 
-    # TODO: probably doesn't belong here.
-    TransactionT = txnmod.transaction
+    TransactionT = transaction.ITransaction
     """The type for a transaction used with dirstate.
 
     This is meant to help callers avoid having to remember to delay the import
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/interfaces/transaction.py	Fri Feb 07 13:48:50 2025 +0100
@@ -0,0 +1,253 @@
+# transaction.py - simple journaling scheme for mercurial
+#
+# This transaction scheme is intended to gracefully handle program
+# errors and interruptions. More serious failures like system crashes
+# can be recovered with an fsck-like tool. As the whole repository is
+# effectively log-structured, this should amount to simply truncating
+# anything that isn't referenced in the changelog.
+#
+# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import annotations
+
+import abc
+
+from typing import (
+    Callable,
+    Collection,
+    List,
+    Optional,
+    Protocol,
+    Tuple,
+    Union,
+)
+
+from ._basetypes import (
+    CallbackCategoryT,
+    HgPathT,
+    VfsKeyT,
+)
+
+JournalEntryT = Tuple[HgPathT, int]
+
+
+class ITransaction(Protocol):
+    @property
+    @abc.abstractmethod
+    def finalized(self) -> bool:
+        ...
+
+    @abc.abstractmethod
+    def startgroup(self) -> None:
+        """delay registration of file entry
+
+        This is used by strip to delay vision of strip offset. The transaction
+        sees either none or all of the strip actions to be done."""
+
+    @abc.abstractmethod
+    def endgroup(self) -> None:
+        """apply delayed registration of file entry.
+
+        This is used by strip to delay vision of strip offset. The transaction
+        sees either none or all of the strip actions to be done."""
+
+    @abc.abstractmethod
+    def add(self, file: HgPathT, offset: int) -> None:
+        """record the state of an append-only file before update"""
+
+    @abc.abstractmethod
+    def addbackup(
+        self,
+        file: HgPathT,
+        hardlink: bool = True,
+        location: VfsKeyT = b'',
+        for_offset: Union[bool, int] = False,
+    ) -> None:
+        """Adds a backup of the file to the transaction
+
+        Calling addbackup() creates a hardlink backup of the specified file
+        that is used to recover the file in the event of the transaction
+        aborting.
+
+        * `file`: the file path, relative to .hg/store
+        * `hardlink`: use a hardlink to quickly create the backup
+
+        If `for_offset` is set, we expect a offset for this file to have been
+        previously recorded
+        """
+
+    @abc.abstractmethod
+    def registertmp(self, tmpfile: HgPathT, location: VfsKeyT = b'') -> None:
+        """register a temporary transaction file
+
+        Such files will be deleted when the transaction exits (on both
+        failure and success).
+        """
+
+    @abc.abstractmethod
+    def addfilegenerator(
+        self,
+        genid: bytes,
+        filenames: Collection[HgPathT],
+        genfunc: Callable,
+        order: int = 0,
+        location: VfsKeyT = b'',
+        post_finalize: bool = False,
+    ) -> None:
+        """add a function to generates some files at transaction commit
+
+        The `genfunc` argument is a function capable of generating proper
+        content of each entry in the `filename` tuple.
+
+        At transaction close time, `genfunc` will be called with one file
+        object argument per entries in `filenames`.
+
+        The transaction itself is responsible for the backup, creation and
+        final write of such file.
+
+        The `genid` argument is used to ensure the same set of file is only
+        generated once. Call to `addfilegenerator` for a `genid` already
+        present will overwrite the old entry.
+
+        The `order` argument may be used to control the order in which multiple
+        generator will be executed.
+
+        The `location` arguments may be used to indicate the files are located
+        outside of the the standard directory for transaction. It should match
+        one of the key of the `transaction.vfsmap` dictionary.
+
+        The `post_finalize` argument can be set to `True` for file generation
+        that must be run after the transaction has been finalized.
+        """
+
+    @abc.abstractmethod
+    def removefilegenerator(self, genid: bytes) -> None:
+        """reverse of addfilegenerator, remove a file generator function"""
+
+    @abc.abstractmethod
+    def findoffset(self, file: HgPathT) -> Optional[int]:
+        ...
+
+    @abc.abstractmethod
+    def readjournal(self) -> List[JournalEntryT]:
+        ...
+
+    @abc.abstractmethod
+    def replace(self, file: HgPathT, offset: int) -> None:
+        """
+        replace can only replace already committed entries
+        that are not pending in the queue
+        """
+
+    @abc.abstractmethod
+    def nest(self, name: bytes = b'<unnamed>') -> ITransaction:
+        ...
+
+    @abc.abstractmethod
+    def release(self) -> None:
+        ...
+
+    @abc.abstractmethod
+    def running(self) -> bool:
+        ...
+
+    @abc.abstractmethod
+    def addpending(
+        self,
+        category: CallbackCategoryT,
+        callback: Callable[[ITransaction], None],
+    ) -> None:
+        """add a callback to be called when the transaction is pending
+
+        The transaction will be given as callback's first argument.
+
+        Category is a unique identifier to allow overwriting an old callback
+        with a newer callback.
+        """
+
+    @abc.abstractmethod
+    def writepending(self) -> bool:
+        """write pending file to temporary version
+
+        This is used to allow hooks to view a transaction before commit"""
+
+    @abc.abstractmethod
+    def hasfinalize(self, category: CallbackCategoryT) -> bool:
+        """check is a callback already exist for a category"""
+
+    @abc.abstractmethod
+    def addfinalize(
+        self,
+        category: CallbackCategoryT,
+        callback: Callable[[ITransaction], None],
+    ) -> None:
+        """add a callback to be called when the transaction is closed
+
+        The transaction will be given as callback's first argument.
+
+        Category is a unique identifier to allow overwriting old callbacks with
+        newer callbacks.
+        """
+
+    @abc.abstractmethod
+    def addpostclose(
+        self,
+        category: CallbackCategoryT,
+        callback: Callable[[ITransaction], None],
+    ) -> None:
+        """add or replace a callback to be called after the transaction closed
+
+        The transaction will be given as callback's first argument.
+
+        Category is a unique identifier to allow overwriting an old callback
+        with a newer callback.
+        """
+
+    @abc.abstractmethod
+    def getpostclose(
+        self, category: CallbackCategoryT
+    ) -> Optional[Callable[[ITransaction], None]]:
+        """return a postclose callback added before, or None"""
+
+    @abc.abstractmethod
+    def addabort(
+        self,
+        category: CallbackCategoryT,
+        callback: Callable[[ITransaction], None],
+    ) -> None:
+        """add a callback to be called when the transaction is aborted.
+
+        The transaction will be given as the first argument to the callback.
+
+        Category is a unique identifier to allow overwriting an old callback
+        with a newer callback.
+        """
+
+    @abc.abstractmethod
+    def addvalidator(
+        self,
+        category: CallbackCategoryT,
+        callback: Callable[[ITransaction], None],
+    ) -> None:
+        """adds a callback to be called when validating the transaction.
+
+        The transaction will be given as the first argument to the callback.
+
+        callback should raise exception if to abort transaction"""
+
+    @abc.abstractmethod
+    def close(self) -> None:
+        '''commit the transaction'''
+
+    @abc.abstractmethod
+    def abort(self) -> None:
+        """abort the transaction (generally called on error, or when the
+        transaction is not explicitly committed before going out of
+        scope)"""
+
+    @abc.abstractmethod
+    def add_journal(self, vfs_id: VfsKeyT, path: HgPathT) -> None:
+        ...
--- a/mercurial/interfaces/types.py	Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/interfaces/types.py	Fri Feb 07 13:48:50 2025 +0100
@@ -21,6 +21,8 @@
 
 from . import (
     matcher,
+    transaction,
 )
 
 MatcherT = matcher.IMatcher
+TransactionT = transaction.ITransaction
--- a/mercurial/transaction.py	Fri Feb 07 16:40:49 2025 +0100
+++ b/mercurial/transaction.py	Fri Feb 07 13:48:50 2025 +0100
@@ -21,7 +21,6 @@
     Collection,
     List,
     Optional,
-    Tuple,
     Union,
 )
 
@@ -29,6 +28,7 @@
 from .interfaces.types import (
     CallbackCategoryT,
     HgPathT,
+    TransactionT,
     VfsKeyT,
 )
 from . import (
@@ -38,6 +38,7 @@
     util,
 )
 from .utils import stringutil
+from .interfaces import transaction as itxn
 
 version = 2
 
@@ -45,8 +46,6 @@
 GEN_GROUP_PRE_FINALIZE = b'prefinalize'
 GEN_GROUP_POST_FINALIZE = b'postfinalize'
 
-JournalEntryT = Tuple[HgPathT, int]
-
 
 def active(func):
     def _active(self, *args, **kwds):
@@ -240,7 +239,7 @@
         pass
 
 
-class transaction(util.transactional):
+class transaction(util.transactional, itxn.ITransaction):
     def __init__(
         self,
         report,
@@ -573,7 +572,7 @@
         return self._offsetmap.get(file)
 
     @active
-    def readjournal(self) -> List[JournalEntryT]:
+    def readjournal(self) -> List[itxn.JournalEntryT]:
         self._file.seek(0)
         entries = []
         for l in self._file.readlines():
@@ -604,7 +603,7 @@
         self._file.flush()
 
     @active
-    def nest(self, name: bytes = b'<unnamed>') -> transaction:
+    def nest(self, name: bytes = b'<unnamed>') -> TransactionT:
         self._count += 1
         self._usages += 1
         self._names.append(name)
@@ -625,7 +624,7 @@
     def addpending(
         self,
         category: CallbackCategoryT,
-        callback: Callable[[transaction], None],
+        callback: Callable[[TransactionT], None],
     ) -> None:
         """add a callback to be called when the transaction is pending
 
@@ -658,7 +657,7 @@
     def addfinalize(
         self,
         category: CallbackCategoryT,
-        callback: Callable[[transaction], None],
+        callback: Callable[[TransactionT], None],
     ) -> None:
         """add a callback to be called when the transaction is closed
 
@@ -673,7 +672,7 @@
     def addpostclose(
         self,
         category: CallbackCategoryT,
-        callback: Callable[[transaction], None],
+        callback: Callable[[TransactionT], None],
     ) -> None:
         """add or replace a callback to be called after the transaction closed
 
@@ -688,7 +687,7 @@
     def getpostclose(
         self,
         category: CallbackCategoryT,
-    ) -> Optional[Callable[[transaction], None]]:
+    ) -> Optional[Callable[[TransactionT], None]]:
         """return a postclose callback added before, or None"""
         return self._postclosecallback.get(category, None)
 
@@ -696,7 +695,7 @@
     def addabort(
         self,
         category: CallbackCategoryT,
-        callback: Callable[[transaction], None],
+        callback: Callable[[TransactionT], None],
     ) -> None:
         """add a callback to be called when the transaction is aborted.
 
@@ -711,7 +710,7 @@
     def addvalidator(
         self,
         category: CallbackCategoryT,
-        callback: Callable[[transaction], None],
+        callback: Callable[[TransactionT], None],
     ) -> None:
         """adds a callback to be called when validating the transaction.