source: trunk/misc/coding_tools/check-interfaces.py

Last change on this file was b856238, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-15T15:53:34Z

remove old Python2 future statements

  • Property mode set to 100644
File size: 8.8 KB
Line 
1
2# To check a particular Tahoe source distribution, this should be invoked from
3# the root directory of that distribution as
4#
5#   bin/tahoe @misc/coding_tools/check-interfaces.py
6
7
8import os, sys, re, platform
9
10import zope.interface as zi
11# We use the forked version of verifyClass below.
12#from zope.interface.verify import verifyClass
13from zope.interface.advice import addClassAdvisor
14
15
16interesting_modules = re.compile(r'(allmydata)|(foolscap)\..*')
17excluded_classnames = re.compile(r'(_)|(Mock)|(Fake)|(Dummy).*')
18excluded_file_basenames = re.compile(r'(check)|(bench)_.*')
19
20
21_other_modules_with_violations = set()
22_err = sys.stderr
23_report_argname_mismatch = False  # very noisy and usually not important
24
25
26# deep magic
27def strictly_implements(*interfaces):
28    frame = sys._getframe(1)
29    f_locals = frame.f_locals
30
31    # Try to make sure we were called from a class def. Assumes Python > 2.2.
32    if f_locals is frame.f_globals or '__module__' not in f_locals:
33        raise TypeError("implements can be used only from a class definition.")
34
35    if '__implements_advice_data__' in f_locals:
36        raise TypeError("implements can be used only once in a class definition.")
37
38    def _implements_advice(cls):
39        interfaces, classImplements = cls.__dict__['__implements_advice_data__']
40        del cls.__implements_advice_data__
41        classImplements(cls, *interfaces)
42
43        if interesting_modules.match(cls.__module__):
44            if not excluded_classnames.match(cls.__name__):
45                for interface in interfaces:
46                    try:
47                        verifyClass(interface, cls)
48                    except Exception as e:
49                        print("%s.%s does not correctly implement %s.%s:\n%s"
50                                       % (cls.__module__, cls.__name__,
51                                          interface.__module__, interface.__name__, e), file=_err)
52        else:
53            _other_modules_with_violations.add(cls.__module__)
54        return cls
55
56    f_locals['__implements_advice_data__'] = interfaces, zi.classImplements
57    addClassAdvisor(_implements_advice, depth=2)
58
59
60def check():
61    # patchee-monkey
62    zi.implements = strictly_implements
63
64    if len(sys.argv) >= 2:
65        if sys.argv[1] == '--help' or len(sys.argv) > 2:
66            print("Usage: check-miscaptures.py [SOURCEDIR]", file=_err)
67            return
68        srcdir = sys.argv[1]
69    else:
70        # import modules under src/ by default
71        srcdir = 'src'
72
73    # attempt to avoid side-effects from importing command scripts
74    sys.argv = ['', '--help']
75
76    syslow = platform.system().lower()
77    is_windows = 'cygwin' in syslow or 'windows' in syslow
78
79    for (dirpath, dirnames, filenames) in os.walk(srcdir):
80        for fn in filenames:
81            (basename, ext) = os.path.splitext(fn)
82            if ext in ('.pyc', '.pyo') and not os.path.exists(os.path.join(dirpath, basename+'.py')):
83                print("Warning: no .py source file for %r.\n"
84                               % (os.path.join(dirpath, fn),), file=_err)
85
86            if ext == '.py' and not excluded_file_basenames.match(basename):
87                relpath = os.path.join(dirpath[len(srcdir)+1:], basename)
88                module = relpath.replace(os.sep, '/').replace('/', '.')
89                try:
90                    __import__(module)
91                except ImportError as e:
92                    if not is_windows and (' _win' in str(e) or 'win32' in str(e)):
93                        print("Warning: %r imports a Windows-specific module, so we cannot check it (%s).\n"
94                                       % (module, str(e)), file=_err)
95                    else:
96                        import traceback
97                        traceback.print_exc(file=_err)
98                        print(file=_err)
99
100    others = list(_other_modules_with_violations)
101    others.sort()
102    print("There were also interface violations in:\n", ", ".join(others), "\n", file=_err)
103
104
105# Forked from
106# http://svn.zope.org/*checkout*/Zope3/trunk/src/zope/interface/verify.py?content-type=text%2Fplain&rev=27687
107# but modified to report all interface violations rather than just the first.
108
109##############################################################################
110#
111# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
112# All Rights Reserved.
113#
114# This software is subject to the provisions of the Zope Public License,
115# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
116# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
117# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
118# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
119# FOR A PARTICULAR PURPOSE.
120#
121##############################################################################
122"""Verify interface implementations
123
124$Id$
125"""
126from zope.interface.exceptions import DoesNotImplement
127from zope.interface.exceptions import BrokenMethodImplementation
128from types import FunctionType, MethodType
129from zope.interface.interface import fromMethod, fromFunction, Method
130
131# This will be monkey-patched when running under Zope 2, so leave this
132# here:
133MethodTypes = (MethodType, )
134
135
136def _verify(iface, candidate, tentative=0, vtype=None):
137    """Verify that 'candidate' might correctly implements 'iface'.
138
139    This involves:
140
141      o Making sure the candidate defines all the necessary methods
142
143      o Making sure the methods have the correct signature
144
145      o Making sure the candidate asserts that it implements the interface
146
147    Note that this isn't the same as verifying that the class does
148    implement the interface.
149
150    If optional tentative is true, suppress the "is implemented by" test.
151    """
152
153    if vtype == 'c':
154        tester = iface.implementedBy
155    else:
156        tester = iface.providedBy
157
158    violations = []
159    def format(e):
160        return "    " + str(e).strip() + "\n"
161
162    if not tentative and not tester(candidate):
163        violations.append(format(DoesNotImplement(iface)))
164
165    # Here the `desc` is either an `Attribute` or `Method` instance
166    for name, desc in iface.namesAndDescriptions(1):
167        if not hasattr(candidate, name):
168            if (not isinstance(desc, Method)) and vtype == 'c':
169                # We can't verify non-methods on classes, since the
170                # class may provide attrs in it's __init__.
171                continue
172
173            if isinstance(desc, Method):
174                violations.append("    The %r method was not provided.\n" % (name,))
175            else:
176                violations.append("    The %r attribute was not provided.\n" % (name,))
177            continue
178
179        attr = getattr(candidate, name)
180        if not isinstance(desc, Method):
181            # If it's not a method, there's nothing else we can test
182            continue
183
184        if isinstance(attr, FunctionType):
185            # should never get here, since classes should not provide functions
186            meth = fromFunction(attr, iface, name=name)
187        elif (isinstance(attr, MethodTypes)
188              and type(attr.im_func) is FunctionType):
189            meth = fromMethod(attr, iface, name)
190        else:
191            if not callable(attr):
192                violations.append(format(BrokenMethodImplementation(name, "Not a method")))
193            # sigh, it's callable, but we don't know how to intrspect it, so
194            # we have to give it a pass.
195            continue
196
197        # Make sure that the required and implemented method signatures are
198        # the same.
199        desc = desc.getSignatureInfo()
200        meth = meth.getSignatureInfo()
201
202        mess = _incompat(desc, meth)
203        if mess:
204            violations.append(format(BrokenMethodImplementation(name, mess)))
205
206    if violations:
207        raise Exception("".join(violations))
208    return True
209
210def verifyClass(iface, candidate, tentative=0):
211    return _verify(iface, candidate, tentative, vtype='c')
212
213def verifyObject(iface, candidate, tentative=0):
214    return _verify(iface, candidate, tentative, vtype='o')
215
216def _incompat(required, implemented):
217    if len(implemented['required']) > len(required['required']):
218        return 'implementation requires too many arguments'
219    if ((len(implemented['positional']) < len(required['positional']))
220        and not implemented['varargs']):
221        return "implementation doesn't allow enough arguments"
222    if required['kwargs'] and not implemented['kwargs']:
223        return "implementation doesn't support keyword arguments"
224    if required['varargs'] and not implemented['varargs']:
225        return "implementation doesn't support variable arguments"
226    if (_report_argname_mismatch and required['positional'] !=
227        implemented['positional'][:len(required['positional'])]
228        and implemented['kwargs'] is None):
229        return 'implementation has different argument names'
230
231
232if __name__ == "__main__":
233    check()
234    # Avoid spurious warnings about ignored exceptions during shutdown by doing a hard exit.
235    os._exit(0)
Note: See TracBrowser for help on using the repository browser.