diff --git a/conformance/results/mypy/specialtypes_sentinels.toml b/conformance/results/mypy/specialtypes_sentinels.toml new file mode 100644 index 00000000..1ff31a1c --- /dev/null +++ b/conformance/results/mypy/specialtypes_sentinels.toml @@ -0,0 +1,27 @@ +conformant = "Unsupported" +conformance_automated = "Fail" +errors_diff = """ +Line 36: Expected 1 errors +Line 39: Expected 1 errors +Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]', 'specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] +Line 22: Unexpected errors ['specialtypes_sentinels.py:22: error: Expression is of type "Any", not MISSING? [assert-type]', 'specialtypes_sentinels.py:22: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]'] +Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] +Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]'] +Line 28: Unexpected errors ['specialtypes_sentinels.py:28: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type]', 'specialtypes_sentinels.py:28: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]'] +""" +output = """ +specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment] +specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type] +specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] +specialtypes_sentinels.py:22: error: Expression is of type "Any", not MISSING? [assert-type] +specialtypes_sentinels.py:22: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type] +specialtypes_sentinels.py:22: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] +specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type] +specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:28: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type] +specialtypes_sentinels.py:28: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type] +specialtypes_sentinels.py:28: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +""" diff --git a/conformance/results/pycroscope/specialtypes_sentinels.toml b/conformance/results/pycroscope/specialtypes_sentinels.toml new file mode 100644 index 00000000..4c242538 --- /dev/null +++ b/conformance/results/pycroscope/specialtypes_sentinels.toml @@ -0,0 +1,8 @@ +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +./specialtypes_sentinels.py:14:10: Default value for argument x incompatible with declared type int [incompatible_default] +./specialtypes_sentinels.py:36:6: Incompatible argument type for x: expected int | Literal[<>, ] but got [incompatible_argument] +./specialtypes_sentinels.py:39:6: Incompatible argument type for x: expected int | Literal[] but got <> [incompatible_argument] +""" diff --git a/conformance/results/pyrefly/specialtypes_sentinels.toml b/conformance/results/pyrefly/specialtypes_sentinels.toml new file mode 100644 index 00000000..f358b00c --- /dev/null +++ b/conformance/results/pyrefly/specialtypes_sentinels.toml @@ -0,0 +1,23 @@ +conformant = "Unsupported" +conformance_automated = "Fail" +errors_diff = """ +Line 36: Expected 1 errors +Line 39: Expected 1 errors +Line 20: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType | Any` [not-a-type]'] +Line 22: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]'] +Line 24: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]'] +Line 26: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]'] +Line 28: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]'] +Line 30: Unexpected errors ['assert_type(Unknown, int) failed [assert-type]'] +""" +output = """ +ERROR specialtypes_sentinels.py:14:20-27: Default `Sentinel` is not assignable to parameter `x` with type `int` [bad-function-definition] +ERROR specialtypes_sentinels.py:20:14-37: Expected a type form, got instance of `type[int] | UnionType | Any` [not-a-type] +ERROR specialtypes_sentinels.py:22:20-32: assert_type(Sentinel, Unknown) failed [assert-type] +ERROR specialtypes_sentinels.py:22:24-31: Expected a type form, got instance of `Sentinel` [not-a-type] +ERROR specialtypes_sentinels.py:24:24-37: Expected a type form, got instance of `type[int] | UnionType` [not-a-type] +ERROR specialtypes_sentinels.py:26:14-32: Expected a type form, got instance of `type[int] | UnionType` [not-a-type] +ERROR specialtypes_sentinels.py:28:20-37: assert_type(Sentinel, Unknown) failed [assert-type] +ERROR specialtypes_sentinels.py:28:24-36: Expected a type form, got instance of `Sentinel` [not-a-type] +ERROR specialtypes_sentinels.py:30:20-28: assert_type(Unknown, int) failed [assert-type] +""" diff --git a/conformance/results/pyright/specialtypes_sentinels.toml b/conformance/results/pyright/specialtypes_sentinels.toml new file mode 100644 index 00000000..a5884e79 --- /dev/null +++ b/conformance/results/pyright/specialtypes_sentinels.toml @@ -0,0 +1,25 @@ +conformant = "Unsupported" +conformance_automated = "Fail" +errors_diff = """ +Line 36: Expected 1 errors +Line 39: Expected 1 errors +Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)', 'specialtypes_sentinels.py:20:30 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] +Line 22: Unexpected errors ['specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] +Line 24: Unexpected errors ['specialtypes_sentinels.py:24:30 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] +Line 26: Unexpected errors ['specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] +Line 28: Unexpected errors ['specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:28:28 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] +Line 30: Unexpected errors ['specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)'] +""" +output = """ +specialtypes_sentinels.py:14:20 - error: Expression of type "Sentinel" cannot be assigned to parameter of type "int" +  "Sentinel" is not assignable to "int" (reportArgumentType) +specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:20:30 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure) +specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:24:30 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure) +specialtypes_sentinels.py:28:28 - error: Variable not allowed in type expression (reportInvalidTypeForm) +specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure) +""" diff --git a/conformance/results/results.html b/conformance/results/results.html index 3248f3f5..79fd628c 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -274,6 +274,14 @@

Python Type System Conformance Test Results

Pass Pass +     specialtypes_sentinels +Unsupported +Unsupported +Unsupported +Unsupported +Pass +Unsupported +      specialtypes_type
Partial

Does not treat `type` same as `type[Any]` for assert_type.

Does not allow access to unknown attributes from object of type `type[Any]`.

Pass diff --git a/conformance/results/ty/specialtypes_sentinels.toml b/conformance/results/ty/specialtypes_sentinels.toml new file mode 100644 index 00000000..1ae8cc92 --- /dev/null +++ b/conformance/results/ty/specialtypes_sentinels.toml @@ -0,0 +1,22 @@ +conformant = "Unsupported" +conformance_automated = "Fail" +errors_diff = """ +Line 36: Expected 1 errors +Line 39: Expected 1 errors +Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation', 'specialtypes_sentinels.py:20:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation'] +Line 22: Unexpected errors ['specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`'] +Line 24: Unexpected errors ['specialtypes_sentinels.py:24:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a type expression'] +Line 26: Unexpected errors ['specialtypes_sentinels.py:26:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation'] +Line 28: Unexpected errors ['specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`'] +Line 30: Unexpected errors ['specialtypes_sentinels.py:30:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`'] +""" +output = """ +specialtypes_sentinels.py:14:11: error[invalid-parameter-default] Default value of type `Sentinel` is not assignable to annotated parameter type `int` +specialtypes_sentinels.py:20:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation +specialtypes_sentinels.py:20:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation +specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo` +specialtypes_sentinels.py:24:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a type expression +specialtypes_sentinels.py:26:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation +specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo` +specialtypes_sentinels.py:30:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int` +""" diff --git a/conformance/results/zuban/specialtypes_sentinels.toml b/conformance/results/zuban/specialtypes_sentinels.toml new file mode 100644 index 00000000..f9716492 --- /dev/null +++ b/conformance/results/zuban/specialtypes_sentinels.toml @@ -0,0 +1,28 @@ +conformant = "Unsupported" +conformance_automated = "Fail" +errors_diff = """ +Line 36: Expected 1 errors +Line 39: Expected 1 errors +Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]', 'specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] +Line 22: Unexpected errors ['specialtypes_sentinels.py:22: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]'] +Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] +Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]'] +Line 28: Unexpected errors ['specialtypes_sentinels.py:28: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]'] +Line 30: Unexpected errors ['specialtypes_sentinels.py:30: error: Expression is of type "int | Any", not "int" [misc]'] +""" +output = """ +specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment] +specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type] +specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] +specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:22: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type] +specialtypes_sentinels.py:22: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] +specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:26: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type] +specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:28: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type] +specialtypes_sentinels.py:28: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +specialtypes_sentinels.py:30: error: Expression is of type "int | Any", not "int" [misc] +""" diff --git a/conformance/tests/specialtypes_sentinels.py b/conformance/tests/specialtypes_sentinels.py new file mode 100644 index 00000000..73c25701 --- /dev/null +++ b/conformance/tests/specialtypes_sentinels.py @@ -0,0 +1,40 @@ +from typing_extensions import Sentinel, assert_type + +# > Sentinel objects may be used in type annotations if they are defined using +# > a simple assignment of the form ``NAME = sentinel('NAME')`` in the +# > global scope or in a class body that is not within a function. + +MISSING = Sentinel("") # name is not required to match the variable name +SPECIAL = Sentinel("SPECIAL") + +class Cls: + IN_CLASS = Sentinel("Cls.IN_CLASS") + + +def func1(x: int = MISSING) -> None: # E: incompatible default + pass + +# > Type checkers must support narrowing union types involving sentinels using the +# > ``is`` and ``is not`` operators + +def func2(x: int | MISSING | SPECIAL = MISSING) -> None: + if x is MISSING: + assert_type(x, MISSING) + else: + assert_type(x, int | SPECIAL) + +def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: + if x is Cls.IN_CLASS: + assert_type(x, Cls.IN_CLASS) + else: + assert_type(x, int) + + +func2(1) # ok +func2(MISSING) # ok +func2(SPECIAL) # ok +func2(Cls.IN_CLASS) # E: incompatible argument + +func3(1) # ok +func3(MISSING) # E: incompatible argument +func3(Cls.IN_CLASS) # ok diff --git a/docs/spec/annotations.rst b/docs/spec/annotations.rst index 085d9277..558b9b70 100644 --- a/docs/spec/annotations.rst +++ b/docs/spec/annotations.rst @@ -139,7 +139,7 @@ The following grammar describes the allowed elements of type and annotation expr : | : | name : (where name must refer to a valid in-scope class, - : type alias, or TypeVar) + : type alias, TypeVar, or sentinel object) : | name '[' (`maybe_unpacked` | `type_expression_list`) : (',' (`maybe_unpacked` | `type_expression_list`))* ']' : (the `type_expression_list` form is valid only when diff --git a/docs/spec/special-types.rst b/docs/spec/special-types.rst index eab658b0..56e61daa 100644 --- a/docs/spec/special-types.rst +++ b/docs/spec/special-types.rst @@ -53,6 +53,46 @@ are highly dynamic. When used in a type hint, the expression ``None`` is considered equivalent to ``type(None)``. +.. _ `sentinels`: + +Sentinels +--------- + +Sentinel objects may be used in type annotations to represent themselves:: + + MISSING = sentinel('MISSING') + OTHER = sentinel('OTHER') + + def f(x: int | MISSING = MISSING) -> int: + if x is MISSING: + return 0 + return x + + f(OTHER) # Error, OTHER is not an int or MISSING + f(MISSING) # OK, MISSING is a valid argument + +Sentinels may be created using the ``sentinel()`` built-in in Python 3.15 +and higher. ``typing_extensions`` provides a backport of this function. For +historical reasons the object was first introduced under the name +``typing_extensions.Sentinel``, and later ``typing_extensions.sentinel`` was +added as an alias; type checkers should support both. + +Sentinel objects may be used in type annotations if they are defined using +a simple assignment of the form ``NAME = sentinel('NAME')`` in the +global scope or in a class body that is not within a function. The name of the +variable need not match the string argument passed to ``sentinel()`` but it is +conventional to do so for names in the global scope. + +Type checkers must support narrowing union types involving sentinels using the +``is`` and ``is not`` operators:: + + def g(x: int | MISSING) -> None: + if x is MISSING: + assert_type(x, MISSING) + else: + assert_type(x, int) + + .. _`noreturn`: ``NoReturn``