Skip to content

Make Options work under mypyc #5518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from mypy.find_sources import create_source_list, InvalidSourceList
from mypy.fscache import FileSystemCache
from mypy.errors import CompileError
from mypy.options import Options, BuildType
from mypy.options import Options, BuildType, PER_MODULE_OPTIONS
from mypy.report import reporter_classes

from mypy.version import __version__
Expand Down Expand Up @@ -1014,11 +1014,11 @@ def parse_config_file(options: Options, filename: Optional[str]) -> None:
print("%s: Per-module sections should not specify reports (%s)" %
(prefix, ', '.join(s + '_report' for s in sorted(report_dirs))),
file=sys.stderr)
if set(updates) - Options.PER_MODULE_OPTIONS:
if set(updates) - PER_MODULE_OPTIONS:
print("%s: Per-module sections should only specify per-module flags (%s)" %
(prefix, ', '.join(sorted(set(updates) - Options.PER_MODULE_OPTIONS))),
(prefix, ', '.join(sorted(set(updates) - PER_MODULE_OPTIONS))),
file=sys.stderr)
updates = {k: v for k, v in updates.items() if k in Options.PER_MODULE_OPTIONS}
updates = {k: v for k, v in updates.items() if k in PER_MODULE_OPTIONS}
globs = name[5:]
for glob in globs.split(','):
# For backwards compatibility, replace (back)slashes with dots.
Expand Down
103 changes: 54 additions & 49 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,59 @@
import sys

from typing import Dict, List, Mapping, Optional, Pattern, Set, Tuple
MYPY = False
if MYPY:
from typing import ClassVar

from mypy import defaults
from mypy.util import get_class_descriptors, replace_object_state


class BuildType:
STANDARD = 0
MODULE = 1
PROGRAM_TEXT = 2
STANDARD = 0 # type: ClassVar[int]
MODULE = 1 # type: ClassVar[int]
PROGRAM_TEXT = 2 # type: ClassVar[int]


PER_MODULE_OPTIONS = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a short comment why PER_MODULE_OPTIONS and OPTIONS_AFFECTING_CACHE can't be ClassVars in Options (similar to what you did for fastparse.py IIRC)?

# Please keep this list sorted
"always_false",
"always_true",
"check_untyped_defs",
"debug_cache",
"disallow_any_decorated",
"disallow_any_explicit",
"disallow_any_expr",
"disallow_any_generics",
"disallow_any_unimported",
"disallow_incomplete_defs",
"disallow_subclassing_any",
"disallow_untyped_calls",
"disallow_untyped_decorators",
"disallow_untyped_defs",
"follow_imports",
"follow_imports_for_stubs",
"ignore_errors",
"ignore_missing_imports",
"local_partial_types",
"no_implicit_optional",
"show_none_errors",
"strict_boolean",
"strict_optional",
"strict_optional_whitelist",
"warn_no_return",
"warn_return_any",
"warn_unused_ignores",
}

OPTIONS_AFFECTING_CACHE = ((PER_MODULE_OPTIONS |
{"quick_and_dirty", "platform", "bazel"})
- {"debug_cache"})


class Options:
"""Options collected from flags."""

PER_MODULE_OPTIONS = {
# Please keep this list sorted
"always_false",
"always_true",
"check_untyped_defs",
"debug_cache",
"disallow_any_decorated",
"disallow_any_explicit",
"disallow_any_expr",
"disallow_any_generics",
"disallow_any_unimported",
"disallow_incomplete_defs",
"disallow_subclassing_any",
"disallow_untyped_calls",
"disallow_untyped_decorators",
"disallow_untyped_defs",
"follow_imports",
"follow_imports_for_stubs",
"ignore_errors",
"ignore_missing_imports",
"local_partial_types",
"no_implicit_optional",
"show_none_errors",
"strict_boolean",
"strict_optional",
"strict_optional_whitelist",
"warn_no_return",
"warn_return_any",
"warn_unused_ignores",
}

OPTIONS_AFFECTING_CACHE = ((PER_MODULE_OPTIONS |
{"quick_and_dirty", "platform", "bazel"})
- {"debug_cache"})

def __init__(self) -> None:
# Cache for clone_for_module()
self.per_module_cache = None # type: Optional[Dict[str, Options]]
Expand Down Expand Up @@ -209,23 +214,23 @@ def __init__(self) -> None:

def snapshot(self) -> object:
"""Produce a comparable snapshot of this Option"""
d = dict(self.__dict__)
# Under mypyc, we don't have a __dict__, so we need to do worse things.
d = dict(getattr(self, '__dict__', ()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So IIUC under mypyc d will start out empty?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

for k in get_class_descriptors(Options):
if hasattr(self, k):
d[k] = getattr(self, k)
del d['per_module_cache']
return d

def __eq__(self, other: object) -> bool:
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__

def __ne__(self, other: object) -> bool:
return not self == other

def __repr__(self) -> str:
return 'Options({})'.format(pprint.pformat(self.snapshot()))

def apply_changes(self, changes: Dict[str, object]) -> 'Options':
new_options = Options()
new_options.__dict__.update(self.__dict__)
new_options.__dict__.update(changes)
# Under mypyc, we don't have a __dict__, so we need to do worse things.
replace_object_state(new_options, self, copy_dict=True)
for key, value in changes.items():
setattr(new_options, key, value)
return new_options

def build_per_module_cache(self) -> None:
Expand Down Expand Up @@ -281,7 +286,7 @@ def clone_for_module(self, module: str) -> 'Options':
"""
if self.per_module_cache is None:
self.build_per_module_cache()
assert self.per_module_cache is not None
assert self.per_module_cache is not None

# If the module just directly has a config entry, use it.
if module in self.per_module_cache:
Expand Down Expand Up @@ -327,4 +332,4 @@ def compile_glob(self, s: str) -> Pattern[str]:
return re.compile(expr + '\\Z')

def select_options_affecting_cache(self) -> Mapping[str, object]:
return {opt: getattr(self, opt) for opt in self.OPTIONS_AFFECTING_CACHE}
return {opt: getattr(self, opt) for opt in OPTIONS_AFFECTING_CACHE}
2 changes: 1 addition & 1 deletion mypy/test/testargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_coherence(self) -> None:
_, parsed_options = process_options([], require_targets=False)
# FIX: test this too. Requires changing working dir to avoid finding 'setup.cfg'
options.config_file = parsed_options.config_file
assert_equal(options, parsed_options)
assert_equal(options.snapshot(), parsed_options.snapshot())

def test_executable_inference(self) -> None:
"""Test the --python-executable flag with --python-version"""
Expand Down
6 changes: 1 addition & 5 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1743,11 +1743,7 @@ def copy_type(t: Type) -> Type:
# and copying everything in with replace_object_state.
typ = type(t)
nt = typ.__new__(typ)
replace_object_state(nt, t)
# replace_object_state leaves the new object with the same
# __dict__ as the old, so make a copy.
if hasattr(nt, '__dict__'):
nt.__dict__ = nt.__dict__.copy()
replace_object_state(nt, t, copy_dict=True)
return nt


Expand Down
28 changes: 17 additions & 11 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,18 @@ def correct_relative_import(cur_mod_id: str,
fields_cache = {} # type: Dict[Type[object], List[str]]


def replace_object_state(new: object, old: object) -> None:
def get_class_descriptors(cls: 'Type[object]') -> Sequence[str]:
# Maintain a cache of type -> attributes defined by descriptors in the class
# (that is, attributes from __slots__ and C extension classes)
if cls not in fields_cache:
members = inspect.getmembers(
cls,
lambda o: inspect.isgetsetdescriptor(o) or inspect.ismemberdescriptor(o))
fields_cache[cls] = [x for x, y in members if x != '__weakref__']
return fields_cache[cls]


def replace_object_state(new: object, old: object, copy_dict: bool=False) -> None:
"""Copy state of old node to the new node.

This handles cases where there is __dict__ and/or attribute descriptors
Expand All @@ -205,17 +216,12 @@ def replace_object_state(new: object, old: object) -> None:
Assume that both objects have the same __class__.
"""
if hasattr(old, '__dict__'):
new.__dict__ = old.__dict__
if copy_dict:
new.__dict__ = dict(old.__dict__)
else:
new.__dict__ = old.__dict__

cls = old.__class__
# Maintain a cache of type -> attributes defined by descriptors in the class
# (that is, attributes from __slots__ and C extension classes)
if cls not in fields_cache:
members = inspect.getmembers(
cls,
lambda o: inspect.isgetsetdescriptor(o) or inspect.ismemberdescriptor(o))
fields_cache[cls] = [x for x, y in members if x != '__weakref__']
for attr in fields_cache[cls]:
for attr in get_class_descriptors(old.__class__):
try:
if hasattr(old, attr):
setattr(new, attr, getattr(old, attr))
Expand Down