Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into fix-nested-deferral
Browse files Browse the repository at this point in the history
  • Loading branch information
ilevkivskyi committed Feb 21, 2025
2 parents 9b9354a + 256cf68 commit 7f2cc68
Show file tree
Hide file tree
Showing 93 changed files with 1,253 additions and 466 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
# the oldest and newest supported Python versions
- name: Test suite with py39-ubuntu, mypyc-compiled
python: '3.9'
os: ubuntu-24.04-arm
os: ubuntu-22.04-arm
toxenv: py
tox_extra_args: "-n 4"
test_mypyc: true
Expand All @@ -44,31 +44,31 @@ jobs:
tox_extra_args: "-n 4"
- name: Test suite with py310-ubuntu
python: '3.10'
os: ubuntu-24.04-arm
os: ubuntu-22.04-arm
toxenv: py
tox_extra_args: "-n 4"
- name: Test suite with py311-ubuntu, mypyc-compiled
python: '3.11'
os: ubuntu-24.04-arm
os: ubuntu-22.04-arm
toxenv: py
tox_extra_args: "-n 4"
test_mypyc: true
- name: Test suite with py312-ubuntu, mypyc-compiled
python: '3.12'
os: ubuntu-24.04-arm
os: ubuntu-22.04-arm
toxenv: py
tox_extra_args: "-n 4"
test_mypyc: true
- name: Test suite with py313-ubuntu, mypyc-compiled
python: '3.13'
os: ubuntu-24.04-arm
os: ubuntu-22.04-arm
toxenv: py
tox_extra_args: "-n 4"
test_mypyc: true

# - name: Test suite with py314-dev-ubuntu
# python: '3.14-dev'
# os: ubuntu-24.04-arm
# os: ubuntu-22.04-arm
# toxenv: py
# tox_extra_args: "-n 4"
# allow_failure: true
Expand Down
2 changes: 1 addition & 1 deletion CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Dropbox core team:

Non-Dropbox core team members:

Ethan Smith
Emma Harper Smith <[email protected]>
Guido van Rossum <[email protected]>
Jelle Zijlstra <[email protected]>
Michael J. Sullivan <[email protected]>
Expand Down
4 changes: 4 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ for full details, see :ref:`running-mypy`.
never recursively discover files with extensions other than ``.py`` or
``.pyi``.

.. option:: --exclude-gitignore

This flag will add everything that matches ``.gitignore`` file(s) to :option:`--exclude`.


Optional arguments
******************
Expand Down
8 changes: 8 additions & 0 deletions docs/source/config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ section of the command line docs.
See :ref:`using-a-pyproject-toml`.

.. confval:: exclude_gitignore

:type: boolean
:default: False

This flag will add everything that matches ``.gitignore`` file(s) to :confval:`exclude`.
This option may only be set in the global section (``[mypy]``).

.. confval:: namespace_packages

:type: boolean
Expand Down
12 changes: 7 additions & 5 deletions docs/source/metaclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ Mypy supports the lookup of attributes in the metaclass:

.. code-block:: python
from typing import ClassVar, Self
from typing import ClassVar, TypeVar
S = TypeVar("S")
class M(type):
count: ClassVar[int] = 0
def make(cls) -> Self:
def make(cls: type[S]) -> S:
M.count += 1
return cls()
Expand All @@ -55,9 +57,6 @@ Mypy supports the lookup of attributes in the metaclass:
b: B = B.make() # metaclasses are inherited
print(B.count + " objects were created") # Error: Unsupported operand types for + ("int" and "str")
.. note::
In Python 3.10 and earlier, ``Self`` is available in ``typing_extensions``.

.. _limitations:

Gotchas and limitations of metaclass support
Expand Down Expand Up @@ -88,3 +87,6 @@ so it's better not to combine metaclasses and class hierarchies:
such as ``class A(metaclass=f()): ...``
* Mypy does not and cannot understand arbitrary metaclass code.
* Mypy only recognizes subclasses of :py:class:`type` as potential metaclasses.
* ``Self`` is not allowed as annotation in metaclasses as per `PEP 673`_.

.. _PEP 673: https://peps.python.org/pep-0673/#valid-locations-for-self
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
From b5f2cc9633f9f6cd9326eee96a32efb3aff70701 Mon Sep 17 00:00:00 2001
From: Marc Mueller <[email protected]>
Date: Sat, 15 Feb 2025 20:11:06 +0100
Subject: [PATCH] Partially revert Clean up argparse hacks

---
mypy/typeshed/stdlib/argparse.pyi | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/mypy/typeshed/stdlib/argparse.pyi b/mypy/typeshed/stdlib/argparse.pyi
index 029bfeefe..9dbd8c308 100644
--- a/mypy/typeshed/stdlib/argparse.pyi
+++ b/mypy/typeshed/stdlib/argparse.pyi
@@ -2,7 +2,7 @@ import sys
from _typeshed import SupportsWrite, sentinel
from collections.abc import Callable, Generator, Iterable, Sequence
from re import Pattern
-from typing import IO, Any, ClassVar, Final, Generic, NoReturn, Protocol, TypeVar, overload
+from typing import IO, Any, ClassVar, Final, Generic, NewType, NoReturn, Protocol, TypeVar, overload
from typing_extensions import Self, TypeAlias, deprecated

__all__ = [
@@ -38,7 +38,9 @@ ONE_OR_MORE: Final = "+"
OPTIONAL: Final = "?"
PARSER: Final = "A..."
REMAINDER: Final = "..."
-SUPPRESS: Final = "==SUPPRESS=="
+_SUPPRESS_T = NewType("_SUPPRESS_T", str)
+SUPPRESS: _SUPPRESS_T | str # not using Literal because argparse sometimes compares SUPPRESS with is
+# the | str is there so that foo = argparse.SUPPRESS; foo = "test" checks out in mypy
ZERO_OR_MORE: Final = "*"
_UNRECOGNIZED_ARGS_ATTR: Final = "_unrecognized_args" # undocumented

@@ -81,7 +83,7 @@ class _ActionsContainer:
# more precisely, Literal["?", "*", "+", "...", "A...", "==SUPPRESS=="],
# but using this would make it hard to annotate callers that don't use a
# literal argument and for subclasses to override this method.
- nargs: int | str | None = None,
+ nargs: int | str | _SUPPRESS_T | None = None,
const: Any = ...,
default: Any = ...,
type: _ActionType = ...,
--
2.48.1

9 changes: 3 additions & 6 deletions misc/upload-pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ def item_ok_for_pypi(name: str) -> bool:
if not is_whl_or_tar(name):
return False

if name.endswith(".tar.gz"):
name = name[:-7]
if name.endswith(".whl"):
name = name[:-4]
name = name.removesuffix(".tar.gz")
name = name.removesuffix(".whl")

if name.endswith("wasm32"):
return False
Expand Down Expand Up @@ -123,8 +121,7 @@ def upload_to_pypi(version: str, dry_run: bool = True) -> None:
assert re.match(r"v?[1-9]\.[0-9]+\.[0-9](\+\S+)?$", version)
if "dev" in version:
assert dry_run, "Must use --dry-run with dev versions of mypy"
if version.startswith("v"):
version = version[1:]
version = version.removeprefix("v")

target_dir = tempfile.mkdtemp()
dist = Path(target_dir) / "dist"
Expand Down
1 change: 1 addition & 0 deletions mypy-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
# and the pins in setup.py
typing_extensions>=4.6.0
mypy_extensions>=1.0.0
pathspec>=0.9.0
tomli>=1.1.0; python_version<'3.11'
4 changes: 2 additions & 2 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,7 @@ def read_plugins_snapshot(manager: BuildManager) -> dict[str, str] | None:
if snapshot is None:
return None
if not isinstance(snapshot, dict):
manager.log(f"Could not load plugins snapshot: cache is not a dict: {type(snapshot)}")
manager.log(f"Could not load plugins snapshot: cache is not a dict: {type(snapshot)}") # type: ignore[unreachable]
return None
return snapshot

Expand Down Expand Up @@ -1285,7 +1285,7 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> CacheMeta | No
if meta is None:
return None
if not isinstance(meta, dict):
manager.log(f"Could not load cache for {id}: meta cache is not a dict: {repr(meta)}")
manager.log(f"Could not load cache for {id}: meta cache is not a dict: {repr(meta)}") # type: ignore[unreachable]
return None
m = cache_meta_from_dict(meta, data_json)
t2 = time.time()
Expand Down
54 changes: 31 additions & 23 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
LITERAL_TYPE,
MDEF,
NOT_ABSTRACT,
SYMBOL_FUNCBASE_TYPES,
AssertStmt,
AssignmentExpr,
AssignmentStmt,
Expand Down Expand Up @@ -215,22 +216,22 @@

T = TypeVar("T")

DEFAULT_LAST_PASS: Final = 1 # Pass numbers start at 0
DEFAULT_LAST_PASS: Final = 2 # Pass numbers start at 0

# Maximum length of fixed tuple types inferred when narrowing from variadic tuples.
MAX_PRECISE_TUPLE_SIZE: Final = 8

DeferredNodeType: _TypeAlias = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
DeferredNodeType: _TypeAlias = Union[FuncDef, OverloadedFuncDef, Decorator]
FineGrainedDeferredNodeType: _TypeAlias = Union[FuncDef, MypyFile, OverloadedFuncDef]


# A node which is postponed to be processed during the next pass.
# In normal mode one can defer functions and methods (also decorated and/or overloaded)
# and lambda expressions. Nested functions can't be deferred -- only top-level functions
# but not lambda expressions. Nested functions can't be deferred -- only top-level functions
# and methods of classes not defined within a function can be deferred.
class DeferredNode(NamedTuple):
node: DeferredNodeType
# And its TypeInfo (for semantic analysis self type handling
# And its TypeInfo (for semantic analysis self type handling)
active_typeinfo: TypeInfo | None


Expand Down Expand Up @@ -527,10 +528,7 @@ def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) ->
else:
self.recurse_into_functions = True
with self.binder.top_frame_context():
if isinstance(node, LambdaExpr):
self.expr_checker.accept(node)
else:
self.accept(node)
self.accept(node)

def check_top_level(self, node: MypyFile) -> None:
"""Check only the top-level of a module, skipping function definitions."""
Expand All @@ -557,13 +555,13 @@ def defer_node(self, node: DeferredNodeType, enclosing_class: TypeInfo | None) -
self.deferred_nodes.append(DeferredNode(node, enclosing_class))

def handle_cannot_determine_type(self, name: str, context: Context) -> None:
node = self.scope.top_non_lambda_function()
node = self.scope.top_level_function()
if self.pass_num < self.last_pass and isinstance(node, FuncDef):
# Don't report an error yet. Just defer. Note that we don't defer
# lambdas because they are coupled to the surrounding function
# through the binder and the inferred type of the lambda, so it
# would get messy.
enclosing_class = self.scope.enclosing_class()
enclosing_class = self.scope.enclosing_class(node)
self.defer_node(node, enclosing_class)
# Set a marker so that we won't infer additional types in this
# function. Any inferred types could be bogus, because there's at
Expand Down Expand Up @@ -2156,7 +2154,14 @@ def check_method_override_for_base_with_name(
if self.pass_num < self.last_pass:
# If there are passes left, defer this node until next pass,
# otherwise try reconstructing the method type from available information.
self.defer_node(defn, defn.info)
# For consistency, defer an enclosing top-level function (if any).
top_level = self.scope.top_level_function()
if isinstance(top_level, FuncDef):
self.defer_node(top_level, self.scope.enclosing_class(top_level))
else:
# Specify enclosing class explicitly, as we check type override before
# entering e.g. decorators or overloads.
self.defer_node(defn, defn.info)
return True
elif isinstance(original_node, (FuncDef, OverloadedFuncDef)):
original_type = self.function_type(original_node)
Expand Down Expand Up @@ -2866,7 +2871,7 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None:
if sym.type is not None:
return sym.type
if isinstance(sym.node, FuncBase):
if isinstance(sym.node, SYMBOL_FUNCBASE_TYPES):
return self.function_type(sym.node)
if isinstance(sym.node, TypeInfo):
if sym.node.typeddict_type:
Expand Down Expand Up @@ -4460,7 +4465,9 @@ def simple_rvalue(self, rvalue: Expression) -> bool:
if isinstance(rvalue, (IntExpr, StrExpr, BytesExpr, FloatExpr, RefExpr)):
return True
if isinstance(rvalue, CallExpr):
if isinstance(rvalue.callee, RefExpr) and isinstance(rvalue.callee.node, FuncBase):
if isinstance(rvalue.callee, RefExpr) and isinstance(
rvalue.callee.node, SYMBOL_FUNCBASE_TYPES
):
typ = rvalue.callee.node.type
if isinstance(typ, CallableType):
return not typ.variables
Expand Down Expand Up @@ -4765,7 +4772,7 @@ def visit_return_stmt(self, s: ReturnStmt) -> None:
self.binder.unreachable()

def check_return_stmt(self, s: ReturnStmt) -> None:
defn = self.scope.top_function()
defn = self.scope.current_function()
if defn is not None:
if defn.is_generator:
return_type = self.get_generator_return_type(
Expand All @@ -4777,7 +4784,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
return_type = self.return_types[-1]
return_type = get_proper_type(return_type)

is_lambda = isinstance(self.scope.top_function(), LambdaExpr)
is_lambda = isinstance(defn, LambdaExpr)
if isinstance(return_type, UninhabitedType):
# Avoid extra error messages for failed inference in lambdas
if not is_lambda and not return_type.ambiguous:
Expand Down Expand Up @@ -8553,14 +8560,15 @@ class CheckerScope:
def __init__(self, module: MypyFile) -> None:
self.stack = [module]

def top_function(self) -> FuncItem | None:
def current_function(self) -> FuncItem | None:
for e in reversed(self.stack):
if isinstance(e, FuncItem):
return e
return None

def top_non_lambda_function(self) -> FuncItem | None:
for e in reversed(self.stack):
def top_level_function(self) -> FuncItem | None:
"""Return top-level non-lambda function."""
for e in self.stack:
if isinstance(e, FuncItem) and not isinstance(e, LambdaExpr):
return e
return None
Expand All @@ -8570,11 +8578,11 @@ def active_class(self) -> TypeInfo | None:
return self.stack[-1]
return None

def enclosing_class(self) -> TypeInfo | None:
def enclosing_class(self, func: FuncItem | None = None) -> TypeInfo | None:
"""Is there a class *directly* enclosing this function?"""
top = self.top_function()
assert top, "This method must be called from inside a function"
index = self.stack.index(top)
func = func or self.current_function()
assert func, "This method must be called from inside a function"
index = self.stack.index(func)
assert index, "CheckerScope stack must always start with a module"
enclosing = self.stack[index - 1]
if isinstance(enclosing, TypeInfo):
Expand All @@ -8588,7 +8596,7 @@ def active_self_type(self) -> Instance | TupleType | None:
In particular, inside a function nested in method this returns None.
"""
info = self.active_class()
if not info and self.top_function():
if not info and self.current_function():
info = self.enclosing_class()
if info:
return fill_typevars(info)
Expand Down
6 changes: 2 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2580,8 +2580,6 @@ def check_argument_types(
for actual, actual_type, actual_kind, callee_arg_type, callee_arg_kind in zip(
actuals, actual_types, actual_kinds, callee_arg_types, callee_arg_kinds
):
if actual_type is None:
continue # Some kind of error was already reported.
# Check that a *arg is valid as varargs.
expanded_actual = mapper.expand_actual_type(
actual_type,
Expand Down Expand Up @@ -5525,7 +5523,7 @@ def visit_super_expr(self, e: SuperExpr) -> Type:
if type_info in mro:
index = mro.index(type_info)
else:
method = self.chk.scope.top_function()
method = self.chk.scope.current_function()
# Mypy explicitly allows supertype upper bounds (and no upper bound at all)
# for annotating self-types. However, if such an annotation is used for
# checking super() we will still get an error. So to be consistent, we also
Expand Down Expand Up @@ -5600,7 +5598,7 @@ def _super_arg_types(self, e: SuperExpr) -> Type | tuple[Type, Type]:
type_type: ProperType = TypeType(current_type)

# Use the type of the self argument, in case it was annotated
method = self.chk.scope.top_function()
method = self.chk.scope.current_function()
assert method is not None
if method.arguments:
instance_type: Type = method.arguments[0].variable.type or current_type
Expand Down
Loading

0 comments on commit 7f2cc68

Please sign in to comment.