diff --git a/mypy/build.py b/mypy/build.py index f803929a8fdf..085c3820c383 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -791,7 +791,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache # Ignore cache if (relevant) options aren't the same. cached_options = m.options - current_options = manager.options.clone_for_module(id).select_options_affecting_cache() + current_options = manager.options.clone_for_module(id, path).select_options_affecting_cache() if manager.options.quick_and_dirty: # In quick_and_dirty mode allow non-quick_and_dirty cache files. cached_options['quick_and_dirty'] = True @@ -925,7 +925,7 @@ def write_cache(id: str, path: str, tree: MypyFile, mtime = st.st_mtime size = st.st_size - options = manager.options.clone_for_module(id) + options = manager.options.clone_for_module(id, path) meta = {'id': id, 'path': path, 'mtime': mtime, @@ -1175,7 +1175,7 @@ def __init__(self, else: self.import_context = [] self.id = id or '__main__' - self.options = manager.options.clone_for_module(self.id) + self.options = manager.options.clone_for_module(self.id, path) if not path and source is None: file_id = id if id == 'builtins' and self.options.python_version[0] == 2: diff --git a/mypy/main.py b/mypy/main.py index a5511671c966..c11898f01aa7 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -16,7 +16,7 @@ from mypy import util from mypy.build import BuildSource, BuildResult, PYTHON_EXTENSIONS from mypy.errors import CompileError -from mypy.options import Options, BuildType +from mypy.options import Options, BuildType, parse_section, parse_version from mypy.report import reporter_classes from mypy.version import __version__ @@ -119,26 +119,6 @@ def __getattr__(self, name: str) -> Any: return getattr(self._standard_namespace, name) -def parse_version(v: str) -> Tuple[int, int]: - m = re.match(r'\A(\d)\.(\d+)\Z', v) - if not m: - raise argparse.ArgumentTypeError( - "Invalid python version '{}' (expected format: 'x.y')".format(v)) - major, minor = int(m.group(1)), int(m.group(2)) - if major == 2: - if minor != 7: - raise argparse.ArgumentTypeError( - "Python 2.{} is not supported (must be 2.7)".format(minor)) - elif major == 3: - if minor <= 2: - raise argparse.ArgumentTypeError( - "Python 3.{} is not supported (must be 3.3 or higher)".format(minor)) - else: - raise argparse.ArgumentTypeError( - "Python major version '{}' out of range (must be 2 or 3)".format(major)) - return major, minor - - # Make the help output a little less jarring. class AugmentedHelpFormatter(argparse.HelpFormatter): def __init__(self, prog: Optional[str]) -> None: @@ -559,22 +539,6 @@ def get_init_file(dir: str) -> Optional[str]: return None -# For most options, the type of the default value set in options.py is -# sufficient, and we don't have to do anything here. This table -# exists to specify types for values initialized to None or container -# types. -config_types = { - 'python_version': parse_version, - 'strict_optional_whitelist': lambda s: s.split(), - 'custom_typing_module': str, - 'custom_typeshed_dir': str, - 'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)], - 'junit_xml': str, - # These two are for backwards compatibility - 'silent_imports': bool, - 'almost_silent': bool, -} - SHARED_CONFIG_FILES = ('setup.cfg',) @@ -641,67 +605,6 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None: options.per_module_options[pattern] = updates -def parse_section(prefix: str, template: Options, - section: Mapping[str, str]) -> Tuple[Dict[str, object], Dict[str, str]]: - """Parse one section of a config file. - - Returns a dict of option values encountered, and a dict of report directories. - """ - results = {} # type: Dict[str, object] - report_dirs = {} # type: Dict[str, str] - for key in section: - key = key.replace('-', '_') - if key in config_types: - ct = config_types[key] - else: - dv = getattr(template, key, None) - if dv is None: - if key.endswith('_report'): - report_type = key[:-7].replace('_', '-') - if report_type in reporter_classes: - report_dirs[report_type] = section.get(key) - else: - print("%s: Unrecognized report type: %s" % (prefix, key), - file=sys.stderr) - continue - print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]), - file=sys.stderr) - continue - ct = type(dv) - v = None # type: Any - try: - if ct is bool: - v = section.getboolean(key) # type: ignore # Until better stub - elif callable(ct): - try: - v = ct(section.get(key)) - except argparse.ArgumentTypeError as err: - print("%s: %s: %s" % (prefix, key, err), file=sys.stderr) - continue - else: - print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr) - continue - except ValueError as err: - print("%s: %s: %s" % (prefix, key, err), file=sys.stderr) - continue - if key == 'silent_imports': - print("%s: silent_imports has been replaced by " - "ignore_missing_imports=True; follow_imports=skip" % prefix, file=sys.stderr) - if v: - if 'ignore_missing_imports' not in results: - results['ignore_missing_imports'] = True - if 'follow_imports' not in results: - results['follow_imports'] = 'skip' - if key == 'almost_silent': - print("%s: almost_silent has been replaced by " - "follow_imports=error" % prefix, file=sys.stderr) - if v: - if 'follow_imports' not in results: - results['follow_imports'] = 'error' - results[key] = v - return results, report_dirs - - def fail(msg: str) -> None: sys.stderr.write('%s\n' % msg) sys.exit(1) diff --git a/mypy/options.py b/mypy/options.py index 8c8764200800..dcb92caec3a4 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -1,10 +1,15 @@ +import argparse +from configparser import RawConfigParser import fnmatch +import os import pprint +import re import sys from typing import Any, Mapping, Optional, Tuple, List, Pattern, Dict from mypy import defaults +from mypy.report import reporter_classes class BuildType: @@ -138,14 +143,55 @@ def __ne__(self, other: object) -> bool: def __repr__(self) -> str: return 'Options({})'.format(pprint.pformat(self.__dict__)) - def clone_for_module(self, module: str) -> 'Options': + def clone_for_module(self, module: str, path: Optional[str]) -> 'Options': updates = {} for pattern in self.per_module_options: if self.module_matches_pattern(module, pattern): updates.update(self.per_module_options[pattern]) + + new_options = Options() + + if path and os.path.exists(path): + options_section = [] + found_options = False + with open(path) as file_contents: + for line in file_contents: + if not re.match('\s*#', line): + break + + if re.match('\s*#\s*\[mypy\]', line): + options_section.append(line.strip().strip('#')) + found_options = True + continue + + if found_options: + options_section.append(line.strip().strip('#')) + + if found_options: + parser = RawConfigParser() + parser.read_string("\n".join(options_section)) + updates, report_dirs = parse_section( + "%s [mypy]" % path, + new_options, + parser['mypy'] + ) + if report_dirs: + print("Warning: can't specify new mypy reports " + "in a per-file override (from {})".format(path)) + + for option, file_override in updates.items(): + if file_override == getattr(new_options, option): + # Skip options that are set to the defaults + continue + + if option not in self.PER_MODULE_OPTIONS: + print("Warning: {!r} in {} is not a valid " + "per-module option".format(option, path)) + else: + updates[option] = file_override + if not updates: return self - new_options = Options() new_options.__dict__.update(self.__dict__) new_options.__dict__.update(updates) return new_options @@ -158,3 +204,101 @@ def module_matches_pattern(self, module: str, pattern: Pattern[str]) -> bool: def select_options_affecting_cache(self) -> Mapping[str, bool]: return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE} + + +def parse_version(v: str) -> Tuple[int, int]: + m = re.match(r'\A(\d)\.(\d+)\Z', v) + if not m: + raise argparse.ArgumentTypeError( + "Invalid python version '{}' (expected format: 'x.y')".format(v)) + major, minor = int(m.group(1)), int(m.group(2)) + if major == 2: + if minor != 7: + raise argparse.ArgumentTypeError( + "Python 2.{} is not supported (must be 2.7)".format(minor)) + elif major == 3: + if minor <= 2: + raise argparse.ArgumentTypeError( + "Python 3.{} is not supported (must be 3.3 or higher)".format(minor)) + else: + raise argparse.ArgumentTypeError( + "Python major version '{}' out of range (must be 2 or 3)".format(major)) + return major, minor + + +# For most options, the type of the default value set in options.py is +# sufficient, and we don't have to do anything here. This table +# exists to specify types for values initialized to None or container +# types. +config_types = { + 'python_version': parse_version, + 'strict_optional_whitelist': lambda s: s.split(), + 'custom_typing_module': str, + 'custom_typeshed_dir': str, + 'mypy_path': lambda s: [p.strip() for p in re.split('[,:]', s)], + 'junit_xml': str, + # These two are for backwards compatibility + 'silent_imports': bool, + 'almost_silent': bool, +} + + +def parse_section(prefix: str, template: Options, + section: Mapping[str, str]) -> Tuple[Dict[str, object], Dict[str, str]]: + """Parse one section of a config file. + + Returns a dict of option values encountered, and a dict of report directories. + """ + results = {} # type: Dict[str, object] + report_dirs = {} # type: Dict[str, str] + for key in section: + key = key.replace('-', '_') + if key in config_types: + ct = config_types[key] + else: + dv = getattr(template, key, None) + if dv is None: + if key.endswith('_report'): + report_type = key[:-7].replace('_', '-') + if report_type in reporter_classes: + report_dirs[report_type] = section.get(key) + else: + print("%s: Unrecognized report type: %s" % (prefix, key), + file=sys.stderr) + continue + print("%s: Unrecognized option: %s = %s" % (prefix, key, section[key]), + file=sys.stderr) + continue + ct = type(dv) + v = None # type: Any + try: + if ct is bool: + v = section.getboolean(key) # type: ignore # Until better stub + elif callable(ct): + try: + v = ct(section.get(key)) + except argparse.ArgumentTypeError as err: + print("%s: %s: %s" % (prefix, key, err), file=sys.stderr) + continue + else: + print("%s: Don't know what type %s should have" % (prefix, key), file=sys.stderr) + continue + except ValueError as err: + print("%s: %s: %s" % (prefix, key, err), file=sys.stderr) + continue + if key == 'silent_imports': + print("%s: silent_imports has been replaced by " + "ignore_missing_imports=True; follow_imports=skip" % prefix, file=sys.stderr) + if v: + if 'ignore_missing_imports' not in results: + results['ignore_missing_imports'] = True + if 'follow_imports' not in results: + results['follow_imports'] = 'skip' + if key == 'almost_silent': + print("%s: almost_silent has been replaced by " + "follow_imports=error" % prefix, file=sys.stderr) + if v: + if 'follow_imports' not in results: + results['follow_imports'] = 'error' + results[key] = v + return results, report_dirs diff --git a/mypy/report.py b/mypy/report.py index 74b44ac1f995..9814e4d0d2f7 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -16,11 +16,16 @@ from mypy.nodes import MypyFile, Expression, FuncDef from mypy import stats -from mypy.options import Options from mypy.traverser import TraverserVisitor from mypy.types import Type from mypy.version import __version__ +# Can't use TYPE_CHECKING because it's not in the Python 3.5.1 stdlib +MYPY = False +if MYPY: + from typing import Deque + from mypy.options import Options + try: import lxml.etree as etree LXML_INSTALLED = True @@ -57,7 +62,7 @@ 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, type_map: Dict[Expression, Type], options: 'Options') -> None: for reporter in self.reporters: reporter.on_file(tree, type_map, options) @@ -71,7 +76,10 @@ def __init__(self, reports: Reports, output_dir: str) -> None: self.output_dir = output_dir @abstractmethod - def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], options: Options) -> None: + def on_file(self, + tree: MypyFile, + type_map: Dict[Expression, Type], + options: 'Options') -> None: pass @abstractmethod @@ -108,7 +116,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: # Count physical lines. This assumes the file's encoding is a # superset of ASCII (or at least uses \n in its line endings). with open(tree.path, 'rb') as f: @@ -236,7 +244,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: with open(tree.path) as f: tree_source = f.readlines() @@ -267,7 +275,7 @@ class OldHtmlReporter(AbstractReporter): def on_file(self, tree: MypyFile, - type_map: Dict[Expression, Type], options: Options) -> None: + type_map: Dict[Expression, Type], options: 'Options') -> None: stats.generate_html_report(tree, tree.path, type_map, self.output_dir) def on_finish(self) -> None: @@ -310,7 +318,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: self.last_xml = None path = os.path.relpath(tree.path) if stats.is_special_module(path): @@ -423,7 +431,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: path = os.path.relpath(tree.path) visitor = stats.StatisticsVisitor(inferred=True, typemap=type_map, all_nodes=True) tree.accept(visitor) @@ -519,7 +527,7 @@ class XmlReporter(AbstractXmlReporter): def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: last_xml = self.memory_xml.last_xml if last_xml is None: return @@ -561,7 +569,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: last_xml = self.memory_xml.last_xml if last_xml is None: return @@ -603,7 +611,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None: def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], - options: Options) -> None: + options: 'Options') -> None: pass def on_finish(self) -> None: diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index d0648844daaa..7e6a216d02c4 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -196,6 +196,38 @@ xy.py:1: error: Function is missing a type annotation xy.py:2: error: Call to untyped function "f" in typed context xx.py:1: error: Function is missing a type annotation +[case testInFileConfigSection] +# cmd: mypy x.py y.py z.py +[file mypy.ini] +[[mypy] +disallow_untyped_defs = True +[file x.py] +def f(a): + pass +def g(a: int) -> int: + return f(a) +[file y.py] +# [mypy] +# disallow_untyped_defs = False +def f(a): + pass +def g(a: int) -> int: + return f(a) +[file z.py] +# +# [mypy] +# disallow_untyped_calls = True +# + +def f(a): + pass +def g(a: int) -> int: + return f(a) +[out] +z.py:6: error: Function is missing a type annotation +z.py:9: error: Call to untyped function "f" in typed context +x.py:1: error: Function is missing a type annotation + [case testMultipleGlobConfigSection] # cmd: mypy x.py y.py z.py [file mypy.ini]