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

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)