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.
146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
import re
|
|
import sys
|
|
import importlib
|
|
import types
|
|
import inspect
|
|
import enum
|
|
|
|
import pytest
|
|
|
|
import trio
|
|
import trio.testing
|
|
|
|
from .. import _core
|
|
from .. import _util
|
|
|
|
|
|
def test_core_is_properly_reexported():
|
|
# Each export from _core should be re-exported by exactly one of these
|
|
# three modules:
|
|
sources = [trio, trio.lowlevel, trio.testing]
|
|
for symbol in dir(_core):
|
|
if symbol.startswith("_") or symbol == "tests":
|
|
continue
|
|
found = 0
|
|
for source in sources:
|
|
if symbol in dir(source) and getattr(source, symbol) is getattr(
|
|
_core, symbol
|
|
):
|
|
found += 1
|
|
print(symbol, found)
|
|
assert found == 1
|
|
|
|
|
|
def public_modules(module):
|
|
yield module
|
|
for name, class_ in module.__dict__.items():
|
|
if name.startswith("_"): # pragma: no cover
|
|
continue
|
|
if not isinstance(class_, types.ModuleType):
|
|
continue
|
|
if not class_.__name__.startswith(module.__name__): # pragma: no cover
|
|
continue
|
|
if class_ is module:
|
|
continue
|
|
# We should rename the trio.tests module (#274), but until then we use
|
|
# a special-case hack:
|
|
if class_.__name__ == "trio.tests":
|
|
continue
|
|
yield from public_modules(class_)
|
|
|
|
|
|
PUBLIC_MODULES = list(public_modules(trio))
|
|
PUBLIC_MODULE_NAMES = [m.__name__ for m in PUBLIC_MODULES]
|
|
|
|
|
|
# It doesn't make sense for downstream redistributors to run this test, since
|
|
# they might be using a newer version of Python with additional symbols which
|
|
# won't be reflected in trio.socket, and this shouldn't cause downstream test
|
|
# runs to start failing.
|
|
@pytest.mark.redistributors_should_skip
|
|
# pylint/jedi often have trouble with alpha releases, where Python's internals
|
|
# are in flux, grammar may not have settled down, etc.
|
|
@pytest.mark.skipif(
|
|
sys.version_info.releaselevel == "alpha",
|
|
reason="skip static introspection tools on Python dev/alpha releases",
|
|
)
|
|
@pytest.mark.parametrize("modname", PUBLIC_MODULE_NAMES)
|
|
@pytest.mark.parametrize("tool", ["pylint", "jedi"])
|
|
@pytest.mark.filterwarnings(
|
|
# https://github.com/pypa/setuptools/issues/3274
|
|
"ignore:module 'sre_constants' is deprecated:DeprecationWarning",
|
|
)
|
|
def test_static_tool_sees_all_symbols(tool, modname):
|
|
module = importlib.import_module(modname)
|
|
|
|
def no_underscores(symbols):
|
|
return {symbol for symbol in symbols if not symbol.startswith("_")}
|
|
|
|
runtime_names = no_underscores(dir(module))
|
|
|
|
# We should rename the trio.tests module (#274), but until then we use a
|
|
# special-case hack:
|
|
if modname == "trio":
|
|
runtime_names.remove("tests")
|
|
|
|
if tool == "pylint":
|
|
from pylint.lint import PyLinter
|
|
|
|
linter = PyLinter()
|
|
ast = linter.get_ast(module.__file__, modname)
|
|
static_names = no_underscores(ast)
|
|
elif tool == "jedi":
|
|
import jedi
|
|
|
|
# Simulate typing "import trio; trio.<TAB>"
|
|
script = jedi.Script("import {}; {}.".format(modname, modname))
|
|
completions = script.complete()
|
|
static_names = no_underscores(c.name for c in completions)
|
|
else: # pragma: no cover
|
|
assert False
|
|
|
|
# It's expected that the static set will contain more names than the
|
|
# runtime set:
|
|
# - static tools are sometimes sloppy and include deleted names
|
|
# - some symbols are platform-specific at runtime, but always show up in
|
|
# static analysis (e.g. in trio.socket or trio.lowlevel)
|
|
# So we check that the runtime names are a subset of the static names.
|
|
missing_names = runtime_names - static_names
|
|
if missing_names: # pragma: no cover
|
|
print("{} can't see the following names in {}:".format(tool, modname))
|
|
print()
|
|
for name in sorted(missing_names):
|
|
print(" {}".format(name))
|
|
assert False
|
|
|
|
|
|
def test_classes_are_final():
|
|
for module in PUBLIC_MODULES:
|
|
for name, class_ in module.__dict__.items():
|
|
if not isinstance(class_, type):
|
|
continue
|
|
# Deprecated classes are exported with a leading underscore
|
|
if name.startswith("_"): # pragma: no cover
|
|
continue
|
|
|
|
# Abstract classes can be subclassed, because that's the whole
|
|
# point of ABCs
|
|
if inspect.isabstract(class_):
|
|
continue
|
|
# Exceptions are allowed to be subclassed, because exception
|
|
# subclassing isn't used to inherit behavior.
|
|
if issubclass(class_, BaseException):
|
|
continue
|
|
# These are classes that are conceptually abstract, but
|
|
# inspect.isabstract returns False for boring reasons.
|
|
if class_ in {trio.abc.Instrument, trio.socket.SocketType}:
|
|
continue
|
|
# Enums have their own metaclass, so we can't use our metaclasses.
|
|
# And I don't think there's a lot of risk from people subclassing
|
|
# enums...
|
|
if issubclass(class_, enum.Enum):
|
|
continue
|
|
# ... insert other special cases here ...
|
|
|
|
assert isinstance(class_, _util.Final)
|