Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions tests/test_typing_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,150 @@ def test_unusable_types(arg_type):
guess = TypingHintArgSpecGuesser.typing_hint_to_arg_spec_params

assert guess(arg_type) == {}


# ------------------------------------------------------------------------------
# Test type hints on combinations of generics

from typing import (
Dict, Tuple, Mapping, MutableMapping, DefaultDict, ChainMap, OrderedDict,
Callable, Optional, List
)

DFLT_MULTI_PARAM_TYPES = (
Dict, Tuple, Mapping, MutableMapping, DefaultDict, ChainMap, OrderedDict
)

def type_combos(
generic_types,
type_variables=None,
*,
multi_param_types=DFLT_MULTI_PARAM_TYPES
):
"""
Generate "generic" using combinations of types such as
`Optional[List], Dict[Tuple, List], Callable[[List], Dict]`
from a list of generic types such as `Optional`, `List`, `Dict`, `Callable`
and a list of type variables that are used to parametrize these generic types.

:param generic_types: A list of generic types
:param type_variables: A list of type variables
:return: A generator that yields generic types

>>> from typing import Optional, Dict, Tuple, List, Callable
>>> list(type_combos([Optional, Tuple], [list, dict])) # doctest: +NORMALIZE_WHITESPACE
[typing.Optional[list], typing.Optional[dict],
typing.Tuple[list, dict], typing.Tuple[dict, list]]

More significant example:

>>> generic_types = [Optional, Callable, Dict, Tuple]
>>> type_variables = [tuple, dict, List]
>>>
>>> for combo in type_combos(generic_types, type_variables):
... print(combo)
typing.Optional[tuple]
typing.Optional[dict]
typing.Optional[typing.List]
typing.Callable[[typing.Tuple[dict, ...]], tuple]
typing.Callable[[typing.Tuple[typing.List, ...]], tuple]
typing.Callable[[typing.Tuple[dict, typing.List]], tuple]
typing.Callable[[typing.Tuple[typing.List, dict]], tuple]
typing.Callable[[typing.Tuple[tuple, ...]], dict]
typing.Callable[[typing.Tuple[typing.List, ...]], dict]
typing.Callable[[typing.Tuple[tuple, typing.List]], dict]
typing.Callable[[typing.Tuple[typing.List, tuple]], dict]
typing.Callable[[typing.Tuple[tuple, ...]], typing.List]
typing.Callable[[typing.Tuple[dict, ...]], typing.List]
typing.Callable[[typing.Tuple[tuple, dict]], typing.List]
typing.Callable[[typing.Tuple[dict, tuple]], typing.List]
typing.Dict[tuple, dict]
typing.Dict[tuple, typing.List]
typing.Dict[dict, tuple]
typing.Dict[dict, typing.List]
typing.Dict[typing.List, tuple]
typing.Dict[typing.List, dict]
typing.Tuple[tuple, dict]
typing.Tuple[tuple, typing.List]
typing.Tuple[dict, tuple]
typing.Tuple[dict, typing.List]
typing.Tuple[typing.List, tuple]
typing.Tuple[typing.List, dict]
"""
from itertools import permutations

if type_variables is None:
type_variables = list(generic_types)

def generate_combos(generic_type, remaining_vars):
if generic_type is Callable:
# Separate one variable for the output type
for output_type in remaining_vars:
input_vars = [var for var in remaining_vars if var != output_type]
# Generate combinations of input types
for n in range(1, len(input_vars) + 1):
for input_combo in permutations(input_vars, n):
# Format single-element tuples correctly
if len(input_combo) == 1:
input_type = Tuple[input_combo[0], ...]
else:
input_type = Tuple[input_combo]
yield Callable[[input_type], output_type]
elif generic_type in multi_param_types:
required_params = 2 # These types generally require two type parameters
for combo in permutations(remaining_vars, required_params):
yield generic_type[combo]
else:
for type_var in remaining_vars:
yield generic_type[type_var]
for generic_type in generic_types:
yield from generate_combos(generic_type, type_variables)


def issue_216_happens_annotations(func, annotation):
"""
Util to test what annotations make the
https://github.com/neithere/argh/issues/216
issue happen
"""
import argh
func.__annotations__['x'] = annotation
try:
argh.dispatch_command(func)
except IndexError as e:
if e.args[0] == 'tuple index out of range':
return True
except BaseException:
pass
return False


# TODO: Use pytest.mark.parametrize?
def test_that_issue_216_does_not_happen(
generic_types=(Dict, Tuple, OrderedDict, Callable, Optional, List),
type_variables=None
):
"""
Test that the issue 216 happens with the annotations
that we expect it to happen.

NOTE: This takes ~18s to run on my side.
Could reduce the number of generic_types and type_variables to accelerate.
(The current settings lead to 109_776 combinations being tested.
"""
from functools import partial

if type_variables is None:
type_variables = list(set(generic_types) - {Optional}) + [int, str, float]

combos = list(type_combos(generic_types, type_variables))

def func(x = None):
return None

there_is_an_issue = partial(issue_216_happens_annotations, func)

failed = list(map(there_is_an_issue, combos))

failed_combos = [typ for typ, failed_ in zip(combos, failed) if failed_]
assert not failed_combos, f"There were some failed type combos: {failed_combos=}"