indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Our abstract interfaces are more useful if we guarantee that
  implementations conform to certain rules. Namely, we want to ensure
  that objects implementing interfaces don't expose new public
  attributes that aren't part of the interface. That way, as long as
  consumers don't access "internal" attributes (those beginning with
  "_") then (in theory) objects implementing interfaces can be swapped
  out and everything will "just work."
  
  We add a test that enforces our "no public attributes not part
  of the abstract interface" rule.
  
  We /could/ implement "interface compliance detection" at run-time.
  However, that is littered with problems.
  
  The obvious solutions are custom __new__ and __init__ methods.
  These rely on derived types actually calling the parent's
  implementation, which is no sure bet. Furthermore, __new__ and
  __init__ will likely be called before instance-specific attributes
  are assigned. In other words, they won't detect public attributes
  set on self.__dict__. This means public attribute detection won't
  be robust.
  
  We could work around lack of robust self.__dict__ public attribute
  detection by having our interfaces implement a custom __getattribute__,
  __getattr__, and/or __setattr__. However, this incurs an undesirable
  run-time penalty. And, subclasses could override our custom
  method, bypassing the check.
  
  The most robust solution is a non-runtime test. So that's what this
  commit implements. We have a generic function for validating that an
  object only has public attributes defined by abstract classes. Then,
  we instantiate some peers and verify a newly constructed object
  plays by the rules.

REPOSITORY
  rHG Mercurial

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

AFFECTED FILES
  tests/test-check-interfaces.py
  tests/test-check-interfaces.py.out

CHANGE DETAILS

diff --git a/tests/test-check-interfaces.py.out 
b/tests/test-check-interfaces.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-check-interfaces.py.out
@@ -0,0 +1,2 @@
+public attributes not in abstract interface: badpeer.badattribute
+public attributes not in abstract interface: badpeer.badmethod
diff --git a/tests/test-check-interfaces.py b/tests/test-check-interfaces.py
new file mode 100644
--- /dev/null
+++ b/tests/test-check-interfaces.py
@@ -0,0 +1,71 @@
+# Test that certain objects conform to well-defined interfaces.
+
+from __future__ import absolute_import, print_function
+
+from mercurial import (
+    httppeer,
+    localrepo,
+    sshpeer,
+    ui as uimod,
+)
+
+def checkobject(o):
+    """Verify a constructed object conforms to interface rules.
+
+    An object must have __abstractmethods__ defined.
+
+    All "public" attributes of the object (attributes not prefixed with
+    an underscore) must be in __abstractmethods__ or appear on a base class
+    with __abstractmethods__.
+    """
+    name = o.__class__.__name__
+
+    allowed = set()
+    for cls in o.__class__.__mro__:
+        if not getattr(cls, '__abstractmethods__', set()):
+            continue
+
+        allowed |= cls.__abstractmethods__
+        allowed |= {a for a in dir(cls) if not a.startswith('_')}
+
+    if not allowed:
+        print('%s does not have abstract methods' % name)
+        return
+
+    public = {a for a in dir(o) if not a.startswith('_')}
+
+    for attr in sorted(public - allowed):
+        print('public attributes not in abstract interface: %s.%s' % (
+            name, attr))
+
+# Facilitates testing localpeer.
+class dummyrepo(object):
+    def __init__(self):
+        self.ui = uimod.ui()
+    def filtered(self, name):
+        pass
+    def _restrictcapabilities(self, caps):
+        pass
+
+# Facilitates testing sshpeer without requiring an SSH server.
+class testingsshpeer(sshpeer.sshpeer):
+    def _validaterepo(self, *args, **kwargs):
+        pass
+
+class badpeer(httppeer.httppeer):
+    def __init__(self):
+        super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
+        self.badattribute = True
+
+    def badmethod(self):
+        pass
+
+def main():
+    ui = uimod.ui()
+
+    checkobject(badpeer())
+    checkobject(httppeer.httppeer(ui, 'http://localhost'))
+    checkobject(localrepo.localpeer(dummyrepo()))
+    checkobject(testingsshpeer(ui, 'ssh://localhost/foo'))
+
+main()



To: indygreg, #hg-reviewers
Cc: mercurial-devel
_______________________________________________
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

Reply via email to