diff --git a/mypy/build.py b/mypy/build.py index 33a040408ac1..e51ce2169e75 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -748,7 +748,7 @@ def report_file(self, type_map: Dict[Expression, Type], options: Options) -> None: if self.reports is not None and self.source_set.is_source(file): - self.reports.file(file, type_map, options) + self.reports.file(file, self.modules, type_map, options) def verbosity(self) -> int: return self.options.verbosity @@ -2120,7 +2120,7 @@ def semantic_analysis_pass_three(self) -> None: self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath, self.options, patches) if self.options.dump_type_stats: - dump_type_stats(self.tree, self.xpath) + dump_type_stats(self.tree, self.xpath, self.manager.modules) self.patches = patches + self.patches def semantic_analysis_apply_patches(self) -> None: @@ -2165,7 +2165,10 @@ def finish_passes(self) -> None: self._patch_indirect_dependencies(self.type_checker().module_refs, self.type_map()) if self.options.dump_inference_stats: - dump_type_stats(self.tree, self.xpath, inferred=True, + dump_type_stats(self.tree, + self.xpath, + modules=self.manager.modules, + inferred=True, typemap=self.type_map()) manager.report_file(self.tree, self.type_map(), self.options) diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index f5c128ff1a56..2e1b6fd189aa 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -143,6 +143,9 @@ def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier], rep_types = rhs_type.items elif isinstance(rhs_type, AnyType): return + elif isinstance(rhs_type, Instance) and rhs_type.type.fullname() == 'builtins.tuple': + # Assume that an arbitrary-length tuple has the right number of items. + rep_types = [rhs_type.args[0]] * len(checkers) else: rep_types = [rhs_type] diff --git a/mypy/defaults.py b/mypy/defaults.py index b1519113d154..9b21c230123e 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -26,4 +26,5 @@ 'xslt-html', 'xslt-txt', 'html', - 'txt'] # type: Final + 'txt', + 'lineprecision'] # type: Final diff --git a/mypy/report.py b/mypy/report.py index 09b4299a79c3..da6552530a83 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -13,7 +13,7 @@ from urllib.request import pathname2url import typing -from typing import Any, Callable, Dict, List, Optional, Tuple, cast +from typing import Any, Callable, Dict, List, Optional, Tuple, cast, Iterator from typing_extensions import Final from mypy.nodes import MypyFile, Expression, FuncDef @@ -74,9 +74,13 @@ def add_report(self, report_type: str, report_dir: str) -> 'AbstractReporter': self.named_reporters[report_type] = reporter return reporter - def file(self, tree: MypyFile, type_map: Dict[Expression, Type], options: Options) -> None: + def file(self, + tree: MypyFile, + modules: Dict[str, MypyFile], + type_map: Dict[Expression, Type], + options: Options) -> None: for reporter in self.reporters: - reporter.on_file(tree, type_map, options) + reporter.on_file(tree, modules, type_map, options) def finish(self) -> None: for reporter in self.reporters: @@ -90,7 +94,11 @@ def __init__(self, reports: Reports, output_dir: str) -> None: stats.ensure_dir_exists(output_dir) @abstractmethod - def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], options: Options) -> None: + def on_file(self, + tree: MypyFile, + modules: Dict[str, MypyFile], + type_map: Dict[Expression, Type], + options: Options) -> None: pass @abstractmethod @@ -108,6 +116,23 @@ def alias_reporter(source_reporter: str, target_reporter: str) -> None: reporter_classes[target_reporter] = reporter_classes[source_reporter] +def should_skip_path(path: str) -> bool: + if stats.is_special_module(path): + return True + if path.startswith('..'): + return True + if 'stubs' in path.split('/') or 'stubs' in path.split(os.sep): + return True + return False + + +def iterate_python_lines(path: str) -> Iterator[Tuple[int, str]]: + """Return an iterator over (line number, line text) from a Python file.""" + with tokenize.open(path) as input_file: + for line_info in enumerate(input_file, 1): + yield line_info + + class FuncCounterVisitor(TraverserVisitor): def __init__(self) -> None: super().__init__() @@ -124,6 +149,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: # Count physical lines. This assumes the file's encoding is a @@ -162,6 +188,8 @@ def on_finish(self) -> None: class AnyExpressionsReporter(AbstractReporter): + """Report frequencies of different kinds of Any types.""" + def __init__(self, reports: Reports, output_dir: str) -> None: super().__init__(reports, output_dir) self.counts = {} # type: Dict[str, Tuple[int, int]] @@ -169,10 +197,14 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: - visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(), - typemap=type_map, all_nodes=True, + visitor = stats.StatisticsVisitor(inferred=True, + filename=tree.fullname(), + modules=modules, + typemap=type_map, + all_nodes=True, visit_untyped_defs=False) tree.accept(visitor) self.any_types_counter[tree.fullname()] = visitor.type_of_any_counter @@ -354,12 +386,14 @@ class LineCoverageReporter(AbstractReporter): source file's absolute pathname the list of line numbers that belong to typed functions in that file. """ + def __init__(self, reports: Reports, output_dir: str) -> None: super().__init__(reports, output_dir) self.lines_covered = {} # type: Dict[str, List[int]] def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: with open(tree.path) as f: @@ -421,34 +455,33 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: self.last_xml = None path = os.path.relpath(tree.path) - if stats.is_special_module(path): - return - if path.startswith('..'): - return - if 'stubs' in path.split('/'): + if should_skip_path(path): return - visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(), - typemap=type_map, all_nodes=True) + visitor = stats.StatisticsVisitor(inferred=True, + filename=tree.fullname(), + modules=modules, + typemap=type_map, + all_nodes=True) tree.accept(visitor) root = etree.Element('mypy-report-file', name=path, module=tree._fullname) doc = etree.ElementTree(root) file_info = FileInfo(path, tree._fullname) - with tokenize.open(path) as input_file: - for lineno, line_text in enumerate(input_file, 1): - status = visitor.line_map.get(lineno, stats.TYPE_EMPTY) - file_info.counts[status] += 1 - etree.SubElement(root, 'line', - number=str(lineno), - precision=stats.precision_names[status], - content=line_text.rstrip('\n').translate(self.control_fixer), - any_info=self._get_any_info_for_line(visitor, lineno)) + for lineno, line_text in iterate_python_lines(path): + status = visitor.line_map.get(lineno, stats.TYPE_EMPTY) + file_info.counts[status] += 1 + etree.SubElement(root, 'line', + number=str(lineno), + precision=stats.precision_names[status], + content=line_text.rstrip('\n').translate(self.control_fixer), + any_info=self._get_any_info_for_line(visitor, lineno)) # Assumes a layout similar to what XmlReporter uses. xslt_path = os.path.relpath('mypy-html.xslt', path) transform_pi = etree.ProcessingInstruction('xml-stylesheet', @@ -506,8 +539,8 @@ def get_line_rate(covered_lines: int, total_lines: int) -> str: class CoberturaPackage(object): - """Container for XML and statistics mapping python modules to Cobertura package - """ + """Container for XML and statistics mapping python modules to Cobertura package.""" + def __init__(self, name: str) -> None: self.name = name self.classes = {} # type: Dict[str, Any] @@ -535,8 +568,7 @@ def add_packages(self, parent_element: Any) -> None: class CoberturaXmlReporter(AbstractReporter): - """Reporter for generating Cobertura compliant XML. - """ + """Reporter for generating Cobertura compliant XML.""" def __init__(self, reports: Reports, output_dir: str) -> None: super().__init__(reports, output_dir) @@ -549,11 +581,15 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: path = os.path.relpath(tree.path) - visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(), - typemap=type_map, all_nodes=True) + visitor = stats.StatisticsVisitor(inferred=True, + filename=tree.fullname(), + modules=modules, + typemap=type_map, + all_nodes=True) tree.accept(visitor) class_name = os.path.basename(path) @@ -646,6 +682,7 @@ class XmlReporter(AbstractXmlReporter): def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: last_xml = self.memory_xml.last_xml @@ -688,6 +725,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: last_xml = self.memory_xml.last_xml @@ -730,6 +768,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, + modules: Dict[str, MypyFile], type_map: Dict[Expression, Type], options: Options) -> None: pass @@ -749,6 +788,75 @@ def on_finish(self) -> None: alias_reporter('xslt-html', 'html') alias_reporter('xslt-txt', 'txt') + +class LinePrecisionReporter(AbstractReporter): + """Report per-module line counts for typing precision. + + Each line is classified into one of these categories: + + * precise (fully type checked) + * imprecise (Any types in a type component, such as List[Any]) + * any (something with an Any type, implicit or explicit) + * empty (empty line, comment or docstring) + * unanalyzed (mypy considers line unreachable) + + The meaning of these categories varies slightly depending on + context. + """ + + def __init__(self, reports: Reports, output_dir: str) -> None: + super().__init__(reports, output_dir) + self.files = [] # type: List[FileInfo] + + def on_file(self, + tree: MypyFile, + modules: Dict[str, MypyFile], + type_map: Dict[Expression, Type], + options: Options) -> None: + path = os.path.relpath(tree.path) + if should_skip_path(path): + return + + visitor = stats.StatisticsVisitor(inferred=True, + filename=tree.fullname(), + modules=modules, + typemap=type_map, + all_nodes=True) + tree.accept(visitor) + + file_info = FileInfo(path, tree._fullname) + for lineno, _ in iterate_python_lines(path): + status = visitor.line_map.get(lineno, stats.TYPE_EMPTY) + file_info.counts[status] += 1 + + self.files.append(file_info) + + def on_finish(self) -> None: + output_files = sorted(self.files, key=lambda x: x.module) + report_file = os.path.join(self.output_dir, 'lineprecision.txt') + width = max(4, max(len(info.module) for info in output_files)) + titles = ('Lines', 'Precise', 'Imprecise', 'Any', 'Empty', 'Unanalyzed') + widths = (width,) + tuple(len(t) for t in titles) + # TODO: Need mypyc mypy pin move + fmt = '{:%d} {:%d} {:%d} {:%d} {:%d} {:%d} {:%d}\n' % widths # type: ignore + with open(report_file, 'w') as f: + f.write( + fmt.format('Name', *titles)) + f.write('-' * (width + 51) + '\n') + for file_info in output_files: + counts = file_info.counts + f.write(fmt.format(file_info.module.ljust(width), + file_info.total(), + counts[stats.TYPE_PRECISE], + counts[stats.TYPE_IMPRECISE], + counts[stats.TYPE_ANY], + counts[stats.TYPE_EMPTY], + counts[stats.TYPE_UNANALYZED])) + + +register_reporter('lineprecision', LinePrecisionReporter) + + # Reporter class names are defined twice to speed up mypy startup, as this # module is slow to import. Ensure that the two definitions match. assert set(reporter_classes) == set(REPORTER_NAMES) diff --git a/mypy/stats.py b/mypy/stats.py index bc8d066cc2cd..cd747770b8d4 100644 --- a/mypy/stats.py +++ b/mypy/stats.py @@ -4,7 +4,7 @@ from collections import Counter import typing -from typing import Dict, List, cast, Optional +from typing import Dict, List, cast, Optional, Union from typing_extensions import Final from mypy.traverser import TraverserVisitor @@ -16,8 +16,10 @@ from mypy import nodes from mypy.nodes import ( Expression, FuncDef, TypeApplication, AssignmentStmt, NameExpr, CallExpr, MypyFile, - MemberExpr, OpExpr, ComparisonExpr, IndexExpr, UnaryExpr, YieldFromExpr, RefExpr, ClassDef + MemberExpr, OpExpr, ComparisonExpr, IndexExpr, UnaryExpr, YieldFromExpr, RefExpr, ClassDef, + ImportFrom, Import, ImportAll ) +from mypy.util import correct_relative_import TYPE_EMPTY = 0 # type: Final TYPE_UNANALYZED = 1 # type: Final # type of non-typechecked code @@ -38,11 +40,13 @@ class StatisticsVisitor(TraverserVisitor): def __init__(self, inferred: bool, filename: str, + modules: Dict[str, MypyFile], typemap: Optional[Dict[Expression, Type]] = None, all_nodes: bool = False, visit_untyped_defs: bool = True) -> None: self.inferred = inferred self.filename = filename + self.modules = modules self.typemap = typemap self.all_nodes = all_nodes self.visit_untyped_defs = visit_untyped_defs @@ -70,6 +74,35 @@ def __init__(self, TraverserVisitor.__init__(self) + def visit_mypy_file(self, o: MypyFile) -> None: + self.cur_mod_node = o + self.cur_mod_id = o.fullname() + super().visit_mypy_file(o) + + def visit_import_from(self, imp: ImportFrom) -> None: + self.process_import(imp) + + def visit_import_all(self, imp: ImportAll) -> None: + self.process_import(imp) + + def process_import(self, imp: Union[ImportFrom, ImportAll]) -> None: + import_id, ok = correct_relative_import(self.cur_mod_id, + imp.relative, + imp.id, + self.cur_mod_node.is_package_init_file()) + if ok and import_id in self.modules: + kind = TYPE_PRECISE + else: + kind = TYPE_ANY + self.record_line(imp.line, kind) + + def visit_import(self, imp: Import) -> None: + if all(id in self.modules for id, _ in imp.ids): + kind = TYPE_PRECISE + else: + kind = TYPE_ANY + self.record_line(imp.line, kind) + def visit_func_def(self, o: FuncDef) -> None: self.line = o.line if len(o.expanded) > 1 and o.expanded != [o] * len(o.expanded): @@ -235,12 +268,18 @@ def record_line(self, line: int, precision: int) -> None: self.line_map.get(line, TYPE_EMPTY)) -def dump_type_stats(tree: MypyFile, path: str, inferred: bool = False, +def dump_type_stats(tree: MypyFile, + path: str, + modules: Dict[str, MypyFile], + inferred: bool = False, typemap: Optional[Dict[Expression, Type]] = None) -> None: if is_special_module(path): return print(path) - visitor = StatisticsVisitor(inferred, filename=tree.fullname(), typemap=typemap) + visitor = StatisticsVisitor(inferred, + filename=tree.fullname(), + modules=modules, + typemap=typemap) tree.accept(visitor) for line in visitor.output: print(line) diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 5f1c06da327a..dc0e4beea84b 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -18,7 +18,9 @@ from mypy.main import process_options from mypy.options import Options -from mypy.test.data import DataDrivenTestCase +from mypy.test.data import DataDrivenTestCase, fix_cobertura_filename +from mypy.test.config import test_temp_dir +import mypy.version skip = pytest.mark.skip @@ -423,3 +425,43 @@ def copy_and_fudge_mtime(source_path: str, target_path: str) -> None: if new_time: os.utime(target_path, times=(new_time, new_time)) + + +def check_test_output_files(testcase: DataDrivenTestCase, + step: int, + strip_prefix: str = '') -> None: + for path, expected_content in testcase.output_files: + if path.startswith(strip_prefix): + path = path[len(strip_prefix):] + if not os.path.exists(path): + raise AssertionError( + 'Expected file {} was not produced by test case{}'.format( + path, ' on step %d' % step if testcase.output2 else '')) + with open(path, 'r', encoding='utf8') as output_file: + actual_output_content = output_file.read().splitlines() + normalized_output = normalize_file_output(actual_output_content, + os.path.abspath(test_temp_dir)) + # We always normalize things like timestamp, but only handle operating-system + # specific things if requested. + if testcase.normalize_output: + if testcase.suite.native_sep and os.path.sep == '\\': + normalized_output = [fix_cobertura_filename(line) + for line in normalized_output] + normalized_output = normalize_error_messages(normalized_output) + assert_string_arrays_equal(expected_content.splitlines(), normalized_output, + 'Output file {} did not match its expected output{}'.format( + path, ' on step %d' % step if testcase.output2 else '')) + + +def normalize_file_output(content: List[str], current_abs_path: str) -> List[str]: + """Normalize file output for comparison.""" + timestamp_regex = re.compile(r'\d{10}') + result = [x.replace(current_abs_path, '$PWD') for x in content] + version = mypy.version.__version__ + result = [re.sub(r'\b' + re.escape(version) + r'\b', '$VERSION', x) for x in result] + # We generate a new mypy.version when building mypy wheels that + # lacks base_version, so handle that case. + base_version = getattr(mypy.version, 'base_version', version) + result = [re.sub(r'\b' + re.escape(base_version) + r'\b', '$VERSION', x) for x in result] + result = [timestamp_regex.sub('$TIMESTAMP', x) for x in result] + return result diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index abb482fa3af9..88f4ed7899e6 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -16,7 +16,7 @@ from mypy.test.helpers import ( assert_string_arrays_equal, normalize_error_messages, assert_module_equivalence, retry_on_error, update_testcase_output, parse_options, - copy_and_fudge_mtime, assert_target_equivalence + copy_and_fudge_mtime, assert_target_equivalence, check_test_output_files ) from mypy.errors import CompileError from mypy.newsemanal.semanal_main import core_modules @@ -85,6 +85,7 @@ 'check-literal.test', 'check-newsemanal.test', 'check-inline-config.test', + 'check-reports.test', ] # Tests that use Python 3.8-only AST features (like expression-scoped ignores): @@ -106,7 +107,6 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if incremental: # Incremental tests are run once with a cold cache, once with a warm cache. # Expect success on first run, errors from testcase.output (if any) on second run. - # We briefly sleep to make sure file timestamps are distinct. num_steps = max([2] + list(testcase.output2.keys())) # Check that there are no file changes beyond the last run (they would be ignored). for dn, dirs, files in os.walk(os.curdir): @@ -248,6 +248,9 @@ def run_case_once(self, testcase: DataDrivenTestCase, 'stale' + suffix, expected_stale, res.manager.stale_modules) + if testcase.output_files: + check_test_output_files(testcase, incremental_step, strip_prefix='tmp/') + def verify_cache(self, module_data: List[Tuple[str, str, str]], a: List[str], manager: build.BuildManager, graph: Graph) -> None: # There should be valid cache metadata for each module except diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index b256966b9f33..34981ee6cba6 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -12,10 +12,10 @@ from typing import List from mypy.test.config import test_temp_dir, PREFIX -from mypy.test.data import fix_cobertura_filename from mypy.test.data import DataDrivenTestCase, DataSuite -from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages -import mypy.version +from mypy.test.helpers import ( + assert_string_arrays_equal, normalize_error_messages, check_test_output_files +) # Path to Python 3 interpreter python3_path = sys.executable @@ -79,25 +79,7 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None: 'Expected zero status and empty stderr%s, got %d and\n%s' % (' on step %d' % step if testcase.output2 else '', result, '\n'.join(err + out))) - for path, expected_content in testcase.output_files: - if not os.path.exists(path): - raise AssertionError( - 'Expected file {} was not produced by test case{}'.format( - path, ' on step %d' % step if testcase.output2 else '')) - with open(path, 'r', encoding='utf8') as output_file: - actual_output_content = output_file.read().splitlines() - normalized_output = normalize_file_output(actual_output_content, - os.path.abspath(test_temp_dir)) - # We always normalize things like timestamp, but only handle operating-system - # specific things if requested. - if testcase.normalize_output: - if testcase.suite.native_sep and os.path.sep == '\\': - normalized_output = [fix_cobertura_filename(line) - for line in normalized_output] - normalized_output = normalize_error_messages(normalized_output) - assert_string_arrays_equal(expected_content.splitlines(), normalized_output, - 'Output file {} did not match its expected output{}'.format( - path, ' on step %d' % step if testcase.output2 else '')) + check_test_output_files(testcase, step) else: if testcase.normalize_output: out = normalize_error_messages(err + out) @@ -126,17 +108,3 @@ def parse_args(line: str) -> List[str]: if not m: return [] # No args; mypy will spit out an error. return m.group(1).split() - - -def normalize_file_output(content: List[str], current_abs_path: str) -> List[str]: - """Normalize file output for comparison.""" - timestamp_regex = re.compile(r'\d{10}') - result = [x.replace(current_abs_path, '$PWD') for x in content] - version = mypy.version.__version__ - result = [re.sub(r'\b' + re.escape(version) + r'\b', '$VERSION', x) for x in result] - # We generate a new mypy.version when building mypy wheels that - # lacks base_version, so handle that case. - base_version = getattr(mypy.version, 'base_version', version) - result = [re.sub(r'\b' + re.escape(base_version) + r'\b', '$VERSION', x) for x in result] - result = [timestamp_regex.sub('$TIMESTAMP', x) for x in result] - return result diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 1f9b61b963e7..f6dfa6ddb5c1 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1290,6 +1290,14 @@ b'%c' % (123) [case testUnicodeInterpolation_python2] u'%s' % (u'abc',) +[case testStringInterpolationVariableLengthTuple] +from typing import Tuple +def f(t: Tuple[int, ...]) -> None: + '%d %d' % t + '%d %d %d' % t +[builtins fixtures/primitives.pyi] + + -- Lambdas -- ------- diff --git a/test-data/unit/check-reports.test b/test-data/unit/check-reports.test new file mode 100644 index 000000000000..e41e2278abb4 --- /dev/null +++ b/test-data/unit/check-reports.test @@ -0,0 +1,125 @@ +[case testReportBasic] +# flags: --xml-report out +def f(): pass + +def g() -> None: pass +[outfile out/index.xml] + + +[case testLinePrecisionBasic] +# flags: --lineprecision-report out +def f(): pass + +def g() -> None: + a = 1 +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 5 2 0 1 2 0 + +[case testLinePrecisionImpreciseType] +# flags: --lineprecision-report out +def f(x: list) -> None: pass +[builtins fixtures/list.pyi] +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 2 0 1 0 1 0 + +[case testLinePrecisionUnanalyzed] +# flags: --lineprecision-report out +import sys +MYPY = False +if not MYPY: + a = 1 + +def f(x: int) -> None: + if isinstance(x, str): + b = 1 + c = 1 +[builtins fixtures/isinstance.pyi] +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 10 5 0 0 2 3 + +[case testLinePrecisionEmptyLines] +# flags: --lineprecision-report out +def f() -> None: + """docstring + + long + """ + x = 0 + + # comment + y = 0 # comment (non-empty) +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 10 3 0 0 7 0 + +[case testLinePrecisionImportFrom] +# flags: --lineprecision-report out --ignore-missing-imports +from m import f +from m import g +from bad import foo +from bad import ( # treated as a single line + foo2, + foo3, +) + +[file m.py] +def f(): pass +def g() -> None: pass + +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 8 2 0 2 4 0 +m 2 1 0 1 0 0 + +[case testLinePrecisionImport] +# flags: --lineprecision-report out --ignore-missing-imports +import m +import bad +import m, bad + +[file m.py] +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 4 1 0 2 1 0 +m 0 0 0 0 0 0 + +[case testLinePrecisionStarImport] +# flags: --lineprecision-report out --ignore-missing-imports +from m import * +from bad import * + +[file m.py] +def f(): pass +def g() -> None: pass +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 3 1 0 1 1 0 +m 2 1 0 1 0 0 + +[case testLinePrecisionRelativeImport] +# flags: --lineprecision-report out --ignore-missing-imports +import a + +[file a/__init__.py] +from .m import f +from .bad import g + +[file a/m.py] +def f(): pass + +[outfile out/lineprecision.txt] +Name Lines Precise Imprecise Any Empty Unanalyzed +------------------------------------------------------------- +__main__ 2 1 0 0 1 0 +a 2 1 0 1 0 0 +a.m 1 0 0 1 0 0 diff --git a/test-data/unit/reports.test b/test-data/unit/reports.test index 69c71f030f76..c806394f9482 100644 --- a/test-data/unit/reports.test +++ b/test-data/unit/reports.test @@ -1,9 +1,6 @@ -- Tests for reports --- ------------------------------ -- --- This file follows syntax of cmdline.test - --- ---------------------------------------- +-- This file follows syntax of cmdline.test. [case testConfigErrorUnknownReport] # cmd: mypy -c pass @@ -30,7 +27,7 @@ def bar() -> str: def untyped_function(): return 42 [outfile build/cobertura.xml] - + $PWD @@ -44,6 +41,7 @@ def untyped_function(): + @@ -128,7 +126,7 @@ T = TypeVar('T') 2 3 -
from typing import TypeVar
+
from typing import TypeVar
 
 T = TypeVar('T')
 
@@ -173,7 +171,7 @@ def bar(x): 5 6
-
from any import any_f
+
from any import any_f
 def bar(x):
     # type: (str) -> None
 2
 3
 
-
import sys
+
import sys
 
 old_stdout = sys.stdout