ansible-test - More PEP 484 type hints.

pull/75793/head
Matt Clay 4 years ago
parent 263fd64759
commit 0d5a9f2138

@ -84,6 +84,7 @@ from ...test import (
TestFailure, TestFailure,
TestSkipped, TestSkipped,
TestMessage, TestMessage,
TestResult,
calculate_best_confidence, calculate_best_confidence,
) )
@ -125,10 +126,8 @@ TARGET_SANITY_ROOT = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'sanity')
created_venvs = [] # type: t.List[str] created_venvs = [] # type: t.List[str]
def command_sanity(args): def command_sanity(args): # type: (SanityConfig) -> None
""" """Run sanity tests."""
:type args: SanityConfig
"""
create_result_directories(args) create_result_directories(args)
target_configs = t.cast(t.List[PosixConfig], args.targets) target_configs = t.cast(t.List[PosixConfig], args.targets)
@ -606,33 +605,25 @@ class SanityIgnoreProcessor:
class SanitySuccess(TestSuccess): class SanitySuccess(TestSuccess):
"""Sanity test success.""" """Sanity test success."""
def __init__(self, test, python_version=None): def __init__(self, test, python_version=None): # type: (str, t.Optional[str]) -> None
"""
:type test: str
:type python_version: str
"""
super().__init__(COMMAND, test, python_version) super().__init__(COMMAND, test, python_version)
class SanitySkipped(TestSkipped): class SanitySkipped(TestSkipped):
"""Sanity test skipped.""" """Sanity test skipped."""
def __init__(self, test, python_version=None): def __init__(self, test, python_version=None): # type: (str, t.Optional[str]) -> None
"""
:type test: str
:type python_version: str
"""
super().__init__(COMMAND, test, python_version) super().__init__(COMMAND, test, python_version)
class SanityFailure(TestFailure): class SanityFailure(TestFailure):
"""Sanity test failure.""" """Sanity test failure."""
def __init__(self, test, python_version=None, messages=None, summary=None): def __init__(
""" self,
:type test: str test, # type: str
:type python_version: str python_version=None, # type: t.Optional[str]
:type messages: list[SanityMessage] messages=None, # type: t.Optional[t.List[SanityMessage]]
:type summary: unicode summary=None, # type: t.Optional[str]
""" ): # type: (...) -> None
super().__init__(COMMAND, test, python_version, messages, summary) super().__init__(COMMAND, test, python_version, messages, summary)
@ -809,13 +800,8 @@ class SanitySingleVersion(SanityTest, metaclass=abc.ABCMeta):
return False return False
@abc.abstractmethod @abc.abstractmethod
def test(self, args, targets, python): def test(self, args, targets, python): # type: (SanityConfig, SanityTargets, PythonConfig) -> TestResult
""" """Run the sanity test and return the result."""
:type args: SanityConfig
:type targets: SanityTargets
:type python: PythonConfig
:rtype: TestResult
"""
def load_processor(self, args): # type: (SanityConfig) -> SanityIgnoreProcessor def load_processor(self, args): # type: (SanityConfig) -> SanityIgnoreProcessor
"""Load the ignore processor for this sanity test.""" """Load the ignore processor for this sanity test."""
@ -947,13 +933,8 @@ class SanityCodeSmellTest(SanitySingleVersion):
return targets return targets
def test(self, args, targets, python): def test(self, args, targets, python): # type: (SanityConfig, SanityTargets, PythonConfig) -> TestResult
""" """Run the sanity test and return the result."""
:type args: SanityConfig
:type targets: SanityTargets
:type python: PythonConfig
:rtype: TestResult
"""
cmd = [python.path, self.path] cmd = [python.path, self.path]
env = ansible_environment(args, color=False) env = ansible_environment(args, color=False)
@ -1027,12 +1008,8 @@ class SanityCodeSmellTest(SanitySingleVersion):
class SanityVersionNeutral(SanityTest, metaclass=abc.ABCMeta): class SanityVersionNeutral(SanityTest, metaclass=abc.ABCMeta):
"""Base class for sanity test plugins which are idependent of the python version being used.""" """Base class for sanity test plugins which are idependent of the python version being used."""
@abc.abstractmethod @abc.abstractmethod
def test(self, args, targets): def test(self, args, targets): # type: (SanityConfig, SanityTargets) -> TestResult
""" """Run the sanity test and return the result."""
:type args: SanityConfig
:type targets: SanityTargets
:rtype: TestResult
"""
def load_processor(self, args): # type: (SanityConfig) -> SanityIgnoreProcessor def load_processor(self, args): # type: (SanityConfig) -> SanityIgnoreProcessor
"""Load the ignore processor for this sanity test.""" """Load the ignore processor for this sanity test."""
@ -1047,13 +1024,8 @@ class SanityVersionNeutral(SanityTest, metaclass=abc.ABCMeta):
class SanityMultipleVersion(SanityTest, metaclass=abc.ABCMeta): class SanityMultipleVersion(SanityTest, metaclass=abc.ABCMeta):
"""Base class for sanity test plugins which should run on multiple python versions.""" """Base class for sanity test plugins which should run on multiple python versions."""
@abc.abstractmethod @abc.abstractmethod
def test(self, args, targets, python): def test(self, args, targets, python): # type: (SanityConfig, SanityTargets, PythonConfig) -> TestResult
""" """Run the sanity test and return the result."""
:type args: SanityConfig
:type targets: SanityTargets
:type python: PythonConfig
:rtype: TestResult
"""
def load_processor(self, args, python_version): # type: (SanityConfig, str) -> SanityIgnoreProcessor def load_processor(self, args, python_version): # type: (SanityConfig, str) -> SanityIgnoreProcessor
"""Load the ignore processor for this sanity test.""" """Load the ignore processor for this sanity test."""

@ -29,6 +29,7 @@ from ...target import (
walk_windows_integration_targets, walk_windows_integration_targets,
walk_integration_targets, walk_integration_targets,
walk_module_targets, walk_module_targets,
CompletionTarget,
) )
from ..integration.cloud import ( from ..integration.cloud import (
@ -169,12 +170,8 @@ class IntegrationAliasesTest(SanitySingleVersion):
return self._ci_test_groups return self._ci_test_groups
def format_test_group_alias(self, name, fallback=''): def format_test_group_alias(self, name, fallback=''): # type: (str, str) -> str
""" """Return a test group alias using the given name and fallback."""
:type name: str
:type fallback: str
:rtype: str
"""
group_numbers = self.ci_test_groups.get(name, None) group_numbers = self.ci_test_groups.get(name, None)
if group_numbers: if group_numbers:
@ -232,11 +229,8 @@ class IntegrationAliasesTest(SanitySingleVersion):
return SanitySuccess(self.name) return SanitySuccess(self.name)
def check_posix_targets(self, args): def check_posix_targets(self, args): # type: (SanityConfig) -> t.List[SanityMessage]
""" """Check POSIX integration test targets and return messages with any issues found."""
:type args: SanityConfig
:rtype: list[SanityMessage]
"""
posix_targets = tuple(walk_posix_integration_targets()) posix_targets = tuple(walk_posix_integration_targets())
clouds = get_cloud_platforms(args, posix_targets) clouds = get_cloud_platforms(args, posix_targets)
@ -298,13 +292,13 @@ class IntegrationAliasesTest(SanitySingleVersion):
return messages return messages
def check_ci_group(self, targets, find, find_incidental=None): def check_ci_group(
""" self,
:type targets: tuple[CompletionTarget] targets, # type: t.Tuple[CompletionTarget, ...]
:type find: str find, # type: str
:type find_incidental: list[str] | None find_incidental=None, # type: t.Optional[t.List[str]]
:rtype: list[SanityMessage] ): # type: (...) -> t.List[SanityMessage]
""" """Check the CI groups set in the provided targets and return a list of messages with any issues found."""
all_paths = set(target.path for target in targets) all_paths = set(target.path for target in targets)
supported_paths = set(target.path for target in filter_targets(targets, [find], directories=False, errors=False)) supported_paths = set(target.path for target in filter_targets(targets, [find], directories=False, errors=False))
unsupported_paths = set(target.path for target in filter_targets(targets, [self.UNSUPPORTED], directories=False, errors=False)) unsupported_paths = set(target.path for target in filter_targets(targets, [self.UNSUPPORTED], directories=False, errors=False))
@ -330,11 +324,8 @@ class IntegrationAliasesTest(SanitySingleVersion):
return messages return messages
def check_changes(self, args, results): def check_changes(self, args, results): # type: (SanityConfig, t.Dict[str, t.Any]) -> None
""" """Check changes and store results in the provided results dictionary."""
:type args: SanityConfig
:type results: dict[str, any]
"""
integration_targets = list(walk_integration_targets()) integration_targets = list(walk_integration_targets())
module_targets = list(walk_module_targets()) module_targets = list(walk_module_targets())
@ -381,12 +372,8 @@ class IntegrationAliasesTest(SanitySingleVersion):
results['comments'] += comments results['comments'] += comments
results['labels'].update(labels) results['labels'].update(labels)
def format_comment(self, template, targets): def format_comment(self, template, targets): # type: (str, t.List[str]) -> t.Optional[str]
""" """Format and return a comment based on the given template and targets, or None if there are no targets."""
:type template: str
:type targets: list[str]
:rtype: str | None
"""
if not targets: if not targets:
return None return None

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import functools
import shlex import shlex
import sys import sys
import tempfile import tempfile
@ -160,6 +159,10 @@ class SshConnection(Connection):
options.append(f'{self.settings.user}@{self.settings.host}') options.append(f'{self.settings.user}@{self.settings.host}')
options.append(' '.join(shlex.quote(cmd) for cmd in command)) options.append(' '.join(shlex.quote(cmd) for cmd in command))
def error_callback(ex): # type: (SubprocessError) -> None
"""Error handler."""
self.capture_log_details(ssh_logfile.name, ex)
return run_command( return run_command(
args=self.args, args=self.args,
cmd=['ssh'] + options, cmd=['ssh'] + options,
@ -167,7 +170,7 @@ class SshConnection(Connection):
data=data, data=data,
stdin=stdin, stdin=stdin,
stdout=stdout, stdout=stdout,
error_callback=functools.partial(self.capture_log_details, ssh_logfile.name), error_callback=error_callback,
) )
@staticmethod @staticmethod

@ -291,15 +291,14 @@ def generate_command(
return cmd return cmd
def filter_options(args, argv, options, exclude, require): def filter_options(
""" args, # type: EnvironmentConfig
:type args: EnvironmentConfig argv, # type: t.List[str]
:type argv: list[str] options, # type: t.Dict[str, int]
:type options: dict[str, int] exclude, # type: t.List[str]
:type exclude: list[str] require, # type: t.List[str]
:type require: list[str] ): # type: (...) -> t.Iterable[str]
:rtype: collections.Iterable[str] """Return an iterable that filters out unwanted CLI options and injects new ones as requested."""
"""
options = options.copy() options = options.copy()
options['--truncate'] = 1 options['--truncate'] = 1

@ -11,13 +11,6 @@ from ..util import (
) )
try:
# noinspection PyTypeChecker
TPathProvider = t.TypeVar('TPathProvider', bound='PathProvider')
except AttributeError:
TPathProvider = None # pylint: disable=invalid-name
def get_path_provider_classes(provider_type): # type: (t.Type[TPathProvider]) -> t.List[t.Type[TPathProvider]] def get_path_provider_classes(provider_type): # type: (t.Type[TPathProvider]) -> t.List[t.Type[TPathProvider]]
"""Return a list of path provider classes of the given type.""" """Return a list of path provider classes of the given type."""
return sorted(get_subclasses(provider_type), key=lambda c: (c.priority, c.__name__)) return sorted(get_subclasses(provider_type), key=lambda c: (c.priority, c.__name__))
@ -74,3 +67,6 @@ class PathProvider(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
def is_content_root(path): # type: (str) -> bool def is_content_root(path): # type: (str) -> bool
"""Return True if the given path is a content root for this provider.""" """Return True if the given path is a content root for this provider."""
TPathProvider = t.TypeVar('TPathProvider', bound=PathProvider)

@ -32,26 +32,9 @@ from .data import (
MODULE_EXTENSIONS = '.py', '.ps1' MODULE_EXTENSIONS = '.py', '.ps1'
try:
# noinspection PyTypeChecker
TCompletionTarget = t.TypeVar('TCompletionTarget', bound='CompletionTarget')
except AttributeError:
TCompletionTarget = None # pylint: disable=invalid-name
try: def find_target_completion(target_func, prefix, short): # type: (t.Callable[[], t.Iterable[CompletionTarget]], str, bool) -> t.List[str]
# noinspection PyTypeChecker """Return a list of targets from the given target function which match the given prefix."""
TIntegrationTarget = t.TypeVar('TIntegrationTarget', bound='IntegrationTarget')
except AttributeError:
TIntegrationTarget = None # pylint: disable=invalid-name
def find_target_completion(target_func, prefix, short):
"""
:type target_func: () -> collections.Iterable[CompletionTarget]
:type prefix: unicode
:type short: bool
:rtype: list[str]
"""
try: try:
targets = target_func() targets = target_func()
matches = list(walk_completion_targets(targets, prefix, short)) matches = list(walk_completion_targets(targets, prefix, short))
@ -60,13 +43,8 @@ def find_target_completion(target_func, prefix, short):
return [u'%s' % ex] return [u'%s' % ex]
def walk_completion_targets(targets, prefix, short=False): def walk_completion_targets(targets, prefix, short=False): # type: (t.Iterable[CompletionTarget], str, bool) -> t.Tuple[str, ...]
""" """Return a tuple of targets from the given target iterable which match the given prefix."""
:type targets: collections.Iterable[CompletionTarget]
:type prefix: str
:type short: bool
:rtype: tuple[str]
"""
aliases = set(alias for target in targets for alias in target.aliases) aliases = set(alias for target in targets for alias in target.aliases)
if prefix.endswith('/') and prefix in aliases: if prefix.endswith('/') and prefix in aliases:
@ -85,14 +63,13 @@ def walk_completion_targets(targets, prefix, short=False):
return tuple(sorted(matches)) return tuple(sorted(matches))
def walk_internal_targets(targets, includes=None, excludes=None, requires=None): def walk_internal_targets(
""" targets, # type: t.Iterable[TCompletionTarget]
:type targets: collections.Iterable[T <= CompletionTarget] includes=None, # type: t.Optional[t.List[str]]
:type includes: list[str] excludes=None, # type: t.Optional[t.List[str]]
:type excludes: list[str] requires=None, # type: t.Optional[t.List[str]]
:type requires: list[str] ): # type: (...) -> t.Tuple[TCompletionTarget, ...]
:rtype: tuple[T <= CompletionTarget] """Return a tuple of matching completion targets."""
"""
targets = tuple(targets) targets = tuple(targets)
include_targets = sorted(filter_targets(targets, includes, directories=False), key=lambda include_target: include_target.name) include_targets = sorted(filter_targets(targets, includes, directories=False), key=lambda include_target: include_target.name)
@ -173,69 +150,49 @@ def walk_module_targets():
yield target yield target
def walk_units_targets(): def walk_units_targets(): # type: () -> t.Iterable[TestTarget]
""" """Return an iterable of units targets."""
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(path=data_context().content.unit_path, module_path=data_context().content.unit_module_path, extensions=('.py',), prefix='test_') return walk_test_targets(path=data_context().content.unit_path, module_path=data_context().content.unit_module_path, extensions=('.py',), prefix='test_')
def walk_compile_targets(include_symlinks=True): def walk_compile_targets(include_symlinks=True): # type: (bool) -> t.Iterable[TestTarget, ...]
""" """Return an iterable of compile targets."""
:type include_symlinks: bool
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(module_path=data_context().content.module_path, extensions=('.py',), extra_dirs=('bin',), include_symlinks=include_symlinks) return walk_test_targets(module_path=data_context().content.module_path, extensions=('.py',), extra_dirs=('bin',), include_symlinks=include_symlinks)
def walk_powershell_targets(include_symlinks=True): def walk_powershell_targets(include_symlinks=True): # type: (bool) -> t.Iterable[TestTarget]
""" """Return an iterable of PowerShell targets."""
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(module_path=data_context().content.module_path, extensions=('.ps1', '.psm1'), include_symlinks=include_symlinks) return walk_test_targets(module_path=data_context().content.module_path, extensions=('.ps1', '.psm1'), include_symlinks=include_symlinks)
def walk_sanity_targets(): def walk_sanity_targets(): # type: () -> t.Iterable[TestTarget]
""" """Return an iterable of sanity targets."""
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(module_path=data_context().content.module_path, include_symlinks=True, include_symlinked_directories=True) return walk_test_targets(module_path=data_context().content.module_path, include_symlinks=True, include_symlinked_directories=True)
def walk_posix_integration_targets(include_hidden=False): def walk_posix_integration_targets(include_hidden=False): # type: (bool) -> t.Iterable[IntegrationTarget]
""" """Return an iterable of POSIX integration targets."""
:type include_hidden: bool
:rtype: collections.Iterable[IntegrationTarget]
"""
for target in walk_integration_targets(): for target in walk_integration_targets():
if 'posix/' in target.aliases or (include_hidden and 'hidden/posix/' in target.aliases): if 'posix/' in target.aliases or (include_hidden and 'hidden/posix/' in target.aliases):
yield target yield target
def walk_network_integration_targets(include_hidden=False): def walk_network_integration_targets(include_hidden=False): # type: (bool) -> t.Iterable[IntegrationTarget]
""" """Return an iterable of network integration targets."""
:type include_hidden: bool
:rtype: collections.Iterable[IntegrationTarget]
"""
for target in walk_integration_targets(): for target in walk_integration_targets():
if 'network/' in target.aliases or (include_hidden and 'hidden/network/' in target.aliases): if 'network/' in target.aliases or (include_hidden and 'hidden/network/' in target.aliases):
yield target yield target
def walk_windows_integration_targets(include_hidden=False): def walk_windows_integration_targets(include_hidden=False): # type: (bool) -> t.Iterable[IntegrationTarget]
""" """Return an iterable of windows integration targets."""
:type include_hidden: bool
:rtype: collections.Iterable[IntegrationTarget]
"""
for target in walk_integration_targets(): for target in walk_integration_targets():
if 'windows/' in target.aliases or (include_hidden and 'hidden/windows/' in target.aliases): if 'windows/' in target.aliases or (include_hidden and 'hidden/windows/' in target.aliases):
yield target yield target
def walk_integration_targets(): def walk_integration_targets(): # type: () -> t.Iterable[IntegrationTarget]
""" """Return an iterable of integration targets."""
:rtype: collections.Iterable[IntegrationTarget]
"""
path = data_context().content.integration_targets_path path = data_context().content.integration_targets_path
modules = frozenset(target.module for target in walk_module_targets()) modules = frozenset(target.module for target in walk_module_targets())
paths = data_context().content.walk_files(path) paths = data_context().content.walk_files(path)
@ -305,17 +262,16 @@ def load_integration_prefixes():
return prefixes return prefixes
def walk_test_targets(path=None, module_path=None, extensions=None, prefix=None, extra_dirs=None, include_symlinks=False, include_symlinked_directories=False): def walk_test_targets(
""" path=None, # type: t.Optional[str]
:type path: str | None module_path=None, # type: t.Optional[str]
:type module_path: str | None extensions=None, # type: t.Optional[t.Tuple[str, ...]]
:type extensions: tuple[str] | None prefix=None, # type: t.Optional[str]
:type prefix: str | None extra_dirs=None, # type: t.Optional[t.Tuple[str, ...]]
:type extra_dirs: tuple[str] | None include_symlinks=False, # type: bool
:type include_symlinks: bool include_symlinked_directories=False, # type: bool
:type include_symlinked_directories: bool ): # type: (...) -> t.Iterable[TestTarget]
:rtype: collections.Iterable[TestTarget] """Iterate over available test targets."""
"""
if path: if path:
file_paths = data_context().content.walk_files(path, include_symlinked_directories=include_symlinked_directories) file_paths = data_context().content.walk_files(path, include_symlinked_directories=include_symlinked_directories)
else: else:
@ -486,11 +442,7 @@ class CompletionTarget(metaclass=abc.ABCMeta):
class DirectoryTarget(CompletionTarget): class DirectoryTarget(CompletionTarget):
"""Directory target.""" """Directory target."""
def __init__(self, path, modules): def __init__(self, path, modules): # type: (str, t.Tuple[str, ...]) -> None
"""
:type path: str
:type modules: tuple[str]
"""
super().__init__() super().__init__()
self.name = path self.name = path
@ -500,14 +452,14 @@ class DirectoryTarget(CompletionTarget):
class TestTarget(CompletionTarget): class TestTarget(CompletionTarget):
"""Generic test target.""" """Generic test target."""
def __init__(self, path, module_path, module_prefix, base_path, symlink=None): def __init__(
""" self,
:type path: str path, # type: str
:type module_path: str | None module_path, # type: t.Optional[str]
:type module_prefix: str | None module_prefix, # type: t.Optional[str]
:type base_path: str base_path, # type: str
:type symlink: bool | None symlink=None, # type: t.Optional[bool]
""" ):
super().__init__() super().__init__()
if symlink is None: if symlink is None:
@ -614,12 +566,7 @@ class IntegrationTarget(CompletionTarget):
'skip', 'skip',
))) )))
def __init__(self, path, modules, prefixes): def __init__(self, path, modules, prefixes): # type: (str, t.FrozenSet[str], t.Dict[str, str]) -> None
"""
:type path: str
:type modules: frozenset[str]
:type prefixes: dict[str, str]
"""
super().__init__() super().__init__()
self.relative_path = os.path.relpath(path, data_context().content.integration_targets_path) self.relative_path = os.path.relpath(path, data_context().content.integration_targets_path)
@ -763,10 +710,7 @@ class IntegrationTarget(CompletionTarget):
class TargetPatternsNotMatched(ApplicationError): class TargetPatternsNotMatched(ApplicationError):
"""One or more targets were not matched when a match was required.""" """One or more targets were not matched when a match was required."""
def __init__(self, patterns): def __init__(self, patterns): # type: (t.Set[str]) -> None
"""
:type patterns: set[str]
"""
self.patterns = sorted(patterns) self.patterns = sorted(patterns)
if len(patterns) > 1: if len(patterns) > 1:
@ -775,3 +719,7 @@ class TargetPatternsNotMatched(ApplicationError):
message = 'Target pattern not matched: %s' % self.patterns[0] message = 'Target pattern not matched: %s' % self.patterns[0]
super().__init__(message) super().__init__(message)
TCompletionTarget = t.TypeVar('TCompletionTarget', bound=CompletionTarget)
TIntegrationTarget = t.TypeVar('TIntegrationTarget', bound=IntegrationTarget)

@ -16,6 +16,10 @@ from .util_common import (
ResultType, ResultType,
) )
from .metadata import (
Metadata,
)
from .config import ( from .config import (
TestConfig, TestConfig,
) )
@ -23,12 +27,8 @@ from .config import (
from . import junit_xml from . import junit_xml
def calculate_best_confidence(choices, metadata): def calculate_best_confidence(choices, metadata): # type: (t.Tuple[t.Tuple[str, int], ...], Metadata) -> int
""" """Return the best confidence value available from the given choices and metadata."""
:type choices: tuple[tuple[str, int]]
:type metadata: Metadata
:rtype: int
"""
best_confidence = 0 best_confidence = 0
for path, line in choices: for path, line in choices:
@ -38,13 +38,8 @@ def calculate_best_confidence(choices, metadata):
return best_confidence return best_confidence
def calculate_confidence(path, line, metadata): def calculate_confidence(path, line, metadata): # type: (str, int, Metadata) -> int
""" """Return the confidence level for a test result associated with the given file path and line number."""
:type path: str
:type line: int
:type metadata: Metadata
:rtype: int
"""
ranges = metadata.changes.get(path) ranges = metadata.changes.get(path)
# no changes were made to the file # no changes were made to the file
@ -65,12 +60,7 @@ def calculate_confidence(path, line, metadata):
class TestResult: class TestResult:
"""Base class for test results.""" """Base class for test results."""
def __init__(self, command, test, python_version=None): def __init__(self, command, test, python_version=None): # type: (str, str, t.Optional[str]) -> None
"""
:type command: str
:type test: str
:type python_version: str
"""
self.command = command self.command = command
self.test = test self.test = test
self.python_version = python_version self.python_version = python_version
@ -90,27 +80,20 @@ class TestResult:
if args.junit: if args.junit:
self.write_junit(args) self.write_junit(args)
def write_console(self): def write_console(self): # type: () -> None
"""Write results to console.""" """Write results to console."""
def write_lint(self): def write_lint(self): # type: () -> None
"""Write lint results to stdout.""" """Write lint results to stdout."""
def write_bot(self, args): def write_bot(self, args): # type: (TestConfig) -> None
""" """Write results to a file for ansibullbot to consume."""
:type args: TestConfig
"""
def write_junit(self, args): def write_junit(self, args): # type: (TestConfig) -> None
""" """Write results to a junit XML file."""
:type args: TestConfig
"""
def create_result_name(self, extension): def create_result_name(self, extension): # type: (str) -> str
""" """Return the name of the result file using the given extension."""
:type extension: str
:rtype: str
"""
name = 'ansible-test-%s' % self.command name = 'ansible-test-%s' % self.command
if self.test: if self.test:
@ -145,18 +128,13 @@ class TestResult:
class TestTimeout(TestResult): class TestTimeout(TestResult):
"""Test timeout.""" """Test timeout."""
def __init__(self, timeout_duration): def __init__(self, timeout_duration): # type: (int) -> None
"""
:type timeout_duration: int
"""
super().__init__(command='timeout', test='') super().__init__(command='timeout', test='')
self.timeout_duration = timeout_duration self.timeout_duration = timeout_duration
def write(self, args): def write(self, args): # type: (TestConfig) -> None
""" """Write the test results to various locations."""
:type args: TestConfig
"""
message = 'Tests were aborted after exceeding the %d minute time limit.' % self.timeout_duration message = 'Tests were aborted after exceeding the %d minute time limit.' % self.timeout_duration
# Include a leading newline to improve readability on Shippable "Tests" tab. # Include a leading newline to improve readability on Shippable "Tests" tab.
@ -202,10 +180,8 @@ One or more of the following situations may be responsible:
class TestSuccess(TestResult): class TestSuccess(TestResult):
"""Test success.""" """Test success."""
def write_junit(self, args): def write_junit(self, args): # type: (TestConfig) -> None
""" """Write results to a junit XML file."""
:type args: TestConfig
"""
test_case = junit_xml.TestCase(classname=self.command, name=self.name) test_case = junit_xml.TestCase(classname=self.command, name=self.name)
self.save_junit(args, test_case) self.save_junit(args, test_case)
@ -213,27 +189,20 @@ class TestSuccess(TestResult):
class TestSkipped(TestResult): class TestSkipped(TestResult):
"""Test skipped.""" """Test skipped."""
def __init__(self, command, test, python_version=None): def __init__(self, command, test, python_version=None): # type: (str, str, t.Optional[str]) -> None
"""
:type command: str
:type test: str
:type python_version: str
"""
super().__init__(command, test, python_version) super().__init__(command, test, python_version)
self.reason = None # type: t.Optional[str] self.reason = None # type: t.Optional[str]
def write_console(self): def write_console(self): # type: () -> None
"""Write results to console.""" """Write results to console."""
if self.reason: if self.reason:
display.warning(self.reason) display.warning(self.reason)
else: else:
display.info('No tests applicable.', verbosity=1) display.info('No tests applicable.', verbosity=1)
def write_junit(self, args): def write_junit(self, args): # type: (TestConfig) -> None
""" """Write results to a junit XML file."""
:type args: TestConfig
"""
test_case = junit_xml.TestCase( test_case = junit_xml.TestCase(
classname=self.command, classname=self.command,
name=self.name, name=self.name,
@ -245,14 +214,14 @@ class TestSkipped(TestResult):
class TestFailure(TestResult): class TestFailure(TestResult):
"""Test failure.""" """Test failure."""
def __init__(self, command, test, python_version=None, messages=None, summary=None): def __init__(
""" self,
:type command: str command, # type: str
:type test: str test, # type: str
:type python_version: str | None python_version=None, # type: t.Optional[str]
:type messages: list[TestMessage] | None messages=None, # type: t.Optional[t.List[TestMessage]]
:type summary: unicode | None summary=None, # type: t.Optional[str]
""" ):
super().__init__(command, test, python_version) super().__init__(command, test, python_version)
if messages: if messages:
@ -263,16 +232,14 @@ class TestFailure(TestResult):
self.messages = messages self.messages = messages
self.summary = summary self.summary = summary
def write(self, args): def write(self, args): # type: (TestConfig) -> None
""" """Write the test results to various locations."""
:type args: TestConfig
"""
if args.metadata.changes: if args.metadata.changes:
self.populate_confidence(args.metadata) self.populate_confidence(args.metadata)
super().write(args) super().write(args)
def write_console(self): def write_console(self): # type: () -> None
"""Write results to console.""" """Write results to console."""
if self.summary: if self.summary:
display.error(self.summary) display.error(self.summary)
@ -291,7 +258,7 @@ class TestFailure(TestResult):
if doc_url: if doc_url:
display.info('See documentation for help: %s' % doc_url) display.info('See documentation for help: %s' % doc_url)
def write_lint(self): def write_lint(self): # type: () -> None
"""Write lint results to stdout.""" """Write lint results to stdout."""
if self.summary: if self.summary:
command = self.format_command() command = self.format_command()
@ -303,10 +270,8 @@ class TestFailure(TestResult):
for message in self.messages: for message in self.messages:
print(message) print(message)
def write_junit(self, args): def write_junit(self, args): # type: (TestConfig) -> None
""" """Write results to a junit XML file."""
:type args: TestConfig
"""
title = self.format_title() title = self.format_title()
output = self.format_block() output = self.format_block()
@ -323,10 +288,8 @@ class TestFailure(TestResult):
self.save_junit(args, test_case) self.save_junit(args, test_case)
def write_bot(self, args): def write_bot(self, args): # type: (TestConfig) -> None
""" """Write results to a file for ansibullbot to consume."""
:type args: TestConfig
"""
docs = self.find_docs() docs = self.find_docs()
message = self.format_title(help_link=docs) message = self.format_title(help_link=docs)
output = self.format_block() output = self.format_block()
@ -352,18 +315,14 @@ class TestFailure(TestResult):
write_json_test_results(ResultType.BOT, self.create_result_name('.json'), bot_data) write_json_test_results(ResultType.BOT, self.create_result_name('.json'), bot_data)
def populate_confidence(self, metadata): def populate_confidence(self, metadata): # type: (Metadata) -> None
""" """Populate test result confidence using the provided metadata."""
:type metadata: Metadata
"""
for message in self.messages: for message in self.messages:
if message.confidence is None: if message.confidence is None:
message.confidence = calculate_confidence(message.path, message.line, metadata) message.confidence = calculate_confidence(message.path, message.line, metadata)
def format_command(self): def format_command(self): # type: () -> str
""" """Return a string representing the CLI command associated with the test failure."""
:rtype: str
"""
command = 'ansible-test %s' % self.command command = 'ansible-test %s' % self.command
if self.test: if self.test:
@ -397,11 +356,8 @@ class TestFailure(TestResult):
return url return url
def format_title(self, help_link=None): def format_title(self, help_link=None): # type: (t.Optional[str]) -> str
""" """Return a string containing a title/heading for this test failure, including an optional help link to explain the test."""
:type help_link: str | None
:rtype: str
"""
command = self.format_command() command = self.format_command()
if self.summary: if self.summary:
@ -418,10 +374,8 @@ class TestFailure(TestResult):
return title return title
def format_block(self): def format_block(self): # type: () -> str
""" """Format the test summary or messages as a block of text and return the result."""
:rtype: str
"""
if self.summary: if self.summary:
block = self.summary block = self.summary
else: else:
@ -437,16 +391,16 @@ class TestFailure(TestResult):
class TestMessage: class TestMessage:
"""Single test message for one file.""" """Single test message for one file."""
def __init__(self, message, path, line=0, column=0, level='error', code=None, confidence=None): def __init__(
""" self,
:type message: str message, # type: str
:type path: str path, # type: str
:type line: int line=0, # type: int
:type column: int column=0, # type: int
:type level: str level='error', # type: str
:type code: str | None code=None, # type: t.Optional[str]
:type confidence: int | None confidence=None, # type: t.Optional[int]
""" ):
self.__path = path self.__path = path
self.__line = line self.__line = line
self.__column = column self.__column = column
@ -515,11 +469,8 @@ class TestMessage:
def __str__(self): def __str__(self):
return self.format() return self.format()
def format(self, show_confidence=False): def format(self, show_confidence=False): # type: (bool) -> str
""" """Return a string representation of this message, optionally including the confidence level."""
:type show_confidence: bool
:rtype: str
"""
if self.__code: if self.__code:
msg = '%s: %s' % (self.__code, self.__message) msg = '%s: %s' % (self.__code, self.__message)
else: else:

@ -246,22 +246,20 @@ def get_available_python_versions(): # type: () -> t.Dict[str, str]
return dict((version, path) for version, path in ((version, find_python(version, required=False)) for version in SUPPORTED_PYTHON_VERSIONS) if path) return dict((version, path) for version, path in ((version, find_python(version, required=False)) for version in SUPPORTED_PYTHON_VERSIONS) if path)
def raw_command(cmd, capture=False, env=None, data=None, cwd=None, explain=False, stdin=None, stdout=None, def raw_command(
cmd_verbosity=1, str_errors='strict', error_callback=None): cmd, # type: t.Iterable[str]
""" capture=False, # type: bool
:type cmd: collections.Iterable[str] env=None, # type: t.Optional[t.Dict[str, str]]
:type capture: bool data=None, # type: t.Optional[str]
:type env: dict[str, str] | None cwd=None, # type: t.Optional[str]
:type data: str | None explain=False, # type: bool
:type cwd: str | None stdin=None, # type: t.Optional[t.BinaryIO]
:type explain: bool stdout=None, # type: t.Optional[t.BinaryIO]
:type stdin: file | None cmd_verbosity=1, # type: int
:type stdout: file | None str_errors='strict', # type: str
:type cmd_verbosity: int error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]]
:type str_errors: str ): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
:type error_callback: t.Callable[[SubprocessError], None] """Run the specified command and return stdout and stderr as a tuple."""
:rtype: str | None, str | None
"""
if not cwd: if not cwd:
cwd = os.getcwd() cwd = os.getcwd()
@ -389,12 +387,8 @@ def common_environment():
return env return env
def pass_vars(required, optional): def pass_vars(required, optional): # type: (t.Collection[str], t.Collection[str]) -> t.Dict[str, str]
""" """Return a filtered dictionary of environment variables based on the current environment."""
:type required: collections.Iterable[str]
:type optional: collections.Iterable[str]
:rtype: dict[str, str]
"""
env = {} env = {}
for name in required: for name in required:
@ -572,13 +566,14 @@ class Display:
color = self.verbosity_colors.get(verbosity, self.yellow) color = self.verbosity_colors.get(verbosity, self.yellow)
self.print_message(message, color=color, fd=sys.stderr if self.info_stderr else sys.stdout, truncate=truncate) self.print_message(message, color=color, fd=sys.stderr if self.info_stderr else sys.stdout, truncate=truncate)
def print_message(self, message, color=None, fd=sys.stdout, truncate=False): # pylint: disable=locally-disabled, invalid-name def print_message( # pylint: disable=locally-disabled, invalid-name
""" self,
:type message: str message, # type: str
:type color: str | None color=None, # type: t.Optional[str]
:type fd: t.IO[str] fd=sys.stdout, # type: t.TextIO
:type truncate: bool truncate=False, # type: bool
""" ): # type: (...) -> None
"""Display a message."""
if self.redact and self.sensitive: if self.redact and self.sensitive:
for item in self.sensitive: for item in self.sensitive:
if not item: if not item:
@ -612,15 +607,15 @@ class ApplicationWarning(Exception):
class SubprocessError(ApplicationError): class SubprocessError(ApplicationError):
"""Error resulting from failed subprocess execution.""" """Error resulting from failed subprocess execution."""
def __init__(self, cmd, status=0, stdout=None, stderr=None, runtime=None, error_callback=None): def __init__(
""" self,
:type cmd: list[str] cmd, # type: t.List[str]
:type status: int status=0, # type: int
:type stdout: str | None stdout=None, # type: t.Optional[str]
:type stderr: str | None stderr=None, # type: t.Optional[str]
:type runtime: float | None runtime=None, # type: t.Optional[float]
:type error_callback: t.Optional[t.Callable[[SubprocessError], None]] error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]]
""" ): # type: (...) -> None
message = 'Command "%s" returned exit status %s.\n' % (' '.join(shlex.quote(c) for c in cmd), status) message = 'Command "%s" returned exit status %s.\n' % (' '.join(shlex.quote(c) for c in cmd), status)
if stderr: if stderr:

@ -33,6 +33,7 @@ from .util import (
ANSIBLE_TEST_TARGET_ROOT, ANSIBLE_TEST_TARGET_ROOT,
ANSIBLE_TEST_TOOLS_ROOT, ANSIBLE_TEST_TOOLS_ROOT,
ApplicationError, ApplicationError,
SubprocessError,
generate_name, generate_name,
) )
@ -402,23 +403,21 @@ def intercept_python(
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd, always=always) return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd, always=always)
def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None, def run_command(
cmd_verbosity=1, str_errors='strict', error_callback=None): args, # type: CommonConfig
""" cmd, # type: t.Iterable[str]
:type args: CommonConfig capture=False, # type: bool
:type cmd: collections.Iterable[str] env=None, # type: t.Optional[t.Dict[str, str]]
:type capture: bool data=None, # type: t.Optional[str]
:type env: dict[str, str] | None cwd=None, # type: t.Optional[str]
:type data: str | None always=False, # type: bool
:type cwd: str | None stdin=None, # type: t.Optional[t.BinaryIO]
:type always: bool stdout=None, # type: t.Optional[t.BinaryIO]
:type stdin: file | None cmd_verbosity=1, # type: int
:type stdout: file | None str_errors='strict', # type: str
:type cmd_verbosity: int error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]]
:type str_errors: str ): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
:type error_callback: t.Callable[[SubprocessError], None] """Run the specified command and return stdout and stderr as a tuple."""
:rtype: str | None, str | None
"""
explain = args.explain and not always explain = args.explain and not always
return raw_command(cmd, capture=capture, env=env, data=data, cwd=cwd, explain=explain, stdin=stdin, stdout=stdout, return raw_command(cmd, capture=capture, env=env, data=data, cwd=cwd, explain=explain, stdin=stdin, stdout=stdout,
cmd_verbosity=cmd_verbosity, str_errors=str_errors, error_callback=error_callback) cmd_verbosity=cmd_verbosity, str_errors=str_errors, error_callback=error_callback)

@ -4,6 +4,7 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import os import os
import typing as t
import astroid import astroid
@ -16,24 +17,20 @@ ANSIBLE_TEST_MODULE_UTILS_PATH = os.environ['ANSIBLE_TEST_MODULE_UTILS_PATH']
class UnwantedEntry: class UnwantedEntry:
"""Defines an unwanted import.""" """Defines an unwanted import."""
def __init__(self, alternative, modules_only=False, names=None, ignore_paths=None): def __init__(
""" self,
:type alternative: str alternative, # type: str
:type modules_only: bool modules_only=False, # type: bool
:type names: tuple[str] | None names=None, # type: t.Optional[t.Tuple[str, ...]]
:type ignore_paths: tuple[str] | None ignore_paths=None, # type: t.Optional[t.Tuple[str, ...]]
""" ): # type: (...) -> None
self.alternative = alternative self.alternative = alternative
self.modules_only = modules_only self.modules_only = modules_only
self.names = set(names) if names else set() self.names = set(names) if names else set()
self.ignore_paths = ignore_paths self.ignore_paths = ignore_paths
def applies_to(self, path, name=None): def applies_to(self, path, name=None): # type: (str, t.Optional[str]) -> bool
""" """Return True if this entry applies to the given path, otherwise return False."""
:type path: str
:type name: str | None
:rtype: bool
"""
if self.names: if self.names:
if not name: if not name:
return False return False
@ -50,11 +47,8 @@ class UnwantedEntry:
return True return True
def is_module_path(path): def is_module_path(path): # type: (str) -> bool
""" """Return True if the given path is a module or module_utils path, otherwise return False."""
:type path: str
:rtype: bool
"""
return path.startswith(ANSIBLE_TEST_MODULES_PATH) or path.startswith(ANSIBLE_TEST_MODULE_UTILS_PATH) return path.startswith(ANSIBLE_TEST_MODULES_PATH) or path.startswith(ANSIBLE_TEST_MODULE_UTILS_PATH)
@ -136,23 +130,17 @@ class AnsibleUnwantedChecker(BaseChecker):
modules_only=True), modules_only=True),
} }
def visit_import(self, node): def visit_import(self, node): # type: (astroid.node_classes.Import) -> None
""" """Visit an import node."""
:type node: astroid.node_classes.Import
"""
for name in node.names: for name in node.names:
self._check_import(node, name[0]) self._check_import(node, name[0])
def visit_importfrom(self, node): def visit_importfrom(self, node): # type: (astroid.node_classes.ImportFrom) -> None
""" """Visit an import from node."""
:type node: astroid.node_classes.ImportFrom
"""
self._check_importfrom(node, node.modname, node.names) self._check_importfrom(node, node.modname, node.names)
def visit_attribute(self, node): def visit_attribute(self, node): # type: (astroid.node_classes.Attribute) -> None
""" """Visit an attribute node."""
:type node: astroid.node_classes.Attribute
"""
last_child = node.last_child() last_child = node.last_child()
# this is faster than using type inference and will catch the most common cases # this is faster than using type inference and will catch the most common cases
@ -167,10 +155,8 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file, node.attrname): if entry.applies_to(self.linter.current_file, node.attrname):
self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node) self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node)
def visit_call(self, node): def visit_call(self, node): # type: (astroid.node_classes.Call) -> None
""" """Visit a call node."""
:type node: astroid.node_classes.Call
"""
try: try:
for i in node.func.inferred(): for i in node.func.inferred():
func = None func = None
@ -188,11 +174,8 @@ class AnsibleUnwantedChecker(BaseChecker):
except astroid.exceptions.InferenceError: except astroid.exceptions.InferenceError:
pass pass
def _check_import(self, node, modname): def _check_import(self, node, modname): # type: (astroid.node_classes.Import, str) -> None
""" """Check the imports on the specified import node."""
:type node: astroid.node_classes.Import
:type modname: str
"""
self._check_module_import(node, modname) self._check_module_import(node, modname)
entry = self.unwanted_imports.get(modname) entry = self.unwanted_imports.get(modname)
@ -203,12 +186,8 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file): if entry.applies_to(self.linter.current_file):
self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node) self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node)
def _check_importfrom(self, node, modname, names): def _check_importfrom(self, node, modname, names): # type: (astroid.node_classes.ImportFrom, str, t.List[str]) -> None
""" """Check the imports on the specified import from node."""
:type node: astroid.node_classes.ImportFrom
:type modname: str
:type names: list[str[
"""
self._check_module_import(node, modname) self._check_module_import(node, modname)
entry = self.unwanted_imports.get(modname) entry = self.unwanted_imports.get(modname)
@ -220,11 +199,8 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file, name[0]): if entry.applies_to(self.linter.current_file, name[0]):
self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node) self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node)
def _check_module_import(self, node, modname): def _check_module_import(self, node, modname): # type: (t.Union[astroid.node_classes.Import, astroid.node_classes.ImportFrom], str) -> None
""" """Check the module import on the given import or import from node."""
:type node: astroid.node_classes.Import | astroid.node_classes.ImportFrom
:type modname: str
"""
if not is_module_path(self.linter.current_file): if not is_module_path(self.linter.current_file):
return return

@ -7,6 +7,7 @@ import json
import os import os
import re import re
import sys import sys
import typing as t
import yaml import yaml
from yaml.resolver import Resolver from yaml.resolver import Resolver
@ -79,10 +80,8 @@ class YamlChecker:
print(json.dumps(report, indent=4, sort_keys=True)) print(json.dumps(report, indent=4, sort_keys=True))
def check(self, paths): def check(self, paths): # type: (t.List[str]) -> None
""" """Check the specified paths."""
:type paths: t.List[str]
"""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config') config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config')
yaml_conf = YamlLintConfig(file=os.path.join(config_path, 'default.yml')) yaml_conf = YamlLintConfig(file=os.path.join(config_path, 'default.yml'))
@ -107,21 +106,13 @@ class YamlChecker:
else: else:
raise Exception('unsupported extension: %s' % extension) raise Exception('unsupported extension: %s' % extension)
def check_yaml(self, conf, path, contents): def check_yaml(self, conf, path, contents): # type: (YamlLintConfig, str, str) -> None
""" """Check the given YAML."""
:type conf: YamlLintConfig
:type path: str
:type contents: str
"""
self.check_parsable(path, contents) self.check_parsable(path, contents)
self.messages += [self.result_to_message(r, path) for r in linter.run(contents, conf, path)] self.messages += [self.result_to_message(r, path) for r in linter.run(contents, conf, path)]
def check_module(self, conf, path, contents): def check_module(self, conf, path, contents): # type: (YamlLintConfig, str, str) -> None
""" """Check the given module."""
:type conf: YamlLintConfig
:type path: str
:type contents: str
"""
docs = self.get_module_docs(path, contents) docs = self.get_module_docs(path, contents)
for key, value in docs.items(): for key, value in docs.items():
@ -142,12 +133,8 @@ class YamlChecker:
self.messages += [self.result_to_message(r, path, lineno - 1, key) for r in messages] self.messages += [self.result_to_message(r, path, lineno - 1, key) for r in messages]
def check_parsable(self, path, contents, lineno=1): def check_parsable(self, path, contents, lineno=1): # type: (str, str, int) -> None
""" """Check the given contents to verify they can be parsed as YAML."""
:type path: str
:type contents: str
:type lineno: int
"""
try: try:
yaml.load(contents, Loader=TestLoader) yaml.load(contents, Loader=TestLoader)
except MarkedYAMLError as ex: except MarkedYAMLError as ex:
@ -160,14 +147,8 @@ class YamlChecker:
}] }]
@staticmethod @staticmethod
def result_to_message(result, path, line_offset=0, prefix=''): def result_to_message(result, path, line_offset=0, prefix=''): # type: (t.Any, str, int, str) -> t.Dict[str, t.Any]
""" """Convert the given result to a dictionary and return it."""
:type result: any
:type path: str
:type line_offset: int
:type prefix: str
:rtype: dict[str, any]
"""
if prefix: if prefix:
prefix = '%s: ' % prefix prefix = '%s: ' % prefix
@ -180,12 +161,8 @@ class YamlChecker:
level=result.level, level=result.level,
) )
def get_module_docs(self, path, contents): def get_module_docs(self, path, contents): # type: (str, str) -> t.Dict[str, t.Any]
""" """Return the module documentation for the given module contents."""
:type path: str
:type contents: str
:rtype: dict[str, any]
"""
module_doc_types = [ module_doc_types = [
'DOCUMENTATION', 'DOCUMENTATION',
'EXAMPLES', 'EXAMPLES',
@ -240,12 +217,8 @@ class YamlChecker:
return docs return docs
def parse_module(self, path, contents): def parse_module(self, path, contents): # type: (str, str) -> t.Optional[ast.Module]
""" """Parse the given contents and return a module if successful, otherwise return None."""
:type path: str
:type contents: str
:rtype: ast.Module | None
"""
try: try:
return ast.parse(contents) return ast.parse(contents)
except SyntaxError as ex: except SyntaxError as ex:

Loading…
Cancel
Save