Mercurial > hg
view tests/test-check-interfaces.py @ 37181:0dfb5672f015
repository: define interface for local repositories
Per discussions on the mailing list and at the 4.4 and 4.6 sprints,
we want to start defining interfaces for local repository primitives
so that we a) have a better idea of what the formal interface for
various types is b) can more easily introduce alternate implementations
of various components (e.g. in Rust).
We have previously implemented interfaces that declare the peer and
wire protocol APIs using the abc module.
This commit introduces a monolithic interface for the localrepository
class. It uses zope.interface - not abc - for defining and declaring
the interface.
The newly defined "completelocalrepository" interface is objectively
horrible. It is based on what is actually in localrepository and
doesn't represent a reasonable interface definition IMO. There's lots
of... unwanted garbage in the interface. In other words, it reflects
the horrible state of the localrepository "god object." But this is
fine: a goal of this commit is to get the interface defined so that
we have an interface. Future commits can refactor the interface
into sub-interfaces, remove unwanted public attributes, etc.
I attempted to define reasonable docstrings for the various interface
members. But there are so many of them and I didn't know what some are
used for. So I was lazy in a number of places and didn't write
docstrings or detailed usage docs.
Also, the members of the interface are defined in the order they are
declared in localrepo.py. This revealed that the grouping of things
in localrepo.py is... odd.
The localrepository class now declares that it implements our newly
defined interface. Unlike abc, zope.interface doesn't check interface
conformance at type creation time (abc uses __metaclass__ magic to
validate interface conformance when a type is created - usually at
module import time). It does provide some functions for validating
class and object conformance with declared interfaces. We add these
checks to test-check-interfaces.py. We /could/ validate at run-time.
But we hold off for now. (I'm a bit scared of doing that because of
the various ways extensions monkeypatch repo instances.)
After this commit, test-check-interfaces.py will fail if the set of
public attributes on the localrepository class or instances change
without corresponding updates to the interface. This is by design.
Differential Revision: https://phab.mercurial-scm.org/D2933
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Wed, 21 Mar 2018 19:48:36 -0700 |
parents | 8e89c2bec1f7 |
children | 78103e4138b1 |
line wrap: on
line source
# Test that certain objects conform to well-defined interfaces. from __future__ import absolute_import, print_function import os from mercurial.thirdparty.zope import ( interface as zi, ) from mercurial.thirdparty.zope.interface import ( verify as ziverify, ) from mercurial import ( bundlerepo, httppeer, localrepo, repository, sshpeer, statichttprepo, ui as uimod, unionrepo, ) rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 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)) def checkzobject(o): """Verify an object with a zope interface.""" ifaces = zi.providedBy(o) if not ifaces: print('%r does not provide any zope interfaces' % o) return # Run zope.interface's built-in verification routine. This verifies that # everything that is supposed to be present is present. for iface in ifaces: ziverify.verifyObject(iface, o) # Now verify that the object provides no extra public attributes that # aren't declared as part of interfaces. allowed = set() for iface in ifaces: allowed |= set(iface.names(all=True)) public = {a for a in dir(o) if not a.startswith('_')} for attr in sorted(public - allowed): print('public attribute not declared in interfaces: %s.%s' % ( o.__class__.__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 class dummyopener(object): handlers = [] # Facilitates testing sshpeer without requiring an SSH server. class badpeer(httppeer.httppeer): def __init__(self): super(badpeer, self).__init__(None, None, None, dummyopener()) self.badattribute = True def badmethod(self): pass class dummypipe(object): def close(self): pass def main(): ui = uimod.ui() # Needed so we can open a local repo with obsstore without a warning. ui.setconfig('experimental', 'evolution.createmarkers', True) checkobject(badpeer()) checkobject(httppeer.httppeer(None, None, None, dummyopener())) checkobject(localrepo.localpeer(dummyrepo())) checkobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(), dummypipe(), None, None)) checkobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(), dummypipe(), None, None)) checkobject(bundlerepo.bundlepeer(dummyrepo())) checkobject(statichttprepo.statichttppeer(dummyrepo())) checkobject(unionrepo.unionpeer(dummyrepo())) ziverify.verifyClass(repository.completelocalrepository, localrepo.localrepository) repo = localrepo.localrepository(ui, rootdir) checkzobject(repo) main()