Patchwork D1998: wireproto: define and use types for wire protocol commands

login
register
mail settings
Submitter phabricator
Date Feb. 1, 2018, 11:37 p.m.
Message ID <differential-rev-PHID-DREV-3oo4zyjenz2oe47va4nc-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/27205/
State Superseded
Headers show

Comments

phabricator - Feb. 1, 2018, 11:37 p.m.
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Wire protocol commands have historically been declared as
  2-tuples in wireproto.commands. There are some additional features I'd
  like to implement that require going beyond 2-tuples. But because
  the 2-tuple API (both destructuring assignment and direct assignment
  into the dict) is used throughout the code base and in 3rd party
  extensions, we can't do a trivial type change.
  
  This commit creates a new "commandentry" type to represent declared
  wire protocol commands. It implements __getitem__ and __iter__ so
  it can quack like a 2-tuple. The @wireprotocommand decorator now
  creates "commandentry" instances.
  
  We also create a "commanddict" type to represent the dictionary of
  declared wire protocol commands. It inherits from "dict" but provides
  a custom __setitem__ to coerce passed 2-tuples to "commandentry"
  instances. wireproto.commands is now an instance of this type.
  
  Various callers in core rely on the new functionality. And tests
  pass. So I'm reasonably confident things will "just work" in 3rd
  party extensions as well.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1998

AFFECTED FILES
  mercurial/wireproto.py

CHANGE DETAILS




To: indygreg, #hg-reviewers
Cc: mercurial-devel
phabricator - Feb. 7, 2018, 10:19 p.m.
durin42 added a comment.


  I'm curious what registrations you need that don't fit in 2-tuples. Can I see a sample of where that's going?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1998

To: indygreg, #hg-reviewers
Cc: durin42, mercurial-devel
phabricator - Feb. 7, 2018, 10:26 p.m.
indygreg added a comment.


  In https://phab.mercurial-scm.org/D1998#34740, @durin42 wrote:
  
  > I'm curious what registrations you need that don't fit in 2-tuples. Can I see a sample of where that's going?
  
  
  I'll be adding additional attributes to `@wireprotocommand` in future series. One thing I know I'll add is a mechanism to limit which wire protocol versions/transports to expose a command to. This will prevent legacy commands from being exposed to modern protocols and vice versa.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1998

To: indygreg, #hg-reviewers
Cc: durin42, mercurial-devel
phabricator - Feb. 7, 2018, 10:29 p.m.
durin42 accepted this revision.
durin42 added a comment.
This revision is now accepted and ready to land.


  I'm sold.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1998

To: indygreg, #hg-reviewers, durin42
Cc: durin42, mercurial-devel

Patch

diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -634,8 +634,64 @@ 
 
     return compengines
 
-# list of commands
-commands = {}
+class commandentry(object):
+    """Represents a declared wire protocol command."""
+    def __init__(self, func, args=''):
+        self.func = func
+        self.args = args
+
+    def _merge(self, func, args):
+        """Merge this instance with an incoming 2-tuple.
+
+        This is called when a caller using the old 2-tuple API attempts
+        to replace an instance. The incoming values are merged with
+        data not captured by the 2-tuple and a new instance containing
+        the union of the two objects is returned.
+        """
+        return commandentry(func, args)
+
+    # Old code treats instances as 2-tuples. So expose that interface.
+    def __iter__(self):
+        yield self.func
+        yield self.args
+
+    def __getitem__(self, i):
+        if i == 0:
+            return self.func
+        elif i == 1:
+            return self.args
+        else:
+            raise IndexError('can only access elements 0 and 1')
+
+class commanddict(dict):
+    """Container for registered wire protocol commands.
+
+    It behaves like a dict. But __setitem__ is overwritten to allow silent
+    coercion of values from 2-tuples for API compatibility.
+    """
+    def __setitem__(self, k, v):
+        if isinstance(v, commandentry):
+            pass
+        # Cast 2-tuples to commandentry instances.
+        elif isinstance(v, tuple):
+            if len(v) != 2:
+                raise ValueError('command tuples must have exactly 2 elements')
+
+            # It is common for extensions to wrap wire protocol commands via
+            # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
+            # doing this aren't aware of the new API that uses objects to store
+            # command entries, we automatically merge old state with new.
+            if k in self:
+                v = self[k]._merge(v[0], v[1])
+            else:
+                v = commandentry(v[0], v[1])
+        else:
+            raise ValueError('command entries must be commandentry instances '
+                             'or 2-tuples')
+
+        return super(commanddict, self).__setitem__(k, v)
+
+commands = commanddict()
 
 def wireprotocommand(name, args=''):
     """Decorator to declare a wire protocol command.
@@ -646,7 +702,7 @@ 
     accepts. ``*`` is a special value that says to accept all arguments.
     """
     def register(func):
-        commands[name] = (func, args)
+        commands[name] = commandentry(func, args)
         return func
     return register