Skip to content

Commit 520b5cd

Browse files
committed
rework special cases and forward ref
1 parent a770495 commit 520b5cd

File tree

1 file changed

+28
-345
lines changed

1 file changed

+28
-345
lines changed

src/test_typing_extensions.py

Lines changed: 28 additions & 345 deletions
Original file line numberDiff line numberDiff line change
@@ -8469,352 +8469,23 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
84698469
)
84708470

84718471
class TestEvaluateForwardRefs(BaseTestCase):
8472-
def test_evaluate_forward_refs(self):
8473-
NoneType = type(None)
8474-
T = TypeVar("T")
8475-
T_default = TypeVar("T_default", default=None)
8476-
Ts = TypeVarTuple("Ts")
8477-
8478-
# This variable must have a different name TypeVar
8479-
T_local = TypeVar("T_nonlocal")
8480-
8481-
class X:
8482-
T_in_Y = object() # Using X as owner should not access this value
8483-
8484-
T_in_Y = TypeVar("T_in_Y")
8485-
class Y(Generic[T_in_Y]):
8486-
a = "X"
8487-
bT = "Y[T_nonlocal]"
8488-
# Object with __type_params__
8489-
Z = TypeAliasType("Z", Y[T_in_Y], type_params=(T_in_Y,))
8490-
del T_in_Y
8491-
8492-
# Assure that class names are not in globals
8493-
self.assertNotIn(X.__name__, globals())
8494-
self.assertNotIn(Y.__name__, globals())
8495-
8496-
# Namespaces for evaluation
8497-
minimal_localns = {
8498-
"ForwardRef": typing.ForwardRef,
8499-
"NoneAlias" : None,
8500-
"NoneType" : NoneType,
8501-
"T": T,
8502-
"T_default": T_default,
8503-
"Ts" : Ts,
8504-
"StrAlias" : str,
8505-
}
8506-
full_localns = {
8507-
**minimal_localns,
8508-
"X" : X,
8509-
"Y" : Y,
8510-
"Z" : Z,
8511-
}
8512-
minimal_globalns = {
8513-
"typing": typing,
8514-
"ForwardRef": typing.ForwardRef,
8515-
"typing_extensions": typing_extensions,
8516-
**vars(typing_extensions),
8517-
}
8518-
8519-
# Helper functions to construct test cases
8520-
def _unroll_subcase(
8521-
annot, expected, format, is_argument, is_class, globalns, localns
8522-
):
8523-
owner = expected.get("owner", None)
8524-
type_params = expected.get("type_params", None)
8525-
skip_if = expected.get("skip_if", False)
8526-
if skip_if: # Should only be used in list of dicts
8527-
ns = locals()
8528-
skip = all(ns[k] == v for k, v in skip_if.items())
8529-
if skip:
8530-
return None
8531-
return (
8532-
annot,
8533-
expected[format],
8534-
format,
8535-
is_argument,
8536-
is_class,
8537-
globalns,
8538-
localns,
8539-
type_params,
8540-
owner,
8541-
)
8542-
8543-
def unroll_cases(cases):
8544-
"""Generator to yield all test case combinations"""
8545-
for (
8546-
annot,
8547-
expected,
8548-
), format, is_argument, is_class, globalns, localns in itertools.product(
8549-
# annot, expected
8550-
cases.items(),
8551-
# format
8552-
(Format.VALUE, Format.FORWARDREF, Format.STRING),
8553-
# is_argument
8554-
(False, True),
8555-
# is_class is not availiable for all versions
8556-
(False, True) if _FORWARD_REF_HAS_CLASS else (None,),
8557-
# globals
8558-
(globals(), minimal_globalns),
8559-
# locals
8560-
(full_localns, minimal_localns),
8561-
):
8562-
# Argument for ForwardRef
8563-
if isinstance(annot, type):
8564-
annot = annot.__name__
8565-
else:
8566-
annot = str(annot)
8567-
# Dict with Format.VALUE, Format.FORWARDREF, Format.STRING entries
8568-
if isinstance(expected, dict):
8569-
case = _unroll_subcase(
8570-
annot,
8571-
expected,
8572-
format,
8573-
is_argument,
8574-
is_class,
8575-
globalns,
8576-
localns,
8577-
)
8578-
yield case
8579-
# List of dicts with multiple cases depending on other parameters
8580-
elif type(expected) is list: # noqa: E721 # use `is` because of _ConcatenateGenericAlias
8581-
yield from filter(
8582-
None,
8583-
(
8584-
_unroll_subcase(
8585-
annot,
8586-
sub_expected,
8587-
format,
8588-
is_argument,
8589-
is_class,
8590-
globalns,
8591-
localns,
8592-
)
8593-
for sub_expected in expected
8594-
),
8595-
)
8596-
# Single value
8597-
else:
8598-
# Change expected to TypeError if it will fail typing._type_check
8599-
if format != Format.STRING and not inspect.isclass(expected):
8600-
invalid_generic_forms = (Generic, Protocol)
8601-
if is_class is False:
8602-
invalid_generic_forms += (ClassVar,)
8603-
if is_argument:
8604-
invalid_generic_forms += (Final,)
8605-
elif not _FORWARD_REF_HAS_CLASS and is_argument:
8606-
invalid_generic_forms += (
8607-
ClassVar,
8608-
Final,
8609-
)
8610-
if get_origin(expected) in invalid_generic_forms:
8611-
expected = TypeError
8612-
type_params = None
8613-
owner = None
8614-
yield (
8615-
annot,
8616-
expected,
8617-
format,
8618-
is_argument,
8619-
is_class,
8620-
globalns,
8621-
localns,
8622-
type_params,
8623-
owner,
8624-
)
8625-
8626-
cases = {
8627-
# values can be types, dicts or list of dicts.
8628-
# Combination of all subdicts with "skip_if" should provide complete coverage,
8629-
# i.e. they should come in pairs.
8630-
# For non-complete coverage set the value to "Skip: reason".
8631-
None: {
8632-
Format.VALUE: NoneType,
8633-
Format.FORWARDREF: NoneType,
8634-
Format.STRING: "None",
8635-
},
8636-
"NoneAlias": {
8637-
Format.VALUE: NoneType,
8638-
Format.FORWARDREF: NoneType,
8639-
Format.STRING: "NoneAlias",
8640-
},
8641-
str: str,
8642-
"StrAlias": {
8643-
Format.VALUE: str,
8644-
Format.FORWARDREF: str,
8645-
Format.STRING: "StrAlias",
8646-
},
8647-
Union[str, None, "str"]: {
8648-
Format.VALUE: Optional[str],
8649-
Format.FORWARDREF: Optional[str],
8650-
Format.STRING: str(Union[str, None, "str"]),
8651-
},
8652-
"Y.a": [
8653-
{
8654-
"skip_if": {"localns": minimal_localns, "format": Format.FORWARDREF},
8655-
Format.VALUE: X,
8656-
Format.FORWARDREF: X,
8657-
Format.STRING: "Y.a",
8658-
},
8659-
{
8660-
"skip_if": {"localns": full_localns, "format": Format.FORWARDREF},
8661-
Format.VALUE: X,
8662-
Format.FORWARDREF: typing.ForwardRef("Y.a"),
8663-
Format.STRING: "Y.a",
8664-
},
8665-
],
8666-
"Y.bT": [
8667-
# Note: Different to the <3.14 ForwardRef behavior STRING yields "Y.bT"
8668-
# and not "Y[T_nonlocal]"
8669-
{
8670-
"type_params": (T_local, ),
8671-
Format.VALUE: Y[T_local],
8672-
Format.FORWARDREF: Y[T_local],
8673-
Format.STRING: "Y.bT",
8674-
},
8675-
{
8676-
"type_params": None,
8677-
"skip_if": {"localns": minimal_localns, "format": Format.FORWARDREF},
8678-
Format.VALUE: NameError,
8679-
Format.FORWARDREF: typing.ForwardRef("Y[T_nonlocal]"),
8680-
Format.STRING: "Y.bT",
8681-
},
8682-
{
8683-
"type_params": None,
8684-
"skip_if": {"localns": full_localns, "format": Format.FORWARDREF},
8685-
Format.VALUE: NameError,
8686-
Format.FORWARDREF: typing.ForwardRef("Y.bT"),
8687-
Format.STRING: "Y.bT",
8688-
},
8689-
],
8690-
# Special cases for typing._type_check.
8691-
# Note: Depending on `is_class` and `is_argument` will raise TypeError
8692-
# therefore `expected` is converted in the generator.
8693-
ClassVar[None]: ClassVar[None],
8694-
Final[None]: Final[None],
8695-
Protocol[T]: Protocol[T],
8696-
# Plain usage
8697-
Protocol: {
8698-
Format.VALUE: TypeError,
8699-
Format.FORWARDREF: TypeError,
8700-
Format.STRING: "Protocol",
8701-
},
8702-
Generic: {
8703-
Format.VALUE: TypeError,
8704-
Format.FORWARDREF: TypeError,
8705-
Format.STRING: "Generic",
8706-
},
8707-
Final: (
8708-
[
8709-
{
8710-
"skip_if": (
8711-
{"is_class": False}
8712-
if _FORWARD_REF_HAS_CLASS
8713-
else {"is_argument": True}
8714-
),
8715-
Format.VALUE: Final,
8716-
Format.FORWARDREF: Final,
8717-
Format.STRING: str(Final),
8718-
},
8719-
{
8720-
"skip_if": (
8721-
{"is_class": True}
8722-
if _FORWARD_REF_HAS_CLASS
8723-
else {"is_argument": False}
8724-
),
8725-
Format.VALUE: TypeError,
8726-
Format.FORWARDREF: TypeError,
8727-
Format.STRING: str(Final),
8728-
},
8729-
]
8730-
),
8731-
}
8732-
8733-
# Test cases
8734-
for (
8735-
annot,
8736-
expected,
8737-
format,
8738-
is_argument,
8739-
is_class,
8740-
globalns,
8741-
localns,
8742-
type_params,
8743-
owner,
8744-
) in unroll_cases(cases):
8745-
if _FORWARD_REF_HAS_CLASS:
8746-
ref = typing.ForwardRef(annot, is_argument=is_argument, is_class=is_class)
8747-
else:
8748-
ref = typing.ForwardRef(annot, is_argument=is_argument)
8749-
with self.subTest(
8750-
annot=annot,
8751-
expected=expected,
8752-
format=format,
8753-
is_argument=is_argument,
8754-
is_class=is_class,
8755-
type_params=type_params,
8756-
minimal_globalns=globalns == minimal_globalns,
8757-
minimal_localns=localns == minimal_localns,
8758-
owner=owner,
8759-
):
8760-
# Skip test
8761-
if isinstance(expected, str) and expected.lower().startswith("skip"):
8762-
self.skipTest(expected)
8763-
# Expected Exceptions
8764-
if inspect.isclass(expected) and issubclass(expected, Exception):
8765-
with self.assertRaises(expected):
8766-
evaluate_forward_ref(
8767-
ref, locals=localns, globals=globalns, format=format,
8768-
)
8769-
continue
8770-
# Adjust expected:
8771-
# STRING
8772-
if format == Format.STRING:
8773-
expected = (
8774-
str(expected)
8775-
if not isinstance(expected, type)
8776-
else expected.__name__
8777-
)
8778-
# FORWARDREF with X, Y not in locals stays a forward ref
8779-
elif (
8780-
format == Format.FORWARDREF
8781-
and (get_origin(expected) in (X, Y) or expected in (X, Y))
8782-
and localns is minimal_localns
8783-
):
8784-
expected = ref
8785-
# VALUE with X, Y not in locals will raise NameError
8786-
elif (
8787-
format == Format.VALUE
8788-
and (get_origin(expected) in (X, Y) or expected in (X, Y))
8789-
and localns is minimal_localns
8790-
):
8791-
with self.assertRaisesRegex(NameError, "X|Y"):
8792-
evaluate_forward_ref(
8793-
ref,
8794-
locals=localns,
8795-
globals=globalns,
8796-
format=format,
8797-
type_params=type_params,
8798-
owner=owner,
8799-
)
8800-
continue
8801-
8802-
result = evaluate_forward_ref(
8803-
ref,
8804-
locals=localns,
8805-
globals=globalns,
8806-
format=format,
8807-
type_params=type_params,
8808-
owner=owner,
8809-
)
8810-
self.assertEqual(result, expected)
8811-
88128472
def test_forward_ref_fallback(self):
88138473
with self.assertRaises(NameError):
8814-
evaluate_forward_ref(typing.ForwardRef("unbound"), format=Format.VALUE)
8815-
ref = typing.ForwardRef("unbound")
8474+
evaluate_forward_ref(typing.ForwardRef("doesntexist"))
8475+
ref = typing.ForwardRef("doesntexist")
88168476
self.assertIs(evaluate_forward_ref(ref, format=Format.FORWARDREF), ref)
88178477

8478+
class X:
8479+
unresolvable = "doesnotexist2"
8480+
8481+
evaluated_ref = evaluate_forward_ref(
8482+
typing.ForwardRef("X.unresolvable"),
8483+
locals={"X": X},
8484+
type_params=None,
8485+
format=Format.FORWARDREF,
8486+
)
8487+
self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2"))
8488+
88188489
def test_evaluate_with_type_params(self):
88198490
# Use a T name that is not in globals
88208491
self.assertNotIn("Tx", globals())
@@ -8930,9 +8601,6 @@ def test_name_lookup_without_eval(self):
89308601
with support.swap_attr(builtins, "int", dict):
89318602
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), dict)
89328603

8933-
with self.assertRaises(NameError):
8934-
evaluate_forward_ref(typing.ForwardRef("doesntexist"))
8935-
89368604
def test_nested_strings(self):
89378605
# This variable must have a different name TypeVar
89388606
Tx = TypeVar("Tx")
@@ -8968,6 +8636,21 @@ class Y(Generic[Tx]):
89688636
self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10")
89698637
self.assertEqual(get_args(evaluated_ref3), (Z[str],))
89708638

8639+
def test_invalid_special_forms(self):
8640+
# tests _lax_type_check to (not) raise error similar to the typing module
8641+
with self.assertRaisesRegex(TypeError, "Plain"):
8642+
evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing))
8643+
with self.assertRaisesRegex(TypeError, "Plain"):
8644+
evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing))
8645+
if _FORWARD_REF_HAS_CLASS:
8646+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final)
8647+
with self.assertRaisesRegex(TypeError, "Plain"):
8648+
evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing))
8649+
else:
8650+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
8651+
with self.assertRaisesRegex(TypeError, "Plain"):
8652+
evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing))
8653+
89718654

89728655
if __name__ == '__main__':
89738656
main()

0 commit comments

Comments
 (0)