From 496991d1417825dc2d6b043f7ea4fcdf7fc742f7 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 16 Jan 2019 22:26:36 -0800 Subject: [PATCH 1/5] register and track error codes --- mypy/build.py | 8 ++- mypy/dmypy_server.py | 1 + mypy/errors.py | 96 ++++++++++++++++++++------- mypy/interpreted_plugin.py | 3 + mypy/main.py | 26 +++++++- mypy/messages.py | 35 ++++++---- mypy/options.py | 1 + mypy/plugin.py | 13 ++++ mypy/plugins/attrs.py | 53 +++++++++------ mypy/plugins/common.py | 28 +++++++- mypy/plugins/default.py | 9 ++- mypy/test/testcheck.py | 1 + mypy/typeshed | 2 +- test-data/unit/check-error-codes.test | 21 ++++++ 14 files changed, 231 insertions(+), 66 deletions(-) create mode 100644 test-data/unit/check-error-codes.test diff --git a/mypy/build.py b/mypy/build.py index bdc942c5e2be..4c915634eddb 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -41,7 +41,7 @@ from mypy.newsemanal.semanal_main import semantic_analysis_for_scc from mypy.checker import TypeChecker from mypy.indirection import TypeIndirectionVisitor -from mypy.errors import Errors, CompileError, report_internal_error +from mypy.errors import Errors, CompileError, report_internal_error, initialize_error_codes from mypy.util import DecodeError, decode_python_encoding, is_sub_path if MYPY: from mypy.report import Reports # Avoid unconditional slow import @@ -59,7 +59,6 @@ from mypy.fscache import FileSystemCache from mypy.metastore import MetadataStore, FilesystemMetadataStore, SqliteMetadataStore from mypy.typestate import TypeState, reset_global_state - from mypy.mypyc_hacks import BuildManagerBase @@ -194,9 +193,12 @@ def _build(sources: List[BuildSource], reports = Reports(data_dir, options.report_dirs) source_set = BuildSourceSet(sources) - errors = Errors(options.show_error_context, options.show_column_numbers) + errors = Errors(options.show_error_context, options.show_column_numbers, + options.show_error_codes) plugin, snapshot = load_plugins(options, errors) + initialize_error_codes(errors, plugin) + # Construct a build manager object to hold state during the build. # # Ignore current directory prefix in error messages. diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index f5e613c081b7..162c49d19d82 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -314,6 +314,7 @@ def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]: return {'out': '', 'err': str(err), 'status': 2} except SystemExit as e: return {'out': stdout.getvalue(), 'err': stderr.getvalue(), 'status': e.code} + assert sources is not None return self.check(sources) def cmd_check(self, files: Sequence[str]) -> Dict[str, object]: diff --git a/mypy/errors.py b/mypy/errors.py index 0053e3ec08c4..b8d2e14586ab 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -11,6 +11,7 @@ MYPY = False if MYPY: + from mypy.plugin import Plugin from typing_extensions import Final T = TypeVar('T') @@ -60,6 +61,9 @@ class ErrorInfo: # Fine-grained incremental target where this was reported target = None # type: Optional[str] + # Error code identifying the message, for grouping/filtering + id = None # type: Optional[str] + def __init__(self, import_ctx: List[Tuple[str, int]], file: str, @@ -73,7 +77,8 @@ def __init__(self, blocker: bool, only_once: bool, origin: Optional[Tuple[str, int]] = None, - target: Optional[str] = None) -> None: + target: Optional[str] = None, + id: Optional[str] = None) -> None: self.import_ctx = import_ctx self.file = file self.module = module @@ -87,6 +92,23 @@ def __init__(self, self.only_once = only_once self.origin = origin or (file, line) self.target = target + self.id = id + + +class ErrorRegistry: + + def __init__(self) -> None: + # mapping of message string literal to id + self.literal_to_id = {} # type: Dict[str, str] + # mapping of message id to string literal + self.id_to_literal = {} # type: Dict[str, str] + + def register_errors(self, mapping: Dict[str, str]) -> int: + updates = {msg: name for name, msg in mapping.items()} + # FIXME: check for name clashes + self.literal_to_id.update(updates) + self.id_to_literal.update(mapping) + return len(updates) class Errors: @@ -131,16 +153,24 @@ class Errors: # Set to True to show column numbers in error messages. show_column_numbers = False # type: bool + # Set to True to show error codes in error messages. + show_error_codes = False + # State for keeping track of the current fine-grained incremental mode target. # (See mypy.server.update for more about targets.) # Current module id. target_module = None # type: Optional[str] scope = None # type: Optional[Scope] + error_codes = None # type: ErrorRegistry + def __init__(self, show_error_context: bool = False, - show_column_numbers: bool = False) -> None: + show_column_numbers: bool = False, + show_error_codes: bool = False) -> None: self.show_error_context = show_error_context self.show_column_numbers = show_column_numbers + self.show_error_codes = show_error_codes + self.error_codes = ErrorRegistry() self.initialize() def initialize(self) -> None: @@ -160,7 +190,7 @@ def reset(self) -> None: self.initialize() def copy(self) -> 'Errors': - new = Errors(self.show_error_context, self.show_column_numbers) + new = Errors(self.show_error_context, self.show_column_numbers, self.show_error_codes) new.file = self.file new.import_ctx = self.import_ctx[:] new.type_name = self.type_name[:] @@ -233,7 +263,8 @@ def report(self, file: Optional[str] = None, only_once: bool = False, origin_line: Optional[int] = None, - offset: int = 0) -> None: + offset: int = 0, + id: Optional[str] = None) -> None: """Report message at the given line using the current error context. Args: @@ -264,7 +295,7 @@ def report(self, function, line, column, severity, message, blocker, only_once, origin=(self.file, origin_line) if origin_line else None, - target=self.current_target()) + target=self.current_target(), id=id) self.add_error_info(info) def _add_error_info(self, file: str, info: ErrorInfo) -> None: @@ -356,7 +387,7 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: a = [] # type: List[str] errors = self.render_messages(self.sort_messages(error_info)) errors = self.remove_duplicates(errors) - for file, line, column, severity, message in errors: + for file, line, column, severity, message, id in errors: s = '' if file is not None: if self.show_column_numbers and line is not None and line >= 0 \ @@ -366,7 +397,11 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: srcloc = '{}:{}'.format(file, line) else: srcloc = file - s = '{}: {}: {}'.format(srcloc, severity, message) + if self.show_error_codes: + s = '{}: {}: {}: {}'.format(srcloc, severity, + id if id is not None else 'unknown', message) + else: + s = '{}: {}: {}'.format(srcloc, severity, message) else: s = message a.append(s) @@ -404,8 +439,8 @@ def targets(self) -> Set[str]: for info in errs if info.target) - def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int, - str, str]]: + def render_messages(self, errors: List[ErrorInfo] + ) -> List[Tuple[Optional[str], int, int, str, str, Optional[str]]]: """Translate the messages into a sequence of tuples. Each tuple is of form (path, line, col, severity, message). @@ -413,12 +448,13 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], The path item may be None. If the line item is negative, the line number is not defined for the tuple. """ - result = [] # type: List[Tuple[Optional[str], int, int, str, str]] + result = [] # type: List[Tuple[Optional[str], int, int, str, str, Optional[str]]] # (path, line, column, severity, message) prev_import_context = [] # type: List[Tuple[str, int]] prev_function_or_member = None # type: Optional[str] prev_type = None # type: Optional[str] + prev_msg_id = None # type: Optional[str] for e in errors: # Report module import context, if different from previous message. @@ -439,7 +475,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], # Remove prefix to ignore from path (if present) to # simplify path. path = remove_path_prefix(path, self.ignore_prefix) - result.append((None, -1, -1, 'note', fmt.format(path, line))) + result.append((None, -1, -1, 'note', fmt.format(path, line), prev_msg_id)) i -= 1 file = self.simplify_path(e.file) @@ -451,31 +487,34 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], e.type != prev_type): if e.function_or_member is None: if e.type is None: - result.append((file, -1, -1, 'note', 'At top level:')) + result.append((file, -1, -1, 'note', 'At top level:', prev_msg_id)) else: - result.append((file, -1, -1, 'note', 'In class "{}":'.format( - e.type))) + result.append((file, -1, -1, 'note', + 'In class "{}":'.format(e.type), + prev_msg_id)) else: if e.type is None: result.append((file, -1, -1, 'note', - 'In function "{}":'.format( - e.function_or_member))) + 'In function "{}":'.format(e.function_or_member), + prev_msg_id)) else: result.append((file, -1, -1, 'note', 'In member "{}" of class "{}":'.format( - e.function_or_member, e.type))) + e.function_or_member, e.type), + prev_msg_id)) elif e.type != prev_type: if e.type is None: - result.append((file, -1, -1, 'note', 'At top level:')) + result.append((file, -1, -1, 'note', 'At top level:', prev_msg_id)) else: result.append((file, -1, -1, 'note', - 'In class "{}":'.format(e.type))) + 'In class "{}":'.format(e.type), prev_msg_id)) - result.append((file, e.line, e.column, e.severity, e.message)) + result.append((file, e.line, e.column, e.severity, e.message, e.id)) prev_import_context = e.import_ctx prev_function_or_member = e.function_or_member prev_type = e.type + prev_msg_id = e.id return result @@ -502,10 +541,11 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]: result.extend(a) return result - def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str]] - ) -> List[Tuple[Optional[str], int, int, str, str]]: + def remove_duplicates(self, + errors: List[Tuple[Optional[str], int, int, str, str, Optional[str]]] + ) -> List[Tuple[Optional[str], int, int, str, str, Optional[str]]]: """Remove duplicates from a sorted error list.""" - res = [] # type: List[Tuple[Optional[str], int, int, str, str]] + res = [] # type: List[Tuple[Optional[str], int, int, str, str, Optional[str]]] i = 0 while i < len(errors): dup = False @@ -620,3 +660,13 @@ def report_internal_error(err: Exception, file: Optional[str], line: int, # Exit. The caller has nothing more to say. # We use exit code 2 to signal that this is no ordinary error. raise SystemExit(2) + + +def initialize_error_codes(errors: Errors, plugin: 'Plugin') -> None: + """Populate errors.error_codes""" + from mypy.plugins.common import extract_error_codes + import mypy.message_registry + # read native error codes from the message registry + errors.error_codes.register_errors(extract_error_codes('', mypy.message_registry)) + # read custom error codes from plugins + errors.error_codes.register_errors(plugin.get_error_codes()) diff --git a/mypy/interpreted_plugin.py b/mypy/interpreted_plugin.py index e95fbe1e3260..e640d69e3fa6 100644 --- a/mypy/interpreted_plugin.py +++ b/mypy/interpreted_plugin.py @@ -35,6 +35,9 @@ def __init__(self, options: Options) -> None: def set_modules(self, modules: Dict[str, MypyFile]) -> None: self._modules = modules + def get_error_codes(self) -> Dict[str, str]: + return {} + def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]: assert self._modules is not None return lookup_fully_qualified(fullname, self._modules) diff --git a/mypy/main.py b/mypy/main.py index ab23f90c7fde..dc696a419f80 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -70,6 +70,9 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: fscache = FileSystemCache() sources, options = process_options(args, fscache=fscache) + if sources is None: + sys.exit(0) + messages = [] def flush_errors(new_messages: List[str], serious: bool) -> None: @@ -291,7 +294,7 @@ def process_options(args: List[str], fscache: Optional[FileSystemCache] = None, program: str = 'mypy', header: str = HEADER, - ) -> Tuple[List[BuildSource], Options]: + ) -> Tuple[Optional[List[BuildSource]], Options]: """Parse command line arguments. If a FileSystemCache is passed in, and package_root options are given, @@ -600,9 +603,15 @@ def add_invertible_flag(flag: str, dest='show_error_context', help='Precede errors with "note:" messages explaining context', group=error_group) + add_invertible_flag('--show-error-codes', default=False, + help="Show error codes in error messages", + group=error_group) add_invertible_flag('--show-column-numbers', default=False, help="Show column numbers in error messages", group=error_group) + error_group.add_argument( + '--list-error-codes', action='store_true', + help="List known error codes and exit") strict_help = "Strict mode; enables the following flags: {}".format( ", ".join(strict_flag_names)) @@ -706,12 +715,22 @@ def add_invertible_flag(flag: str, # filename for the config file and know if the user requested all strict options. dummy = argparse.Namespace() parser.parse_args(args, dummy) + options = Options() + + if dummy.list_error_codes: + import mypy.errors + errors = mypy.errors.Errors() + plugin, _ = build.load_plugins(options, errors) + mypy.errors.initialize_error_codes(errors, plugin) + for msg_id in sorted(errors.error_codes.id_to_literal): + print('%s %r' % (msg_id, errors.error_codes.id_to_literal[msg_id])) + return None, options + config_file = dummy.config_file if config_file is not None and not os.path.exists(config_file): parser.error("Cannot find config file '%s'" % config_file) # Parse config file first, so command line can override. - options = Options() parse_config_file(options, config_file) # Set strict flags before parsing (if strict mode enabled), so other command @@ -724,6 +743,9 @@ def add_invertible_flag(flag: str, special_opts = argparse.Namespace() parser.parse_args(args, SplitNamespace(options, special_opts, 'special-opts:')) + # unneeded attribute transfered from parser + delattr(options, 'list_error_codes') + # The python_version is either the default, which can be overridden via a config file, # or stored in special_opts and is passed via the command line. options.python_version = special_opts.python_version or options.python_version diff --git a/mypy/messages.py b/mypy/messages.py index ce7a90ea32d1..34c067fe0a15 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -14,7 +14,10 @@ import difflib from textwrap import dedent -from typing import cast, List, Dict, Any, Sequence, Iterable, Tuple, Set, Optional, Union +from typing import ( + cast, List, Dict, Any, Sequence, Iterable, Tuple, Set, Optional, Union, TypeVar, + Callable +) from mypy.erasetype import erase_type from mypy.errors import Errors @@ -36,6 +39,7 @@ if MYPY: from typing_extensions import Final +T = TypeVar('T', bound=Callable[..., Any]) ARG_CONSTRUCTOR_NAMES = { ARG_POS: "Arg", @@ -75,6 +79,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile]) -> None: self.modules = modules self.disable_count = 0 self.disable_type_names = 0 + self.active_message_id = None # # Helpers @@ -107,38 +112,44 @@ def enable_errors(self) -> None: def is_errors(self) -> bool: return self.errors.is_errors() - def report(self, msg: str, context: Optional[Context], severity: str, - file: Optional[str] = None, origin: Optional[Context] = None, - offset: int = 0) -> None: + def report(self, msg: str, format_args: Tuple[Any, ...], context: Optional[Context], + severity: str, file: Optional[str] = None, origin: Optional[Context] = None, + offset: int = 0, id: Optional[str] = None) -> None: """Report an error or note (unless disabled).""" if self.disable_count <= 0: + msg_id = id + if msg_id is None: + msg_id = self.errors.error_codes.literal_to_id.get(msg) + if format_args: + msg = msg.format(*format_args) self.errors.report(context.get_line() if context else -1, context.get_column() if context else -1, msg, severity=severity, file=file, offset=offset, - origin_line=origin.get_line() if origin else None) + origin_line=origin.get_line() if origin else None, + id=msg_id) def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None, origin: Optional[Context] = None) -> None: """Report an error message (unless disabled).""" - self.report(msg, context, 'error', file=file, origin=origin) + self.report(msg, (), context, 'error', file=file, origin=origin) def note(self, msg: str, context: Context, file: Optional[str] = None, origin: Optional[Context] = None, offset: int = 0) -> None: """Report a note (unless disabled).""" - self.report(msg, context, 'note', file=file, origin=origin, + self.report(msg, (), context, 'note', file=file, origin=origin, offset=offset) def note_multiline(self, messages: str, context: Context, file: Optional[str] = None, origin: Optional[Context] = None, offset: int = 0) -> None: """Report as many notes as lines in the message (unless disabled).""" for msg in messages.splitlines(): - self.report(msg, context, 'note', file=file, origin=origin, + self.report(msg, (), context, 'note', file=file, origin=origin, offset=offset) def warn(self, msg: str, context: Context, file: Optional[str] = None, origin: Optional[Context] = None) -> None: """Report a warning message (unless disabled).""" - self.report(msg, context, 'warning', file=file, origin=origin) + self.report(msg, (), context, 'warning', file=file, origin=origin) def quote_type_string(self, type_string: str) -> str: """Quotes a type representation for use in messages.""" @@ -971,9 +982,9 @@ def incompatible_typevar_value(self, typ: Type, typevar_name: str, context: Context) -> None: - self.fail(message_registry.INCOMPATIBLE_TYPEVAR_VALUE - .format(typevar_name, callable_name(callee) or 'function', self.format(typ)), - context) + self.report(message_registry.INCOMPATIBLE_TYPEVAR_VALUE, + (typevar_name, callable_name(callee) or 'function', self.format(typ)), + context, 'error') def dangerous_comparison(self, left: Type, right: Type, kind: str, ctx: Context) -> None: left_str = 'element' if kind == 'container' else 'left operand' diff --git a/mypy/options.py b/mypy/options.py index c498ad6fba50..0e25211dca22 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -225,6 +225,7 @@ def __init__(self) -> None: # -- experimental options -- self.shadow_file = None # type: Optional[List[List[str]]] self.show_column_numbers = False # type: bool + self.show_error_codes = False # type: bool self.dump_graph = False self.dump_deps = False self.logical_deps = False diff --git a/mypy/plugin.py b/mypy/plugin.py index 1d51fe418c84..df8decce70dc 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -357,6 +357,10 @@ def __init__(self, options: Options) -> None: def set_modules(self, modules: Dict[str, MypyFile]) -> None: self._modules = modules + def get_error_codes(self) -> Dict[str, str]: + """Return mapping of error code names to error messages""" + return {} + def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]: assert self._modules is not None return lookup_fully_qualified(fullname, self._modules) @@ -548,6 +552,9 @@ def __init__(self, plugin: mypy.interpreted_plugin.InterpretedPlugin) -> None: def set_modules(self, modules: Dict[str, MypyFile]) -> None: self.plugin.set_modules(modules) + def get_error_codes(self) -> Dict[str, str]: + return self.plugin.get_error_codes() + def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]: return self.plugin.lookup_fully_qualified(fullname) @@ -616,6 +623,12 @@ def set_modules(self, modules: Dict[str, MypyFile]) -> None: for plugin in self._plugins: plugin.set_modules(modules) + def get_error_codes(self) -> Dict[str, str]: + results = {} + for plugin in self._plugins: + results.update(plugin.get_error_codes()) + return results + def get_type_analyze_hook(self, fullname: str ) -> Optional[Callable[[AnalyzeTypeContext], Type]]: return self._find_hook(lambda plugin: plugin.get_type_analyze_hook(fullname)) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index e92b4954cee0..d7974807f555 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -27,7 +27,6 @@ if MYPY: from typing_extensions import Final -KW_ONLY_PYTHON_2_UNSUPPORTED = "kw_only is not supported in Python 2" # The names of the different functions that create classes or arguments. attr_class_makers = { @@ -57,6 +56,24 @@ def __init__(self, self.is_attr_converters_optional = is_attr_converters_optional +class Errors: + CANNOT_DETERMINE_INIT_FROM_CONVERTER = "Cannot determine __init__ type from converter" + AUTO_ATTRIB_UNSUPPORTED = "auto_attribs is not supported in Python 2" + OLD_STYLE_CLASSES_UNSUPPORTED = "attrs only works with new-style classes" + KW_ONLY_UNSUPPORTED = "kw_only is not supported in Python 2" + NON_DEFAULT_ATTR_NOT_ALLOWED_AFTER_DEFAULT = \ + "Non-default attributes not allowed after default attributes." + NON_KW_ONLY_ATTR_NOT_ALLOWED_AFTER_KW_ONLY = \ + "Non keyword-only attributes are not allowed after a keyword-only attribute." + TOO_MANY_NAMES_FOR_ATTR = "Too many names for one attribute" + CANT_PASS_DEFAULT_AND_FACTORY = "Can't pass both `default` and `factory`." + INVALID_ARG_TO_TYPE = 'Invalid argument to type' + CANT_PASS_CONVERT_AND_CONVERTER = "Can't pass both `convert` and `converter`." + CONVERT_IS_DEPRECATED = "convert is deprecated, use converter" + UNSUPPORTED_CONVERTER = \ + "Unsupported converter, only named functions and types are currently supported" + + class Attribute: """The value of an attr.ib() call.""" @@ -118,7 +135,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: init_type = UnionType.make_union([init_type, NoneTyp()]) if not init_type: - ctx.api.fail("Cannot determine __init__ type from converter", self.context) + ctx.api.fail(Errors.CANNOT_DETERMINE_INIT_FROM_CONVERTER, self.context) init_type = AnyType(TypeOfAny.from_error) elif self.converter.name == '': # This means we had a converter but it's not of a type we can infer. @@ -198,14 +215,14 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', if ctx.api.options.python_version[0] < 3: if auto_attribs: - ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) + ctx.api.fail(Errors.AUTO_ATTRIB_UNSUPPORTED, ctx.reason) return if not info.defn.base_type_exprs: # Note: This will not catch subclassing old-style classes. - ctx.api.fail("attrs only works with new-style classes", info.defn) + ctx.api.fail(Errors.OLD_STYLE_CLASSES_UNSUPPORTED, info.defn) return if kw_only: - ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason) + ctx.api.fail(Errors.KW_ONLY_UNSUPPORTED, ctx.reason) return attributes = _analyze_class(ctx, auto_attribs, kw_only) @@ -299,14 +316,9 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', continue if not attribute.has_default and last_default: - ctx.api.fail( - "Non-default attributes not allowed after default attributes.", - attribute.context) + ctx.api.fail(Errors.NON_DEFAULT_ATTR_NOT_ALLOWED_AFTER_DEFAULT, attribute.context) if last_kw_only: - ctx.api.fail( - "Non keyword-only attributes are not allowed after a keyword-only attribute.", - attribute.context - ) + ctx.api.fail(Errors.NON_KW_ONLY_ATTR_NOT_ALLOWED_AFTER_KW_ONLY, attribute.context) last_default |= attribute.has_default return attributes @@ -400,7 +412,7 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', return None if len(stmt.lvalues) > 1: - ctx.api.fail("Too many names for one attribute", stmt) + ctx.api.fail(Errors.TOO_MANY_NAMES_FOR_ATTR, stmt) return None # This is the type that belongs in the __init__ method for this attrib. @@ -412,7 +424,7 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', # See https://github.com/python-attrs/attrs/issues/481 for explanation. kw_only |= _get_bool_argument(ctx, rvalue, 'kw_only', False) if kw_only and ctx.api.options.python_version[0] < 3: - ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, stmt) + ctx.api.fail(Errors.KW_ONLY_UNSUPPORTED, stmt) return None # TODO: Check for attr.NOTHING @@ -420,7 +432,7 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', attr_has_factory = bool(_get_argument(rvalue, 'factory')) if attr_has_default and attr_has_factory: - ctx.api.fail("Can't pass both `default` and `factory`.", rvalue) + ctx.api.fail(Errors.CANT_PASS_DEFAULT_AND_FACTORY, rvalue) elif attr_has_factory: attr_has_default = True @@ -430,7 +442,7 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', try: un_type = expr_to_unanalyzed_type(type_arg) except TypeTranslationError: - ctx.api.fail('Invalid argument to type', type_arg) + ctx.api.fail(Errors.INVALID_ARG_TO_TYPE, type_arg) else: init_type = ctx.api.anal_type(un_type) if init_type and isinstance(lhs.node, Var) and not lhs.node.type: @@ -442,9 +454,9 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', converter = _get_argument(rvalue, 'converter') convert = _get_argument(rvalue, 'convert') if convert and converter: - ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) + ctx.api.fail(Errors.CANT_PASS_CONVERT_AND_CONVERTER, rvalue) elif convert: - ctx.api.fail("convert is deprecated, use converter", rvalue) + ctx.api.fail(Errors.CONVERT_IS_DEPRECATED, rvalue) converter = convert converter_info = _parse_converter(ctx, converter) @@ -477,10 +489,7 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext', return argument # Signal that we have an unsupported converter. - ctx.api.fail( - "Unsupported converter, only named functions and types are currently supported", - converter - ) + ctx.api.fail(Errors.UNSUPPORTED_CONVERTER, converter) return Converter('') return Converter(None) diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index e95634152ab0..9e0c2335ad5b 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -1,4 +1,6 @@ -from typing import List, Optional, Any +import re + +from typing import List, Optional, Any, Dict from mypy.nodes import ( ARG_POS, MDEF, Argument, Block, CallExpr, Expression, FuncBase, @@ -9,6 +11,8 @@ from mypy.types import CallableType, Overloaded, Type, TypeVarDef, LiteralType, Instance from mypy.typevars import fill_typevars +VALID_ERROR_CODE = re.compile('[a-z0-9_]+$') + def _get_decorator_bool_argument( ctx: ClassDefContext, @@ -129,3 +133,25 @@ def try_getting_str_literal(expr: Expression, typ: Type) -> Optional[str]: return expr.value else: return None + + +def extract_error_codes(namespace: str, obj: Any) -> Dict[str, str]: + """Read error code attributes from an object. + + Any attribute of obj that is all caps (i.e. a constant) and holds a string is considered + an error code. + + Arguments: + namespace: a prefix to be added to all error code names. This is usually the name of the + plugin that is calling this function. + obj: object from which to read error codes. + """ + if namespace: + if not namespace.endswith('_'): + namespace += '_' + namespace = namespace.lower() + if not VALID_ERROR_CODE.match(namespace): + raise RuntimeError("Error namespace must contain only letters, " + "numbers, and underscores") + return {namespace + name.lower(): msg for name, msg in vars(obj).items() + if name.upper() and not name.startswith('_') and isinstance(msg, str)} diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 92d3d10f9111..20cc27bf7369 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -1,12 +1,12 @@ from functools import partial -from typing import Callable, Optional +from typing import Callable, Optional, Dict from mypy import message_registry from mypy.nodes import StrExpr, IntExpr, DictExpr, UnaryExpr from mypy.plugin import ( Plugin, FunctionContext, MethodContext, MethodSigContext, AttributeContext, ClassDefContext ) -from mypy.plugins.common import try_getting_str_literal +from mypy.plugins.common import try_getting_str_literal, extract_error_codes from mypy.types import ( Type, Instance, AnyType, TypeOfAny, CallableType, NoneTyp, UnionType, TypedDictType, TypeVarType @@ -92,6 +92,11 @@ def get_class_decorator_hook(self, fullname: str return dataclasses.dataclass_class_maker_callback return None + def get_error_codes(self) -> Dict[str, str]: + from mypy.plugins import attrs + error_codes = extract_error_codes('attrs', attrs.Errors) + return error_codes + def open_callback(ctx: FunctionContext) -> Type: """Infer a better return type for 'open'. diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index e089b2ad590e..0dda86ed964a 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -83,6 +83,7 @@ 'check-redefine.test', 'check-literal.test', 'check-newsemanal.test', + 'check-error-codes.test', ] diff --git a/mypy/typeshed b/mypy/typeshed index 306b4694ae38..f343150a6db5 160000 --- a/mypy/typeshed +++ b/mypy/typeshed @@ -1 +1 @@ -Subproject commit 306b4694ae382e6b9085689606156b17f4166cba +Subproject commit f343150a6db5fade64dda889f98231e0f7780ebb diff --git a/test-data/unit/check-error-codes.test b/test-data/unit/check-error-codes.test new file mode 100644 index 000000000000..c221541a4e87 --- /dev/null +++ b/test-data/unit/check-error-codes.test @@ -0,0 +1,21 @@ +[case testErrorCodesAssignToMethodViaClass] +# flags: --no-strict-optional --show-error-codes +import typing +class A: + def f(self): pass +A.f = None # E: cannot_assign_to_method: Cannot assign to a method + +[case testErrorCodesNoReturnValueExpected] +# flags: --no-strict-optional --show-error-codes +from typing import Generator +def f() -> Generator[int, None, None]: + yield 1 + return 42 # E: no_return_value_expected: No return value expected +[out] + +[case testErrorCodesIncompatibleTypevar] +# flags: --no-strict-optional --show-error-codes +from typing import TypeVar, Union +AnyStr = TypeVar('AnyStr', bytes, str) +def f(x: Union[AnyStr, int], *a: AnyStr) -> None: pass +f('foo', b'bar') # E: incompatible_typevar_value: Value of type variable "AnyStr" of "f" cannot be "object" From 1e594bd16abaf3497a3b5d0737f4a28f48f0cd0d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 21 Feb 2019 22:29:51 -0800 Subject: [PATCH 2/5] General design improvements --- mypy/build.py | 38 ++++++++++++++++++++++-------- mypy/errors.py | 51 ++++++++++++++++++----------------------- mypy/main.py | 7 +++--- mypy/messages.py | 4 +--- mypy/options.py | 2 +- mypy/plugin.py | 8 +------ mypy/plugins/common.py | 20 +++------------- mypy/plugins/default.py | 3 +-- mypy/typeshed | 2 +- 9 files changed, 63 insertions(+), 72 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 4c915634eddb..4bacf2dfcfbe 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -41,7 +41,7 @@ from mypy.newsemanal.semanal_main import semantic_analysis_for_scc from mypy.checker import TypeChecker from mypy.indirection import TypeIndirectionVisitor -from mypy.errors import Errors, CompileError, report_internal_error, initialize_error_codes +from mypy.errors import Errors, CompileError, report_internal_error from mypy.util import DecodeError, decode_python_encoding, is_sub_path if MYPY: from mypy.report import Reports # Avoid unconditional slow import @@ -59,6 +59,7 @@ from mypy.fscache import FileSystemCache from mypy.metastore import MetadataStore, FilesystemMetadataStore, SqliteMetadataStore from mypy.typestate import TypeState, reset_global_state + from mypy.mypyc_hacks import BuildManagerBase @@ -195,10 +196,9 @@ def _build(sources: List[BuildSource], source_set = BuildSourceSet(sources) errors = Errors(options.show_error_context, options.show_column_numbers, options.show_error_codes) + errors.initialize_error_codes() plugin, snapshot = load_plugins(options, errors) - initialize_error_codes(errors, plugin) - # Construct a build manager object to hold state during the build. # # Ignore current directory prefix in error messages. @@ -329,7 +329,27 @@ def load_plugins(options: Options, errors: Errors) -> Tuple[Plugin, Dict[str, st import importlib snapshot = {} # type: Dict[str, str] + def plugin_error(message: str) -> None: + errors.report(line, 0, message) + errors.raise_error() + + def load_plugin(plugin: Plugin, module_name: str) -> None: + try: + error_codes = plugin.get_error_codes() + except Exception: + plugin_error('Error calling get_error_codes of {}'.format(module_name)) + raise + + try: + error_codes = {module_name + '-' + name: msg for name, msg in error_codes.items()} + except Exception: + plugin_error('Error processing result of get_error_codes ({})'.format(module_name)) + raise + errors.register_error_codes(error_codes) + default_plugin = DefaultPlugin(options) # type: Plugin + load_plugin(default_plugin, 'mypy.plugins.default') + if not options.config_file: return default_plugin, snapshot @@ -337,10 +357,6 @@ def load_plugins(options: Options, errors: Errors) -> Tuple[Plugin, Dict[str, st if line == -1: line = 1 # We need to pick some line number that doesn't look too confusing - def plugin_error(message: str) -> None: - errors.report(line, 0, message) - errors.raise_error() - custom_plugins = [] # type: List[Plugin] errors.set_file(options.config_file, None) for plugin_path in options.plugins: @@ -394,11 +410,15 @@ def plugin_error(message: str) -> None: 'Return value of "plugin" must be a subclass of "mypy.plugin.Plugin" ' '(in {})'.format(plugin_path)) try: - custom_plugins.append(plugin_type(options)) - snapshot[module_name] = take_module_snapshot(module) + plugin = plugin_type(options) # type: Plugin except Exception: print('Error constructing plugin instance of {}\n'.format(plugin_type.__name__)) raise # Propagate to display traceback + else: + load_plugin(plugin, module_name) + custom_plugins.append(plugin) + snapshot[module_name] = take_module_snapshot(module) + # Custom plugins take precedence over the default plugin. return ChainedPlugin(options, custom_plugins + [default_plugin]), snapshot diff --git a/mypy/errors.py b/mypy/errors.py index b8d2e14586ab..b29f15a3caf5 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -11,7 +11,6 @@ MYPY = False if MYPY: - from mypy.plugin import Plugin from typing_extensions import Final T = TypeVar('T') @@ -95,22 +94,6 @@ def __init__(self, self.id = id -class ErrorRegistry: - - def __init__(self) -> None: - # mapping of message string literal to id - self.literal_to_id = {} # type: Dict[str, str] - # mapping of message id to string literal - self.id_to_literal = {} # type: Dict[str, str] - - def register_errors(self, mapping: Dict[str, str]) -> int: - updates = {msg: name for name, msg in mapping.items()} - # FIXME: check for name clashes - self.literal_to_id.update(updates) - self.id_to_literal.update(mapping) - return len(updates) - - class Errors: """Container for compile errors. @@ -162,7 +145,8 @@ class Errors: target_module = None # type: Optional[str] scope = None # type: Optional[Scope] - error_codes = None # type: ErrorRegistry + # Mapping of error string literal to code + error_codes = None # type: Dict[str, str] def __init__(self, show_error_context: bool = False, show_column_numbers: bool = False, @@ -170,7 +154,6 @@ def __init__(self, show_error_context: bool = False, self.show_error_context = show_error_context self.show_column_numbers = show_column_numbers self.show_error_codes = show_error_codes - self.error_codes = ErrorRegistry() self.initialize() def initialize(self) -> None: @@ -185,6 +168,7 @@ def initialize(self) -> None: self.only_once_messages = set() self.scope = None self.target_module = None + self.error_codes = {} def reset(self) -> None: self.initialize() @@ -197,6 +181,7 @@ def copy(self) -> 'Errors': new.function_or_member = self.function_or_member[:] new.target_module = self.target_module new.scope = self.scope + new.error_codes = self.error_codes.copy() return new def total_errors(self) -> int: @@ -566,6 +551,24 @@ def remove_duplicates(self, i += 1 return res + def register_error_codes(self, mapping: Dict[str, str]) -> None: + """Add error codes and their message literals to the list of known errors. + + Args: + mapping: map of error code to message literal + """ + # reverse the lookup + updates = {msg: name for name, msg in mapping.items()} + # FIXME: check for name clashes + self.error_codes.update(updates) + + def initialize_error_codes(self) -> None: + """Populate error codes""" + from mypy.plugins.common import extract_error_codes + import mypy.message_registry + # read native error codes from the message registry + self.register_error_codes(extract_error_codes(mypy.message_registry)) + class CompileError(Exception): """Exception raised when there is a compile error. @@ -660,13 +663,3 @@ def report_internal_error(err: Exception, file: Optional[str], line: int, # Exit. The caller has nothing more to say. # We use exit code 2 to signal that this is no ordinary error. raise SystemExit(2) - - -def initialize_error_codes(errors: Errors, plugin: 'Plugin') -> None: - """Populate errors.error_codes""" - from mypy.plugins.common import extract_error_codes - import mypy.message_registry - # read native error codes from the message registry - errors.error_codes.register_errors(extract_error_codes('', mypy.message_registry)) - # read custom error codes from plugins - errors.error_codes.register_errors(plugin.get_error_codes()) diff --git a/mypy/main.py b/mypy/main.py index dc696a419f80..7ffd87738d6b 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -720,10 +720,11 @@ def add_invertible_flag(flag: str, if dummy.list_error_codes: import mypy.errors errors = mypy.errors.Errors() + errors.initialize_error_codes() plugin, _ = build.load_plugins(options, errors) - mypy.errors.initialize_error_codes(errors, plugin) - for msg_id in sorted(errors.error_codes.id_to_literal): - print('%s %r' % (msg_id, errors.error_codes.id_to_literal[msg_id])) + error_codes = {code: msg for msg, code in errors.error_codes.items()} + for code in sorted(error_codes): + print('%s %r' % (code, error_codes[code])) return None, options config_file = dummy.config_file diff --git a/mypy/messages.py b/mypy/messages.py index 34c067fe0a15..e09bcb7958c4 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -39,8 +39,6 @@ if MYPY: from typing_extensions import Final -T = TypeVar('T', bound=Callable[..., Any]) - ARG_CONSTRUCTOR_NAMES = { ARG_POS: "Arg", ARG_OPT: "DefaultArg", @@ -119,7 +117,7 @@ def report(self, msg: str, format_args: Tuple[Any, ...], context: Optional[Conte if self.disable_count <= 0: msg_id = id if msg_id is None: - msg_id = self.errors.error_codes.literal_to_id.get(msg) + msg_id = self.errors.error_codes.get(msg) if format_args: msg = msg.format(*format_args) self.errors.report(context.get_line() if context else -1, diff --git a/mypy/options.py b/mypy/options.py index 0e25211dca22..3a2a842b18ee 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -225,7 +225,7 @@ def __init__(self) -> None: # -- experimental options -- self.shadow_file = None # type: Optional[List[List[str]]] self.show_column_numbers = False # type: bool - self.show_error_codes = False # type: bool + self.show_error_codes = False self.dump_graph = False self.dump_deps = False self.logical_deps = False diff --git a/mypy/plugin.py b/mypy/plugin.py index df8decce70dc..6243e857fb94 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -358,7 +358,7 @@ def set_modules(self, modules: Dict[str, MypyFile]) -> None: self._modules = modules def get_error_codes(self) -> Dict[str, str]: - """Return mapping of error code names to error messages""" + """Register error code names to error messages""" return {} def lookup_fully_qualified(self, fullname: str) -> Optional[SymbolTableNode]: @@ -623,12 +623,6 @@ def set_modules(self, modules: Dict[str, MypyFile]) -> None: for plugin in self._plugins: plugin.set_modules(modules) - def get_error_codes(self) -> Dict[str, str]: - results = {} - for plugin in self._plugins: - results.update(plugin.get_error_codes()) - return results - def get_type_analyze_hook(self, fullname: str ) -> Optional[Callable[[AnalyzeTypeContext], Type]]: return self._find_hook(lambda plugin: plugin.get_type_analyze_hook(fullname)) diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index 9e0c2335ad5b..08a76d23f6a5 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -1,5 +1,3 @@ -import re - from typing import List, Optional, Any, Dict from mypy.nodes import ( @@ -11,8 +9,6 @@ from mypy.types import CallableType, Overloaded, Type, TypeVarDef, LiteralType, Instance from mypy.typevars import fill_typevars -VALID_ERROR_CODE = re.compile('[a-z0-9_]+$') - def _get_decorator_bool_argument( ctx: ClassDefContext, @@ -135,23 +131,13 @@ def try_getting_str_literal(expr: Expression, typ: Type) -> Optional[str]: return None -def extract_error_codes(namespace: str, obj: Any) -> Dict[str, str]: - """Read error code attributes from an object. - +def extract_error_codes(obj: Any) -> Dict[str, str]: + """ Any attribute of obj that is all caps (i.e. a constant) and holds a string is considered an error code. Arguments: - namespace: a prefix to be added to all error code names. This is usually the name of the - plugin that is calling this function. obj: object from which to read error codes. """ - if namespace: - if not namespace.endswith('_'): - namespace += '_' - namespace = namespace.lower() - if not VALID_ERROR_CODE.match(namespace): - raise RuntimeError("Error namespace must contain only letters, " - "numbers, and underscores") - return {namespace + name.lower(): msg for name, msg in vars(obj).items() + return {name.lower(): msg for name, msg in vars(obj).items() if name.upper() and not name.startswith('_') and isinstance(msg, str)} diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 20cc27bf7369..bd3570612cca 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -94,8 +94,7 @@ def get_class_decorator_hook(self, fullname: str def get_error_codes(self) -> Dict[str, str]: from mypy.plugins import attrs - error_codes = extract_error_codes('attrs', attrs.Errors) - return error_codes + return extract_error_codes(attrs.Errors) def open_callback(ctx: FunctionContext) -> Type: diff --git a/mypy/typeshed b/mypy/typeshed index f343150a6db5..306b4694ae38 160000 --- a/mypy/typeshed +++ b/mypy/typeshed @@ -1 +1 @@ -Subproject commit f343150a6db5fade64dda889f98231e0f7780ebb +Subproject commit 306b4694ae382e6b9085689606156b17f4166cba From 2aa8caadf546e77b87e922ef4130d23fcefb83b6 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 23 Feb 2019 14:53:42 -0800 Subject: [PATCH 3/5] Add a namespace/prefix to builtin error codes --- mypy/build.py | 3 +-- mypy/errors.py | 9 +++++---- test-data/unit/check-error-codes.test | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 4bacf2dfcfbe..6d502ac3b5df 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -341,11 +341,10 @@ def load_plugin(plugin: Plugin, module_name: str) -> None: raise try: - error_codes = {module_name + '-' + name: msg for name, msg in error_codes.items()} + errors.register_error_codes(module_name, error_codes) except Exception: plugin_error('Error processing result of get_error_codes ({})'.format(module_name)) raise - errors.register_error_codes(error_codes) default_plugin = DefaultPlugin(options) # type: Plugin load_plugin(default_plugin, 'mypy.plugins.default') diff --git a/mypy/errors.py b/mypy/errors.py index b29f15a3caf5..e2c4d2f41eef 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -551,14 +551,15 @@ def remove_duplicates(self, i += 1 return res - def register_error_codes(self, mapping: Dict[str, str]) -> None: + def register_error_codes(self, namespace: str, error_codes: Dict[str, str]) -> None: """Add error codes and their message literals to the list of known errors. Args: - mapping: map of error code to message literal + namespace: used to indentify the source of the error code (usually a plugin) + error_codes: map of error code to message literal """ # reverse the lookup - updates = {msg: name for name, msg in mapping.items()} + updates = {msg: namespace + ':' + name for name, msg in error_codes.items()} # FIXME: check for name clashes self.error_codes.update(updates) @@ -567,7 +568,7 @@ def initialize_error_codes(self) -> None: from mypy.plugins.common import extract_error_codes import mypy.message_registry # read native error codes from the message registry - self.register_error_codes(extract_error_codes(mypy.message_registry)) + self.register_error_codes('mypy', extract_error_codes(mypy.message_registry)) class CompileError(Exception): diff --git a/test-data/unit/check-error-codes.test b/test-data/unit/check-error-codes.test index c221541a4e87..9694c80bee01 100644 --- a/test-data/unit/check-error-codes.test +++ b/test-data/unit/check-error-codes.test @@ -3,14 +3,14 @@ import typing class A: def f(self): pass -A.f = None # E: cannot_assign_to_method: Cannot assign to a method +A.f = None # E: mypy:cannot_assign_to_method: Cannot assign to a method [case testErrorCodesNoReturnValueExpected] # flags: --no-strict-optional --show-error-codes from typing import Generator def f() -> Generator[int, None, None]: yield 1 - return 42 # E: no_return_value_expected: No return value expected + return 42 # E: mypy:no_return_value_expected: No return value expected [out] [case testErrorCodesIncompatibleTypevar] @@ -18,4 +18,4 @@ def f() -> Generator[int, None, None]: from typing import TypeVar, Union AnyStr = TypeVar('AnyStr', bytes, str) def f(x: Union[AnyStr, int], *a: AnyStr) -> None: pass -f('foo', b'bar') # E: incompatible_typevar_value: Value of type variable "AnyStr" of "f" cannot be "object" +f('foo', b'bar') # E: mypy:incompatible_typevar_value: Value of type variable "AnyStr" of "f" cannot be "object" From a00209a5667804480933cd233528f423f38a0895 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 24 Feb 2019 16:10:17 -0800 Subject: [PATCH 4/5] Move handling of format_args to Errors --- mypy/errors.py | 14 +++++++++++--- mypy/messages.py | 9 ++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index e2c4d2f41eef..2560f5451669 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -3,7 +3,7 @@ import traceback from collections import OrderedDict, defaultdict -from typing import Tuple, List, TypeVar, Set, Dict, Optional +from typing import Tuple, List, TypeVar, Set, Dict, Optional, Any from mypy.scope import Scope from mypy.options import Options @@ -249,7 +249,7 @@ def report(self, only_once: bool = False, origin_line: Optional[int] = None, offset: int = 0, - id: Optional[str] = None) -> None: + format_args: Optional[Tuple[Any, ...]] = None) -> None: """Report message at the given line using the current error context. Args: @@ -260,6 +260,7 @@ def report(self, file: if non-None, override current file as context only_once: if True, only report this exact message once per build origin_line: if non-None, override current context as origin + format_args: arguments to pass to message.format() """ if self.scope: type = self.scope.current_type_name() @@ -270,17 +271,24 @@ def report(self, type = None function = None + if self.show_error_codes: + msg_id = self.error_codes.get(message) + else: + msg_id = None + if column is None: column = -1 if file is None: file = self.file if offset: message = " " * offset + message + if format_args: + message = message.format(*format_args) info = ErrorInfo(self.import_context(), file, self.current_module(), type, function, line, column, severity, message, blocker, only_once, origin=(self.file, origin_line) if origin_line else None, - target=self.current_target(), id=id) + target=self.current_target(), id=msg_id) self.add_error_info(info) def _add_error_info(self, file: str, info: ErrorInfo) -> None: diff --git a/mypy/messages.py b/mypy/messages.py index e09bcb7958c4..0699cb406fb8 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -112,19 +112,14 @@ def is_errors(self) -> bool: def report(self, msg: str, format_args: Tuple[Any, ...], context: Optional[Context], severity: str, file: Optional[str] = None, origin: Optional[Context] = None, - offset: int = 0, id: Optional[str] = None) -> None: + offset: int = 0) -> None: """Report an error or note (unless disabled).""" if self.disable_count <= 0: - msg_id = id - if msg_id is None: - msg_id = self.errors.error_codes.get(msg) - if format_args: - msg = msg.format(*format_args) self.errors.report(context.get_line() if context else -1, context.get_column() if context else -1, msg, severity=severity, file=file, offset=offset, origin_line=origin.get_line() if origin else None, - id=msg_id) + format_args=format_args) def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None, origin: Optional[Context] = None) -> None: From 92a40eda689e4ea1cdb8b8b93c50b8aad96b735b Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 24 Feb 2019 16:13:38 -0800 Subject: [PATCH 5/5] rename label used for unknown error codes --- mypy/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 2560f5451669..259a3634ac00 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -392,7 +392,8 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: srcloc = file if self.show_error_codes: s = '{}: {}: {}: {}'.format(srcloc, severity, - id if id is not None else 'unknown', message) + id if id is not None else 'uncategorized_error', + message) else: s = '{}: {}: {}'.format(srcloc, severity, message) else: