You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
10 KiB
Python
295 lines
10 KiB
Python
5 years ago
|
# Check __all__, __implements__, __extensions__, __imports__ of the modules
|
||
|
|
||
|
from __future__ import print_function
|
||
|
from __future__ import absolute_import
|
||
|
|
||
|
|
||
|
import functools
|
||
|
import sys
|
||
|
import unittest
|
||
|
import types
|
||
|
import importlib
|
||
|
import warnings
|
||
|
|
||
|
from gevent.testing import six
|
||
|
from gevent.testing import modules
|
||
|
from gevent.testing.sysinfo import PLATFORM_SPECIFIC_SUFFIXES
|
||
|
from gevent.testing.util import debug
|
||
|
|
||
|
from gevent._patcher import MAPPING
|
||
|
|
||
|
class ANY(object):
|
||
|
def __contains__(self, item):
|
||
|
return True
|
||
|
|
||
|
ANY = ANY()
|
||
|
|
||
|
NOT_IMPLEMENTED = {
|
||
|
'socket': ['CAPI'],
|
||
|
'thread': ['allocate', 'exit_thread', 'interrupt_main', 'start_new'],
|
||
|
'select': ANY,
|
||
|
'os': ANY,
|
||
|
'threading': ANY,
|
||
|
'__builtin__' if six.PY2 else 'builtins': ANY,
|
||
|
'signal': ANY,
|
||
|
}
|
||
|
|
||
|
COULD_BE_MISSING = {
|
||
|
'socket': ['create_connection', 'RAND_add', 'RAND_egd', 'RAND_status'],
|
||
|
'subprocess': ['_posixsubprocess'],
|
||
|
}
|
||
|
|
||
|
# Things without an __all__ should generally be internal implementation
|
||
|
# helpers
|
||
|
NO_ALL = {
|
||
|
'gevent.threading',
|
||
|
'gevent._compat',
|
||
|
'gevent._corecffi',
|
||
|
'gevent._ffi',
|
||
|
'gevent._fileobjectcommon',
|
||
|
'gevent._fileobjectposix',
|
||
|
'gevent._patcher',
|
||
|
'gevent._socketcommon',
|
||
|
'gevent._tblib',
|
||
|
'gevent._util',
|
||
|
'gevent.resolver._addresses',
|
||
|
'gevent.resolver._hostsfile',
|
||
|
}
|
||
|
|
||
|
ALLOW_IMPLEMENTS = [
|
||
|
'gevent._queue',
|
||
|
# 'gevent.resolver.dnspython',
|
||
|
# 'gevent.resolver_thread',
|
||
|
# 'gevent.resolver.blocking',
|
||
|
# 'gevent.resolver_ares',
|
||
|
# 'gevent.server',
|
||
|
# 'gevent._resolver.hostfile',
|
||
|
# 'gevent.util',
|
||
|
# 'gevent.threadpool',
|
||
|
# 'gevent.timeout',
|
||
|
]
|
||
|
|
||
|
# A list of modules that may contain things that aren't actually, technically,
|
||
|
# extensions, but that need to be in __extensions__ anyway due to the way,
|
||
|
# for example, monkey patching, needs to work.
|
||
|
EXTRA_EXTENSIONS = []
|
||
|
if sys.platform.startswith('win'):
|
||
|
EXTRA_EXTENSIONS.append('gevent.signal')
|
||
|
|
||
|
|
||
|
|
||
|
_MISSING = '<marker object>'
|
||
|
|
||
|
def skip_if_no_stdlib_counterpart(f):
|
||
|
@functools.wraps(f)
|
||
|
def m(self):
|
||
|
if not self.stdlib_module:
|
||
|
self.skipTest("Need stdlib counterpart to %s" % self.modname)
|
||
|
f(self)
|
||
|
|
||
|
return m
|
||
|
|
||
|
class AbstractTestMixin(object):
|
||
|
modname = None
|
||
|
stdlib_has_all = False
|
||
|
stdlib_all = None
|
||
|
stdlib_name = None
|
||
|
stdlib_module = None
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
modname = cls.modname
|
||
|
if modname.endswith(PLATFORM_SPECIFIC_SUFFIXES):
|
||
|
raise unittest.SkipTest("Module %s is platform specific" % modname)
|
||
|
|
||
|
|
||
|
with warnings.catch_warnings():
|
||
|
warnings.simplefilter('ignore', DeprecationWarning)
|
||
|
try:
|
||
|
cls.module = importlib.import_module(modname)
|
||
|
except ImportError:
|
||
|
if modname in modules.OPTIONAL_MODULES:
|
||
|
msg = "Unable to import %s" % modname
|
||
|
raise unittest.SkipTest(msg)
|
||
|
raise
|
||
|
|
||
|
cls.__implements__ = getattr(cls.module, '__implements__', None)
|
||
|
cls.__imports__ = getattr(cls.module, '__imports__', [])
|
||
|
cls.__extensions__ = getattr(cls.module, '__extensions__', [])
|
||
|
|
||
|
cls.stdlib_name = MAPPING.get(modname)
|
||
|
|
||
|
if cls.stdlib_name is not None:
|
||
|
try:
|
||
|
cls.stdlib_module = __import__(cls.stdlib_name)
|
||
|
except ImportError:
|
||
|
pass
|
||
|
else:
|
||
|
cls.stdlib_has_all = True
|
||
|
cls.stdlib_all = getattr(cls.stdlib_module, '__all__', None)
|
||
|
if cls.stdlib_all is None:
|
||
|
cls.stdlib_has_all = False
|
||
|
cls.stdlib_all = [
|
||
|
name
|
||
|
for name in dir(cls.stdlib_module)
|
||
|
if not name.startswith('_')
|
||
|
and not isinstance(getattr(cls.stdlib_module, name), types.ModuleType)
|
||
|
]
|
||
|
|
||
|
def skipIfNoAll(self):
|
||
|
if not hasattr(self.module, '__all__'):
|
||
|
self.assertIn(self.modname, NO_ALL)
|
||
|
self.skipTest("%s Needs __all__" % self.modname)
|
||
|
|
||
|
def test_all(self):
|
||
|
# Check that __all__ is present and does not contain invalid entries
|
||
|
self.skipIfNoAll()
|
||
|
names = {}
|
||
|
six.exec_("from %s import *" % self.modname, names)
|
||
|
names.pop('__builtins__', None)
|
||
|
self.assertEqual(sorted(names), sorted(self.module.__all__))
|
||
|
|
||
|
def test_all_formula(self):
|
||
|
self.skipIfNoAll()
|
||
|
# Check __all__ = __implements__ + __extensions__ + __imported__
|
||
|
# This is disabled because it was previously being skipped entirely
|
||
|
# back when we had to call things manually. In that time, it drifted
|
||
|
# out of sync. It should be enabled again and problems corrected.
|
||
|
all_calculated = (
|
||
|
tuple(self.__implements__ or ())
|
||
|
+ tuple(self.__imports__ or ())
|
||
|
+ tuple(self.__extensions__ or ())
|
||
|
)
|
||
|
try:
|
||
|
self.assertEqual(sorted(all_calculated),
|
||
|
sorted(self.module.__all__))
|
||
|
except AssertionError:
|
||
|
self.skipTest("Module %s fails the all formula; fix it" % self.modname)
|
||
|
|
||
|
def test_implements_presence_justified(self):
|
||
|
# Check that __implements__ is present only if the module is modeled
|
||
|
# after a module from stdlib (like gevent.socket).
|
||
|
|
||
|
if self.modname in ALLOW_IMPLEMENTS:
|
||
|
return
|
||
|
if self.__implements__ is not None and self.stdlib_module is None:
|
||
|
raise AssertionError(
|
||
|
'%s (%r) has __implements__ (%s) but no stdlib counterpart (%s)'
|
||
|
% (self.modname, self.module, self.__implements__, self.stdlib_name))
|
||
|
|
||
|
@skip_if_no_stdlib_counterpart
|
||
|
def test_implements_subset_of_stdlib_all(self):
|
||
|
# Check that __implements__ + __imports__ is a subset of the
|
||
|
# corresponding standard module __all__ or dir()
|
||
|
for name in tuple(self.__implements__ or ()) + tuple(self.__imports__):
|
||
|
if name in self.stdlib_all:
|
||
|
continue
|
||
|
if name in COULD_BE_MISSING.get(self.stdlib_name, ()):
|
||
|
continue
|
||
|
if name in dir(self.stdlib_module): # like thread._local which is not in thread.__all__
|
||
|
continue
|
||
|
raise AssertionError('%r is not found in %r.__all__ nor in dir(%r)' % (name, self.stdlib_module, self.stdlib_module))
|
||
|
|
||
|
@skip_if_no_stdlib_counterpart
|
||
|
def test_implements_actually_implements(self):
|
||
|
# Check that the module actually implements the entries from
|
||
|
# __implements__
|
||
|
|
||
|
for name in self.__implements__ or ():
|
||
|
item = getattr(self.module, name)
|
||
|
try:
|
||
|
stdlib_item = getattr(self.stdlib_module, name)
|
||
|
self.assertIsNot(item, stdlib_item)
|
||
|
except AttributeError:
|
||
|
if name not in COULD_BE_MISSING.get(self.stdlib_name, []):
|
||
|
raise
|
||
|
|
||
|
@skip_if_no_stdlib_counterpart
|
||
|
def test_imports_actually_imports(self):
|
||
|
# Check that the module actually imports the entries from
|
||
|
# __imports__
|
||
|
for name in self.__imports__:
|
||
|
item = getattr(self.module, name)
|
||
|
stdlib_item = getattr(self.stdlib_module, name)
|
||
|
self.assertIs(item, stdlib_item)
|
||
|
|
||
|
@skip_if_no_stdlib_counterpart
|
||
|
def test_extensions_actually_extend(self):
|
||
|
# Check that the module actually defines new entries in
|
||
|
# __extensions__
|
||
|
|
||
|
if self.modname in EXTRA_EXTENSIONS:
|
||
|
return
|
||
|
for name in self.__extensions__:
|
||
|
if hasattr(self.stdlib_module, name):
|
||
|
raise AssertionError("'%r' is not an extension, it is found in %r" % (name, self.stdlib_module))
|
||
|
|
||
|
@skip_if_no_stdlib_counterpart
|
||
|
def test_completeness(self): # pylint:disable=too-many-branches
|
||
|
# Check that __all__ (or dir()) of the corresponsing stdlib is
|
||
|
# a subset of __all__ of this module
|
||
|
|
||
|
missed = []
|
||
|
for name in self.stdlib_all:
|
||
|
if name not in getattr(self.module, '__all__', []):
|
||
|
missed.append(name)
|
||
|
|
||
|
# handle stuff like ssl.socket and ssl.socket_error which have no reason to be in gevent.ssl.__all__
|
||
|
if not self.stdlib_has_all:
|
||
|
for name in missed[:]:
|
||
|
if hasattr(self.module, name):
|
||
|
missed.remove(name)
|
||
|
|
||
|
# remove known misses
|
||
|
not_implemented = NOT_IMPLEMENTED.get(self.stdlib_name)
|
||
|
if not_implemented is not None:
|
||
|
result = []
|
||
|
for name in missed:
|
||
|
if name in not_implemented:
|
||
|
# We often don't want __all__ to be set because we wind up
|
||
|
# documenting things that we just copy in from the stdlib.
|
||
|
# But if we implement it, don't print a warning
|
||
|
if getattr(self.module, name, _MISSING) is _MISSING:
|
||
|
debug('IncompleteImplWarning: %s.%s' % (self.modname, name))
|
||
|
else:
|
||
|
result.append(name)
|
||
|
missed = result
|
||
|
|
||
|
if missed:
|
||
|
if self.stdlib_has_all:
|
||
|
msg = '''The following items
|
||
|
in %r.__all__
|
||
|
are missing from %r:
|
||
|
%r''' % (self.stdlib_module, self.module, missed)
|
||
|
else:
|
||
|
msg = '''The following items
|
||
|
in dir(%r)
|
||
|
are missing from %r:
|
||
|
%r''' % (self.stdlib_module, self.module, missed)
|
||
|
raise AssertionError(msg)
|
||
|
|
||
|
|
||
|
def _create_tests():
|
||
|
for _, modname in modules.walk_modules(include_so=False, recursive=True,
|
||
|
check_optional=False):
|
||
|
if modname.endswith(PLATFORM_SPECIFIC_SUFFIXES):
|
||
|
continue
|
||
|
|
||
|
orig_modname = modname
|
||
|
modname_no_period = orig_modname.replace('.', '_')
|
||
|
|
||
|
cls = type(
|
||
|
'Test_' + modname_no_period,
|
||
|
(AbstractTestMixin, unittest.TestCase),
|
||
|
{
|
||
|
'__module__': __name__,
|
||
|
'modname': orig_modname
|
||
|
}
|
||
|
)
|
||
|
globals()[cls.__name__] = cls
|
||
|
|
||
|
_create_tests()
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
unittest.main()
|