Skip to content

Release 0.521 #3762

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

Closed
wants to merge 12 commits into from
Closed
2 changes: 2 additions & 0 deletions docs/source/revision_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Revision history
List of major changes:

- July 2017
* Publish ``mypy`` version 0.521 on PyPI.

* Publish ``mypy`` version 0.520 on PyPI.

* Add :ref:`fine-grained control of Any types <disallow-any>`.
Expand Down
80 changes: 56 additions & 24 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,16 +860,17 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
"""
# TODO: May need to take more build options into account
meta_json, data_json = get_cache_names(id, path, manager)
manager.trace('Looking for {} {}'.format(id, data_json))
manager.trace('Looking for {} at {}'.format(id, meta_json))
if not os.path.exists(meta_json):
manager.trace('Could not load cache for {}: could not find {}'.format(id, meta_json))
manager.log('Could not load cache for {}: could not find {}'.format(id, meta_json))
return None
with open(meta_json, 'r') as f:
meta_str = f.read()
manager.trace('Meta {} {}'.format(id, meta_str.rstrip()))
meta = json.loads(meta_str) # TODO: Errors
if not isinstance(meta, dict):
manager.trace('Could not load cache for {}: meta cache is not a dict'.format(id))
manager.log('Could not load cache for {}: meta cache is not a dict: {}'
.format(id, repr(meta)))
return None
m = CacheMeta(
meta.get('id'),
Expand All @@ -891,27 +892,36 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> Optional[Cache
if (m.id != id or
m.mtime is None or m.size is None or
m.dependencies is None or m.data_mtime is None):
manager.trace('Metadata abandoned for {}: attributes are missing'.format(id))
manager.log('Metadata abandoned for {}: attributes are missing'.format(id))
return None

# Ignore cache if generated by an older mypy version.
if ((m.version_id != manager.version_id and not manager.options.skip_version_check)
or m.options is None
or len(m.dependencies) != len(m.dep_prios)):
manager.trace('Metadata abandoned for {}: new attributes are missing'.format(id))
manager.log('Metadata abandoned for {}: new attributes are missing'.format(id))
return None

# Ignore cache if (relevant) options aren't the same.
# Note that it's fine to mutilate cached_options since it's only used here.
cached_options = m.options
current_options = manager.options.clone_for_module(id).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
if not cached_options.get('platform') and manager.options.skip_version_check:
# Older versions didn't write platform.
cached_options['platform'] = manager.options.platform
if manager.options.skip_version_check:
# When we're lax about version we're also lax about platform.
cached_options['platform'] = current_options['platform']
if 'debug_cache' in cached_options:
# Older versions included debug_cache, but it's silly to compare it.
del cached_options['debug_cache']
if cached_options != current_options:
manager.trace('Metadata abandoned for {}: options differ'.format(id))
manager.log('Metadata abandoned for {}: options differ'.format(id))
if manager.options.verbosity >= 2:
for key in sorted(set(cached_options) | set(current_options)):
if cached_options.get(key) != current_options.get(key):
manager.trace(' {}: {} != {}'
.format(key, cached_options.get(key), current_options.get(key)))
return None

return m
Expand Down Expand Up @@ -948,41 +958,63 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: str,
# we use cache data file mtime to propagate information about changes in the dependencies.

if meta is None:
manager.log('Metadata not found for {}'.format(id))
return None

# Check data_json; assume if its mtime matches it's good.
# TODO: stat() errors
data_mtime = getmtime(meta.data_json)
if data_mtime != meta.data_mtime:
manager.log('Metadata abandoned for {}: data cache is modified'.format(id))
return None

# TODO: Share stat() outcome with find_module()
path = os.path.abspath(path)
st = manager.get_stat(path) # TODO: Errors
if st.st_size != meta.size:
size = st.st_size
if size != meta.size:
manager.log('Metadata abandoned for {}: file {} has different size'.format(id, path))
return None

if int(st.st_mtime) != meta.mtime or path != meta.path:
mtime = int(st.st_mtime)
if mtime != meta.mtime or path != meta.path:
with open(path, 'rb') as f:
source_hash = hashlib.md5(f.read()).hexdigest()
if source_hash != meta.hash:
manager.log('Metadata abandoned for {}: file {} has different hash'.format(id, path))
return None
else:
manager.log('Metadata ok for {}: file {} (match on path, size, hash)'.format(id, path))
# Optimization: update mtime and path (otherwise, this mismatch will reappear).
meta = meta._replace(mtime=int(st.st_mtime), path=path)
meta = meta._replace(mtime=mtime, path=path)
# Construct a dict we can pass to json.dumps() (compare to write_cache()).
meta_dict = {
'id': id,
'path': path,
'mtime': mtime,
'size': size,
'hash': source_hash,
'data_mtime': data_mtime,
'dependencies': meta.dependencies,
'suppressed': meta.suppressed,
'child_modules': meta.child_modules,
'options': (manager.options.clone_for_module(id)
.select_options_affecting_cache()),
'dep_prios': meta.dep_prios,
'interface_hash': meta.interface_hash,
'version_id': manager.version_id,
}
if manager.options.debug_cache:
meta_str = json.dumps(meta, indent=2, sort_keys=True)
meta_str = json.dumps(meta_dict, indent=2, sort_keys=True)
else:
meta_str = json.dumps(meta)
meta_str = json.dumps(meta_dict)
meta_json, _ = get_cache_names(id, path, manager)
manager.log('Updating mtime for {}: file {}, meta {}, mtime {}'
.format(id, path, meta_json, meta.mtime))
atomic_write(meta_json, meta_str) # Ignore errors, since this is just an optimization.
atomic_write(meta_json, meta_str, '\n') # Ignore errors, it's just an optimization.
return meta

# It's a match on (id, path, mtime/hash, size).
# Check data_json; assume if its mtime matches it's good.
# TODO: stat() errors
if getmtime(meta.data_json) != meta.data_mtime:
manager.log('Metadata abandoned for {}: data cache is modified'.format(id))
return None
manager.log('Found {} {} (metadata is fresh)'.format(id, meta.data_json))
# It's a match on (id, path, size, hash, mtime).
manager.log('Metadata fresh for {}: file {}'.format(id, path))
return meta


Expand Down Expand Up @@ -1096,7 +1128,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
meta_str = json.dumps(meta, indent=2, sort_keys=True)
else:
meta_str = json.dumps(meta)
if not atomic_write(meta_json, meta_str):
if not atomic_write(meta_json, meta_str, '\n'):
# Most likely the error is the replace() call
# (see https://github.com/python/mypy/issues/3215).
# The next run will simply find the cache entry out of date.
Expand Down
7 changes: 5 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1413,8 +1413,11 @@ def lvalue_type_from_base(self, expr_node: Var,

if base_type:
if not has_no_typevars(base_type):
# TODO: Handle TupleType, don't cast
instance = cast(Instance, self.scope.active_self_type())
self_type = self.scope.active_self_type()
if isinstance(self_type, TupleType):
instance = self_type.fallback
else:
instance = self_type
itype = map_instance_to_supertype(instance, base)
base_type = expand_type_by_instance(base_type, itype)

Expand Down
2 changes: 1 addition & 1 deletion mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef],
return func_def

def set_type_optional(self, type: Type, initializer: Expression) -> None:
if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL:
if self.options.no_implicit_optional:
return
# Indicate that type should be wrapped in an Optional if arg is initialized to None.
optional = isinstance(initializer, NameExpr) and initializer.name == 'None'
Expand Down
2 changes: 1 addition & 1 deletion mypy/fastparse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def visit_FunctionDef(self, n: ast27.FunctionDef) -> Statement:
return func_def

def set_type_optional(self, type: Type, initializer: Expression) -> None:
if self.options.no_implicit_optional or not experiments.STRICT_OPTIONAL:
if self.options.no_implicit_optional:
return
# Indicate that type should be wrapped in an Optional if arg is initialized to None.
optional = isinstance(initializer, NameExpr) and initializer.name == 'None'
Expand Down
3 changes: 3 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
if stnode is not None:
value.node = stnode.node
value.type_override = stnode.type_override
if (self.quick_and_dirty and value.kind == TYPE_ALIAS and
stnode.type_override is None):
value.type_override = Instance(stale_info(), [])
value.alias_tvars = stnode.alias_tvars or []
elif not self.quick_and_dirty:
assert stnode is not None, "Could not find cross-ref %s" % (cross_ref,)
Expand Down
14 changes: 13 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,18 +2279,22 @@ class SymbolTableNode:
cross_ref = None # type: Optional[str]
# Was this node created by normalіze_type_alias?
normalized = False # type: bool
# Was this defined by assignment to self attribute?
implicit = False # type: bool

def __init__(self, kind: int, node: Optional[SymbolNode], mod_id: str = None,
typ: 'mypy.types.Type' = None,
module_public: bool = True, normalized: bool = False,
alias_tvars: Optional[List[str]] = None) -> None:
alias_tvars: Optional[List[str]] = None,
implicit: bool = False) -> None:
self.kind = kind
self.node = node
self.type_override = typ
self.mod_id = mod_id
self.module_public = module_public
self.normalized = normalized
self.alias_tvars = alias_tvars
self.implicit = implicit

@property
def fullname(self) -> Optional[str]:
Expand Down Expand Up @@ -2334,6 +2338,10 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
} # type: JsonDict
if not self.module_public:
data['module_public'] = False
if self.normalized:
data['normalized'] = True
if self.implicit:
data['implicit'] = True
if self.kind == MODULE_REF:
assert self.node is not None, "Missing module cross ref in %s for %s" % (prefix, name)
data['cross_ref'] = self.node.fullname()
Expand Down Expand Up @@ -2371,6 +2379,10 @@ def deserialize(cls, data: JsonDict) -> 'SymbolTableNode':
stnode.alias_tvars = data['alias_tvars']
if 'module_public' in data:
stnode.module_public = data['module_public']
if 'normalized' in data:
stnode.normalized = data['normalized']
if 'implicit' in data:
stnode.implicit = data['implicit']
return stnode


Expand Down
3 changes: 2 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class Options:
"strict_optional",
}

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

def __init__(self) -> None:
# -- build options --
Expand Down
17 changes: 13 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1809,11 +1809,12 @@ def analyze_member_lvalue(self, lval: MemberExpr) -> None:
lval.is_def = True
v = Var(lval.name)
v.set_line(lval)
v._fullname = self.qualified_name(lval.name)
v.info = self.type
v.is_ready = False
lval.def_var = v
lval.node = v
self.type.names[lval.name] = SymbolTableNode(MDEF, v)
self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True)
self.check_lvalue_validity(lval.node, lval)

def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:
Expand Down Expand Up @@ -3383,6 +3384,7 @@ def tvar_scope_frame(self, frame: TypeVarScope) -> Iterator[None]:

def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
"""Look up an unqualified name in all active namespaces."""
implicit_name = False
# 1a. Name declared using 'global x' takes precedence
if name in self.global_decls[-1]:
if name in self.globals:
Expand All @@ -3400,7 +3402,11 @@ def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
return None
# 2. Class attributes (if within class definition)
if self.is_class_scope() and name in self.type.names:
return self.type.names[name]
node = self.type.names[name]
if not node.implicit:
return node
implicit_name = True
implicit_node = node
# 3. Local (function) scopes
for table in reversed(self.locals):
if table is not None and name in table:
Expand All @@ -3420,8 +3426,11 @@ def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
node = table[name]
return node
# Give up.
self.name_not_defined(name, ctx)
self.check_for_obsolete_short_name(name, ctx)
if not implicit_name:
self.name_not_defined(name, ctx)
self.check_for_obsolete_short_name(name, ctx)
else:
return implicit_node
return None

def check_for_obsolete_short_name(self, name: str, ctx: Context) -> None:
Expand Down
2 changes: 1 addition & 1 deletion mypy/version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from mypy import git

__version__ = '0.520-dev'
__version__ = '0.521'
base_version = __version__

mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
Expand Down
43 changes: 39 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,43 @@ class A:
main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
main:8: error: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testClassNamesDefinedOnSelUsedInClassBody]
class A(object):
def f(self):
self.attr = 1
attr = 0

class B(object):
attr = 0
def f(self):
self.attr = 1

class C(object):
attr = 0
def f(self):
self.attr = 1
attr = 0

class D(object):
def g(self):
self.attr = 1
attr = 0
def f(self):
self.attr = 1
[out]

[case testClassNamesDefinedOnSelUsedInClassBodyReveal]
class A(object):
def f(self) -> None:
self.attr = 1
reveal_type(attr) # E: Revealed type is 'builtins.int'

class B(object):
attr = 0
def f(self) -> None:
reveal_type(self.attr) # E: Revealed type is 'builtins.int'
[out]


-- Method overriding
-- -----------------
Expand Down Expand Up @@ -3386,9 +3423,7 @@ NT([])
[builtins fixtures/dict.pyi]
[out]

-- The two tests below will not crash after
-- https://github.com/python/mypy/issues/3319 is fixed
[case testCrashForwardSyntheticClassSyntax-skip]
[case testCrashForwardSyntheticClassSyntax]
from typing import NamedTuple
from mypy_extensions import TypedDict
class A1(NamedTuple):
Expand All @@ -3406,7 +3441,7 @@ reveal_type(y['b']) # E: Revealed type is '__main__.B'
[builtins fixtures/dict.pyi]
[out]

[case testCrashForwardSyntheticFunctionSyntax-skip]
[case testCrashForwardSyntheticFunctionSyntax]
from typing import NamedTuple
from mypy_extensions import TypedDict
A1 = NamedTuple('A1', [('b', 'B'), ('x', int)])
Expand Down
Loading