Skip to content

Commit

Permalink
Merge branch 'master' into fix-typos-in-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelg100 authored Jul 9, 2024
2 parents d5c2564 + 3c9f694 commit d3b99fe
Show file tree
Hide file tree
Showing 40 changed files with 641 additions and 133 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
with:
python-version: '3.8'
- name: Install tox
run: pip install --upgrade 'setuptools!=50' tox==4.11.0
run: pip install tox==4.11.0
- name: Setup tox environment
run: tox run -e ${{ env.TOXENV }} --notest
- name: Test
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ jobs:
- name: Test
run: tox run -e ${{ matrix.toxenv }} --skip-pkg-install -- ${{ matrix.tox_extra_args }}

python-nightly:
runs-on: ubuntu-latest
name: Test suite with Python nightly
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.13-dev'
- name: Install tox
run: pip install setuptools==68.2.2 tox==4.11.0
- name: Setup tox environment
run: tox run -e py --notest
- name: Test
run: tox run -e py --skip-pkg-install -- "-n 4"
continue-on-error: true
- name: Mark as a success
run: exit 0

python_32bits:
runs-on: ubuntu-latest
name: Test mypyc suite with 32-bit Python
Expand Down
21 changes: 21 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,27 @@ List of currently incomplete/experimental features:
# Without PreciseTupleTypes: tuple[int, ...]
# With PreciseTupleTypes: tuple[()] | tuple[int] | tuple[int, int]
* ``NewGenericSyntax``: this feature enables support for syntax defined
by :pep:`695`. For example:

.. code-block:: python
class Container[T]: # defines a generic class
content: T
def first[T](items: list[T]) -> T: # defines a generic function
return items[0]
type Items[T] = list[tuple[T, T]] # defines a generic type alias
* ``InlineTypedDict``: this feature enables non-standard syntax for inline
:ref:`TypedDicts <typeddict>`, for example:

.. code-block:: python
def test_values() -> {"int": int, "str": str}:
return {"int": 42, "str": "test"}
Miscellaneous
*************
Expand Down
16 changes: 10 additions & 6 deletions docs/source/type_narrowing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,18 @@ Limitations
Mypy's analysis is limited to individual symbols and it will not track
relationships between symbols. For example, in the following code
it's easy to deduce that if :code:`a` is None then :code:`b` must not be,
therefore :code:`a or b` will always be a string, but Mypy will not be able to tell that:
therefore :code:`a or b` will always be an instance of :code:`C`,
but Mypy will not be able to tell that:

.. code-block:: python
def f(a: str | None, b: str | None) -> str:
class C:
pass
def f(a: C | None, b: C | None) -> C:
if a is not None or b is not None:
return a or b # Incompatible return value type (got "str | None", expected "str")
return 'spam'
return a or b # Incompatible return value type (got "C | None", expected "C")
return C()
Tracking these sort of cross-variable conditions in a type checker would add significant complexity
and performance overhead.
Expand All @@ -385,9 +389,9 @@ or rewrite the function to be slightly more verbose:

.. code-block:: python
def f(a: str | None, b: str | None) -> str:
def f(a: C | None, b: C | None) -> C:
if a is not None:
return a
elif b is not None:
return b
return 'spam'
return C()
38 changes: 38 additions & 0 deletions docs/source/typed_dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,41 @@ section of the docs has a full description with an example, but in short, you wi
need to give each TypedDict the same key where each value has a unique
:ref:`Literal type <literal_types>`. Then, check that key to distinguish
between your TypedDicts.

Inline TypedDict types
----------------------

.. note::

This is an experimental (non-standard) feature. Use
``--enable-incomplete-feature=InlineTypedDict`` to enable.

Sometimes you may want to define a complex nested JSON schema, or annotate
a one-off function that returns a TypedDict. In such cases it may be convenient
to use inline TypedDict syntax. For example:

.. code-block:: python
def test_values() -> {"int": int, "str": str}:
return {"int": 42, "str": "test"}
class Response(TypedDict):
status: int
msg: str
# Using inline syntax here avoids defining two additional TypedDicts.
content: {"items": list[{"key": str, "value": str}]}
Inline TypedDicts can also by used as targets of type aliases, but due to
ambiguity with a regular variables it is only allowed for (newer) explicit
type alias forms:

.. code-block:: python
from typing import TypeAlias
X = {"a": int, "b": int} # creates a variable with type dict[str, type[int]]
Y: TypeAlias = {"a": int, "b": int} # creates a type alias
type Z = {"a": int, "b": int} # same as above (Python 3.12+ only)
Also, due to incompatibility with runtime type-checking it is strongly recommended
to *not* use inline syntax in union types.
6 changes: 4 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2971,7 +2971,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
self.msg.annotation_in_unchecked_function(context=s)

def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
alias_type = self.expr_checker.accept(s.rvalue)
with self.msg.filter_errors():
alias_type = self.expr_checker.accept(s.rvalue)
self.store_type(s.lvalues[-1], alias_type)

def check_assignment(
Expand Down Expand Up @@ -5311,7 +5312,8 @@ def remove_capture_conflicts(self, type_map: TypeMap, inferred_types: dict[Var,
del type_map[expr]

def visit_type_alias_stmt(self, o: TypeAliasStmt) -> None:
self.expr_checker.accept(o.value)
with self.msg.filter_errors():
self.expr_checker.accept(o.value)

def make_fake_typeinfo(
self,
Expand Down
31 changes: 29 additions & 2 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

from mypy.fastparse import parse_type_string
from mypy.nodes import (
MISSING_FALLBACK,
BytesExpr,
CallExpr,
ComplexExpr,
DictExpr,
EllipsisExpr,
Expression,
FloatExpr,
Expand All @@ -29,9 +31,11 @@
AnyType,
CallableArgument,
EllipsisType,
Instance,
ProperType,
RawExpressionType,
Type,
TypedDictType,
TypeList,
TypeOfAny,
UnboundType,
Expand All @@ -55,7 +59,7 @@ def _extract_argument_name(expr: Expression) -> str | None:

def expr_to_unanalyzed_type(
expr: Expression,
options: Options | None = None,
options: Options,
allow_new_syntax: bool = False,
_parent: Expression | None = None,
allow_unpack: bool = False,
Expand All @@ -67,6 +71,8 @@ def expr_to_unanalyzed_type(
If allow_new_syntax is True, allow all type syntax independent of the target
Python version (used in stubs).
# TODO: a lot of code here is duplicated in fastparse.py, refactor this.
"""
# The `parent` parameter is used in recursive calls to provide context for
# understanding whether an CallableArgument is ok.
Expand Down Expand Up @@ -116,7 +122,7 @@ def expr_to_unanalyzed_type(
elif (
isinstance(expr, OpExpr)
and expr.op == "|"
and ((options and options.python_version >= (3, 10)) or allow_new_syntax)
and ((options.python_version >= (3, 10)) or allow_new_syntax)
):
return UnionType(
[
Expand Down Expand Up @@ -206,5 +212,26 @@ def expr_to_unanalyzed_type(
return UnpackType(
expr_to_unanalyzed_type(expr.expr, options, allow_new_syntax), from_star_syntax=True
)
elif isinstance(expr, DictExpr):
if not expr.items:
raise TypeTranslationError()
items: dict[str, Type] = {}
extra_items_from = []
for item_name, value in expr.items:
if not isinstance(item_name, StrExpr):
if item_name is None:
extra_items_from.append(
expr_to_unanalyzed_type(value, options, allow_new_syntax, expr)
)
continue
raise TypeTranslationError()
items[item_name.value] = expr_to_unanalyzed_type(
value, options, allow_new_syntax, expr
)
result = TypedDictType(
items, set(), Instance(MISSING_FALLBACK, ()), expr.line, expr.column
)
result.extra_items_from = extra_items_from
return result
else:
raise TypeTranslationError()
20 changes: 18 additions & 2 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ARG_POS,
ARG_STAR,
ARG_STAR2,
MISSING_FALLBACK,
PARAM_SPEC_KIND,
TYPE_VAR_KIND,
TYPE_VAR_TUPLE_KIND,
Expand All @@ -42,7 +43,6 @@
EllipsisExpr,
Expression,
ExpressionStmt,
FakeInfo,
FloatExpr,
ForStmt,
FuncDef,
Expand Down Expand Up @@ -116,6 +116,7 @@
RawExpressionType,
TupleType,
Type,
TypedDictType,
TypeList,
TypeOfAny,
UnboundType,
Expand Down Expand Up @@ -190,7 +191,6 @@ def ast3_parse(

# There is no way to create reasonable fallbacks at this stage,
# they must be patched later.
MISSING_FALLBACK: Final = FakeInfo("fallback can't be filled out until semanal")
_dummy_fallback: Final = Instance(MISSING_FALLBACK, [], -1)

TYPE_IGNORE_PATTERN: Final = re.compile(r"[^#]*#\s*type:\s*ignore\s*(.*)")
Expand Down Expand Up @@ -2106,6 +2106,22 @@ def visit_Tuple(self, n: ast3.Tuple) -> Type:
column=self.convert_column(n.col_offset),
)

def visit_Dict(self, n: ast3.Dict) -> Type:
if not n.keys:
return self.invalid_type(n)
items: dict[str, Type] = {}
extra_items_from = []
for item_name, value in zip(n.keys, n.values):
if not isinstance(item_name, ast3.Constant) or not isinstance(item_name.value, str):
if item_name is None:
extra_items_from.append(self.visit(value))
continue
return self.invalid_type(n)
items[item_name.value] = self.visit(value)
result = TypedDictType(items, set(), _dummy_fallback, n.lineno, n.col_offset)
result.extra_items_from = extra_items_from
return result

# Attribute(expr value, identifier attr, expr_context ctx)
def visit_Attribute(self, n: Attribute) -> Type:
before_dot = self.visit(n.value)
Expand Down
1 change: 1 addition & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
TYPEDDICT_KEY_MUST_BE_STRING_LITERAL: Final = ErrorMessage(
"Expected TypedDict key to be string literal"
)
TYPEDDICT_OVERRIDE_MERGE: Final = 'Overwriting TypedDict field "{}" while merging'
MALFORMED_ASSERT: Final = ErrorMessage("Assertion is always true, perhaps remove parentheses?")
DUPLICATE_TYPE_SIGNATURES: Final = ErrorMessage("Function has duplicate type signatures")
DESCRIPTOR_SET_NOT_CALLABLE: Final = ErrorMessage("{}.__set__ is not callable")
Expand Down
1 change: 1 addition & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,7 @@ def __getattribute__(self, attr: str) -> type:
VAR_NO_INFO: Final[TypeInfo] = FakeInfo("Var is lacking info")
CLASSDEF_NO_INFO: Final[TypeInfo] = FakeInfo("ClassDef is lacking info")
FUNC_NO_INFO: Final[TypeInfo] = FakeInfo("FuncBase for non-methods lack info")
MISSING_FALLBACK: Final = FakeInfo("fallback can't be filled out until semanal")


class TypeAlias(SymbolNode):
Expand Down
3 changes: 2 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ class BuildType:
UNPACK: Final = "Unpack"
PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes"
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, NEW_GENERIC_SYNTAX))
INLINE_TYPEDDICT: Final = "InlineTypedDict"
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, NEW_GENERIC_SYNTAX, INLINE_TYPEDDICT))
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK))


Expand Down
21 changes: 21 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3935,6 +3935,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
# When this type alias gets "inlined", the Any is not explicit anymore,
# so we need to replace it with non-explicit Anys.
res = make_any_non_explicit(res)
if self.options.disallow_any_unimported and has_any_from_unimported_type(res):
self.msg.unimported_type_becomes_any("Type alias target", res, s)
res = make_any_non_unimported(res)
# Note: with the new (lazy) type alias representation we only need to set no_args to True
# if the expected number of arguments is non-zero, so that aliases like `A = List` work
# but not aliases like `A = TypeAliasType("A", List)` as these need explicit type params.
Expand Down Expand Up @@ -5407,6 +5410,9 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
# When this type alias gets "inlined", the Any is not explicit anymore,
# so we need to replace it with non-explicit Anys.
res = make_any_non_explicit(res)
if self.options.disallow_any_unimported and has_any_from_unimported_type(res):
self.msg.unimported_type_becomes_any("Type alias target", res, s)
res = make_any_non_unimported(res)
eager = self.is_func_scope()
if isinstance(res, ProperType) and isinstance(res, Instance) and not res.args:
fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options)
Expand Down Expand Up @@ -7433,6 +7439,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
return t.copy_modified(args=[a.accept(self) for a in t.args])


def make_any_non_unimported(t: Type) -> Type:
"""Replace all Any types that come from unimported types with special form Any."""
return t.accept(MakeAnyNonUnimported())


class MakeAnyNonUnimported(TrivialSyntheticTypeTranslator):
def visit_any(self, t: AnyType) -> Type:
if t.type_of_any == TypeOfAny.from_unimported_type:
return t.copy_modified(TypeOfAny.special_form, missing_import_name=None)
return t

def visit_type_alias_type(self, t: TypeAliasType) -> Type:
return t.copy_modified(args=[a.accept(self) for a in t.args])


def apply_semantic_analyzer_patches(patches: list[tuple[int, Callable[[], None]]]) -> None:
"""Call patch callbacks in the right order.
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ def process_top_level_function(
deferred, incomplete, progress = semantic_analyze_target(
target, module, state, node, active_type, final_iteration, patches
)
if not incomplete:
state.manager.incomplete_namespaces.discard(module)
if final_iteration:
assert not deferred, "Must not defer during final iteration"
if not progress:
Expand Down
4 changes: 1 addition & 3 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
TYPED_NAMEDTUPLE_NAMES,
AnyType,
CallableType,
Instance,
LiteralType,
TupleType,
Type,
Expand Down Expand Up @@ -632,10 +631,9 @@ def add_method(
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars],
)
if self.options.python_version >= (3, 13):
type_vars = [tv for tv in info.defn.type_vars]
add_method(
"__replace__",
ret=Instance(info, type_vars),
ret=None,
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars],
)

Expand Down
Loading

0 comments on commit d3b99fe

Please sign in to comment.