• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IBM / unitxt / 17262388111

27 Aug 2025 09:08AM UTC coverage: 80.803% (-0.007%) from 80.81%
17262388111

Pull #1929

github

web-flow
Merge 356088586 into 6aff8ad6c
Pull Request #1929: Improved multi turn evaluation to be self contained and use LLM as judge

1595 of 1992 branches covered (80.07%)

Branch coverage included in aggregate %.

10839 of 13396 relevant lines covered (80.91%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.68
src/unitxt/type_utils.py
1
import ast
1✔
2
import collections.abc
1✔
3
import io
1✔
4
import itertools
1✔
5
import re
1✔
6
import typing
1✔
7
from functools import lru_cache
1✔
8
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
1✔
9

10
from .utils import safe_eval
1✔
11

12
_registered_types = {
1✔
13
    "Any": typing.Any,
14
    "List": typing.List,
15
    "Dict": typing.Dict,
16
    "Tuple": typing.Tuple,
17
    "Union": typing.Union,
18
    "Optional": typing.Optional,
19
    "Literal": typing.Literal,
20
    "int": int,
21
    "str": str,
22
    "float": float,
23
    "bool": bool,
24
}
25

26

27
def register_type(new_type):
1✔
28
    assert (
1✔
29
        is_new_type(new_type)
30
        or is_typed_dict(new_type)
31
        or hasattr(new_type, "__verify_type__")
32
    ), "Can register only typing.NewType or typing.TypedDict or object with __verify_type__ class function"
33
    _registered_types[new_type.__name__] = new_type
1✔
34

35

36
Type = typing.Any
1✔
37

38

39
class UnsupportedTypeError(ValueError):
1✔
40
    def __init__(self, type_object):
1✔
41
        supported_types = ", ".join(_registered_types.keys())
1✔
42
        super().__init__(
1✔
43
            f"Type: '{type_object!s}' is not supported type. Use one of {supported_types}"
44
        )
45

46

47
class GenericTypedDict(TypedDict):
1✔
48
    pass
49

50

51
_generics = [
1✔
52
    List[Any],
53
    Dict[Any, Any],
54
    Tuple[Any],
55
    Union[Any, Any],
56
    Optional[Any],
57
    Any,
58
    Literal,
59
]
60

61
_generics_types = [type(t) for t in _generics]
1✔
62

63

64
def is_new_type(object):
1✔
65
    return callable(object) and hasattr(object, "__supertype__")
1✔
66

67

68
def is_typed_dict(object):
1✔
69
    return isinstance(object, type(GenericTypedDict))
1✔
70

71

72
def is_type(object):
1✔
73
    """Checks if the provided object is a type, including generics, Literal, TypedDict, and NewType."""
74
    if object is typing.Type:
1✔
75
        return True
1✔
76
    return (
1✔
77
        isinstance(object, (type, *_generics_types))
78
        or is_new_type(object)
79
        or is_typed_dict(object)
80
    )
81

82

83
def is_type_dict(object):
1✔
84
    if not isinstance(object, dict):
1✔
85
        raise ValueError("Should be dict.")
86
    for value in object.values():
1✔
87
        if isinstance(value, dict):
1✔
88
            if not is_type_dict(value):
×
89
                return False
×
90
        elif not is_type(value):
1✔
91
            return False
×
92
    return True
1✔
93

94

95
def convert_union_type(type_string: str) -> str:
1✔
96
    """Converts Python 3.10 union type hints into form compatible with Python 3.9 version.
97

98
    Args:
99
        type_string (str): A string representation of a Python type hint. It can be any
100
            valid Python type, which does not contain strings (e.g. 'Literal').
101
            Examples include 'List[int|float]', 'str|float|bool' etc.
102

103
            Formally, the function depends on the input string adhering to the following rules.
104
            Assuming that the input is a valid type hint the function does not check that 'word' is
105
            'str', 'bool', 'List' etc. It just depends on the following general structure (spaces ignored):
106
            type -> word OR type( | type)* OR word[type( , type)*]
107
            word is a sequence of (0 or more) chars, each being any char but: [ ] , |
108
            This implies that if any of these 4 chars shows not as a meta char of the input
109
            type_string, but inside some constant string (of Literal, for example), the scheme
110
            will not work.
111

112
            Cases like Literal, that might contain occurrences of the four chars above not as meta chars
113
            in the type string, must be handled as special cases by this function, as shown for Literal,
114
            as an example. Because 'format_type_string' serves as preprocessing for 'parse_type_string',
115
            which has a list of allowed types, of which Literal is not a member, Literal and such are not
116
            relevant at all now; and the case is brought here just for an example for future use.
117

118

119
    Returns:
120
        str: A type string with converted union types, which is compatible with typing module.
121

122
    Examples:
123
        convert_union_type('List[int|float]') -> 'List[Union[int,float]]'
124
        convert_union_type('Optional[int|float|bool]') -> 'Optional[Union[int,float,bool]]'
125

126
    """
127

128
    def consume_literal(string: str) -> str:
1✔
129
        # identifies the prefix of string that matches a full Literal typing, with all its constants, including
130
        # constants that contain [ ] , etc. on which construct_union_part depends.
131
        # string starts with the [ that follows 'Literal'
132
        candidate_end = string.find("]")
1✔
133
        while candidate_end != -1:
1✔
134
            try:
1✔
135
                ast.literal_eval(string[: candidate_end + 1])
1✔
136
                break
1✔
137
            except Exception:
138
                candidate_end = string.find("]", candidate_end + 1)
139

140
        if candidate_end == -1:
1✔
141
            raise ValueError("invalid Literal in input type_string")
142
        return string[: candidate_end + 1]
1✔
143

144
    stack = [""]  # the start of a type
1✔
145
    input = type_string.strip()
1✔
146
    next_word = re.compile(r"([^\[\],|]*)([\[\],|]|$)")
1✔
147
    while len(input) > 0:
1✔
148
        word = next_word.match(input)
1✔
149
        input = input[len(word.group(0)) :].strip()
1✔
150
        stack[-1] += word.group(1)
1✔
151
        if word.group(2) in ["]", ",", ""]:  # "" for eol:$
1✔
152
            # top of stack is now complete to a whole type
153
            lwt = stack.pop()
1✔
154
            if (
1✔
155
                "|" in lwt
156
            ):  # the | -s are only at the top level of lwt, not inside any subtype
157
                lwt = "Union[" + lwt.replace("|", ",") + "]"
1✔
158
            lwt += word.group(2)
1✔
159
            if len(stack) > 0:
1✔
160
                stack[-1] += lwt
1✔
161
            else:
162
                stack = [lwt]
1✔
163
            if word.group(2) == ",":
1✔
164
                stack.append("")  # to start the expected next type
1✔
165

166
        elif word.group(2) in ["|"]:
1✔
167
            # top of stack is the last whole element(s) to be union-ed,
168
            # and more are expected
169
            stack[-1] += "|"
1✔
170

171
        else:  # "["
172
            if word.group(1) == "Literal":
1✔
173
                literal_ops = consume_literal("[" + input)
1✔
174
                stack[-1] += literal_ops
1✔
175
                input = input[len(literal_ops) - 1 :]
1✔
176
            else:
177
                stack[-1] += "["
1✔
178
                stack.append("")
1✔
179
                # start type (,type)*  inside the []
180

181
    assert len(stack) == 1
1✔
182
    if "|" in stack[0]:  # these belong to the top level only
1✔
183
        stack[0] = "Union[" + stack[0].replace("|", ",") + "]"
1✔
184
    return stack[0]
1✔
185

186

187
def format_type_string(type_string: str) -> str:
1✔
188
    """Formats a string representing a valid Python type hint so that it is compatible with Python 3.9 notation.
189

190
    Args:
191
        type_string (str): A string representation of a Python type hint. This can be any
192
                           valid type, which does not contain strings (e.g. 'Literal').
193
                           Examples include 'List[int]', 'Dict[str, Any]', 'Optional[List[str]]', etc.
194

195
    Returns:
196
        str: A formatted type string.
197

198
    Examples:
199
        format_type_string('list[int | float]') -> 'List[Union[int,float]]'
200
        format_type_string('dict[str, Optional[str]]') -> 'Dict[str,Optional[str]]'
201

202
    The function formats valid type string (either after or before Python 3.10) into a
203
    form compatible with 3.9. This is done by captilizing the first letter of a lower-cased
204
    type name and transferring the 'bitwise or operator' into 'Union' notation. The function
205
    also removes whitespaces and redundant module name in type names imported from 'typing'
206
    module, e.g. 'typing.Tuple' -> 'Tuple'.
207

208
    Currently, the capitalization is applied only to types which unitxt allows, i.e.
209
    'list', 'dict', 'tuple'. Moreover, the function expects the input to not contain types
210
    which contain strings, for example 'Literal'.
211
    """
212
    types_map = {
1✔
213
        "list": "List",
214
        "tuple": "Tuple",
215
        "dict": "Dict",
216
        "typing.": "",
217
        " ": "",
218
    }
219
    for old_type, new_type in types_map.items():
1✔
220
        type_string = type_string.replace(old_type, new_type)
1✔
221
    return convert_union_type(type_string)
1✔
222

223

224
def parse_type_string(type_string: str) -> typing.Any:
1✔
225
    """Parses a string representing a Python type hint and evaluates it to return the corresponding type object.
226

227
    This function uses a safe evaluation context
228
    to mitigate the risks of executing arbitrary code.
229

230
    Args:
231
        type_string (str): A string representation of a Python type hint. Examples include
232
                           'List[int]', 'Dict[str, Any]', 'Optional[List[str]]', etc.
233

234
    Returns:
235
        typing.Any: The Python type object corresponding to the given type string.
236

237
    Raises:
238
        ValueError: If the type string contains elements not allowed in the safe context
239
                    or tokens list.
240

241
    The function formats the string first if it represents a new Python type hint
242
    (i.e. valid since Python 3.10), which uses lowercased names for some types and
243
    'bitwise or operator' instead of 'Union', for example: 'list[int|float]' instead
244
    of 'List[Union[int,float]]' etc.
245

246
    The function uses a predefined safe context with common types from the `typing` module
247
    and basic Python data types. It also defines a list of safe tokens that are allowed
248
    in the type string.
249
    """
250
    type_string = format_type_string(type_string)
1✔
251

252
    return safe_eval(
1✔
253
        type_string, context=_registered_types, allowed_tokens=["[", "]", ",", " "]
254
    )
255

256

257
def replace_class_names(full_string: str) -> str:
1✔
258
    # Regular expression to match any fully qualified class name and extract the class name
259
    pattern = r"(?:\w+\.)*<locals>\.(\w+)|(?:\w+\.)*(\w+)"
1✔
260

261
    # Function to replace the matched pattern with just the class name
262
    def replacement(match):
1✔
263
        # If the match has a group for <locals>
264
        if match.group(1):
1✔
265
            return match.group(1)
1✔
266
        # Otherwise, return the last group (class name)
267
        return match.group(2)
1✔
268

269
    # Use re.sub to replace all occurrences in the string
270
    return re.sub(pattern, replacement, full_string)
1✔
271

272

273
def to_type_string(typing_type):
1✔
274
    type_string = strtype(typing_type)
1✔
275
    assert parse_type_string(type_string), "Is not parsed well"
1✔
276
    return type_string
1✔
277

278

279
def to_type_dict(dict_of_typing_types):
1✔
280
    result = {}
1✔
281
    for key, val in dict_of_typing_types.items():
1✔
282
        if isinstance(val, dict):
1✔
283
            result[key] = to_type_dict(val)
×
284
        else:
285
            result[key] = to_type_string(val)
1✔
286
    return result
1✔
287

288

289
def parse_type_dict(type_dict):
1✔
290
    results = {}
1✔
291
    for k, v in type_dict.items():
1✔
292
        if isinstance(v, str):
1✔
293
            results[k] = parse_type_string(v)
1✔
294
        elif isinstance(v, dict):
×
295
            results[k] = parse_type_dict(v)
×
296
        else:
297
            raise ValueError(
298
                f"Can parse only nested dictionary with type strings, got {type(v)}"
299
            )
300
    return results
1✔
301

302

303
def infer_type(obj) -> typing.Any:
1✔
304
    return parse_type_string(infer_type_string(obj))
1✔
305

306

307
def infer_type_string(obj: typing.Any) -> str:
1✔
308
    """Encodes the type of a given object into a string.
309

310
    Args:
311
        obj:Any
312

313
    Returns:
314
      a string representation of the type of the object. e.g. ``"str"``, ``"List[int]"``, ``"Dict[str, Any]"``
315

316
    | formal definition of the returned string:
317
    | Type -> basic | List[Type] | Dict[Type, Type] | Union[Type(, Type)*] | Tuple[Type(, Type)*]
318
    | basic -> ``bool`` | ``str`` | ``int`` | ``float`` | ``Any``
319

320

321
    Examples:
322
        | ``infer_type_string({"how_much": 7})`` returns ``"Dict[str,int]"``
323
        | ``infer_type_string([1, 2])`` returns ``"List[int]"``
324
        | ``infer_type_string([])`` returns ``"List[Any]")``    no contents to list to indicate any type
325
        | ``infer_type_string([[], [7]])`` returns ``"List[List[int]]"``  type of parent list indicated
326
          by the type of the non-empty child list. The empty child list is indeed, by default, also of
327
          that type of the non-empty child.
328
        | ``infer_type_string([[], 7, True])`` returns ``"List[Union[List[Any],int]]"``
329
          because ``bool`` is also an ``int``
330

331
    """
332

333
    def consume_arg(args_list: str) -> typing.Tuple[str, str]:
1✔
334
        first_word = re.search(r"^(List\[|Dict\[|Union\[|Tuple\[)", args_list)
1✔
335
        if not first_word:
1✔
336
            first_word = re.search(r"^(str|bool|int|float|Any)", args_list)
1✔
337
            assert first_word, "parsing error"
1✔
338
            return first_word.group(), args_list[first_word.span()[1] :]
1✔
339
        arg_to_ret = first_word.group()
1✔
340
        args_list = args_list[first_word.span()[1] :]
1✔
341
        arg, args_list = consume_arg(args_list)
1✔
342
        arg_to_ret += arg
1✔
343
        while args_list.startswith(","):
1✔
344
            arg, args_list = consume_arg(args_list[1:])
1✔
345
            arg_to_ret = arg_to_ret + "," + arg
1✔
346
        assert args_list.startswith("]"), "parsing error"
1✔
347
        return arg_to_ret + "]", args_list[1:]
1✔
348

349
    def find_args_in(args: str) -> typing.List[str]:
1✔
350
        to_ret = []
1✔
351
        while len(args) > 0:
1✔
352
            arg, args = consume_arg(args)
1✔
353
            to_ret.append(arg)
1✔
354
            if args.startswith(","):
1✔
355
                args = args[1:]
1✔
356
        return to_ret
1✔
357

358
    def is_covered_by(left: str, right: str) -> bool:
1✔
359
        if left == right:
1✔
360
            return True
1✔
361
        if left.startswith("Union["):
1✔
362
            return all(
1✔
363
                is_covered_by(left_el, right) for left_el in find_args_in(left[6:-1])
364
            )
365
        if right.startswith("Union["):
1✔
366
            return any(
1✔
367
                is_covered_by(left, right_el) for right_el in find_args_in(right[6:-1])
368
            )
369
        if left.startswith("List[") and right.startswith("List["):
1✔
370
            return is_covered_by(
1✔
371
                left[5:-1], right[5:-1]
372
            )  # un-wrap the leading List[  and the trailing ]
373
        if left.startswith("Dict[") and right.startswith("Dict["):
1✔
374
            return is_covered_by(
1✔
375
                left[5 : left.find(",")], right[5 : right.find(",")]
376
            ) and is_covered_by(
377
                left[1 + left.find(",") : -1], right[1 + right.find(",") : -1]
378
            )
379
        if left.startswith("Tuple[") and right.startswith("Tuple["):
1✔
380
            if left.count(",") != right.count(","):
1✔
381
                return False
1✔
382
            return all(
1✔
383
                is_covered_by(left_el, right_el)
384
                for (left_el, right_el) in zip(
385
                    left[6:-1].split(","), right[6:-1].split(",")
386
                )
387
            )
388
        if left == "bool" and right == "int":
1✔
389
            return True
1✔
390
        if left == "Any":
1✔
391
            return True
1✔
392

393
        return False
1✔
394

395
    def merge_into(left: str, right: typing.List[str]):
1✔
396
        # merge the set of types from left into the set of types from right, yielding a set that
397
        # covers both. None of the input sets contain Union as main element. Union may reside inside
398
        # List, or Dict, or Tuple.
399
        # This is needed when building a parent List, e.g. from its elements, and the
400
        # type of that list needs to be the union of the types of its elements.
401
        # if all elements have same type -- this is the type to write in List[type]
402
        # if not -- we write List[Union[type1, type2,...]].
403

404
        for right_el in right:
1✔
405
            if is_covered_by(right_el, left):
1✔
406
                right.remove(right_el)
1✔
407
                right.append(left)
1✔
408
                return
1✔
409
        if not any(is_covered_by(left, right_el) for right_el in right):
1✔
410
            right.append(left)
1✔
411

412
    def encode_a_list_of_type_names(list_of_type_names: typing.List[str]) -> str:
1✔
413
        # The type_names in the input are the set of names of all the elements of one list object,
414
        # or all the keys of one dict object, or all the val thereof, or all the type names of a specific position
415
        # in a tuple object The result should be a name of a type that covers them all.
416
        # So if, for example, the input contains both 'bool' and 'int', then 'int' suffices to cover both.
417
        # 'Any' can not show as a type_name of a basic (sub)object, but 'List[Any]' can show for an element of
418
        # a list object, an element that is an empty list. In such a case, if there are other elements in the input
419
        # that are more specific, e.g. 'List[str]' we should take the latter, and discard 'List[Any]' in order to get
420
        # a meaningful result: as narrow as possible but covers all.
421
        #
422
        to_ret = []
1✔
423
        for type_name in list_of_type_names:
1✔
424
            merge_into(type_name, to_ret)
1✔
425

426
        if len(to_ret) == 1:
1✔
427
            return to_ret[0]
1✔
428
        to_ret.sort()
1✔
429
        ans = "Union["
1✔
430
        for typ in to_ret[:-1]:
1✔
431
            ans += typ + ","
1✔
432
        return ans + to_ret[-1] + "]"
1✔
433

434
    basic_types = [bool, int, str, float]
1✔
435
    names_of_basic_types = ["bool", "int", "str", "float"]
1✔
436
    # bool should show before int, because bool is subtype of int
437

438
    for basic_type, name_of_basic_type in zip(basic_types, names_of_basic_types):
1✔
439
        if isinstance(obj, basic_type):
1✔
440
            return name_of_basic_type
1✔
441
    if isinstance(obj, list):
1✔
442
        included_types = set()
1✔
443
        for list_el in obj:
1✔
444
            included_types.add(infer_type_string(list_el))
1✔
445
        included_types = list(included_types)
1✔
446
        if len(included_types) == 0:
1✔
447
            return "List[Any]"
1✔
448
        return "List[" + encode_a_list_of_type_names(included_types) + "]"
1✔
449
    if isinstance(obj, dict):
1✔
450
        if len(obj) == 0:
1✔
451
            return "Dict[Any,Any]"
1✔
452
        included_key_types = set()
1✔
453
        included_val_types = set()
1✔
454
        for k, v in obj.items():
1✔
455
            included_key_types.add(infer_type_string(k))
1✔
456
            included_val_types.add(infer_type_string(v))
1✔
457
        included_key_types = list(included_key_types)
1✔
458
        included_val_types = list(included_val_types)
1✔
459
        return (
1✔
460
            "Dict["
461
            + encode_a_list_of_type_names(included_key_types)
462
            + ","
463
            + encode_a_list_of_type_names(included_val_types)
464
            + "]"
465
        )
466
    if isinstance(obj, tuple):
1✔
467
        if len(obj) == 0:
1✔
468
            return "Tuple[Any]"
1✔
469
        to_ret = "Tuple["
1✔
470
        for sub_tup in obj[:-1]:
1✔
471
            to_ret += infer_type_string(sub_tup) + ","
1✔
472
        return to_ret + infer_type_string(obj[-1]) + "]"
1✔
473

474
    return "Any"
1✔
475

476

477
def isoftype(object, typing_type):
1✔
478
    """Checks if an object is of a certain typing type, including nested types.
479

480
    This function supports simple types, typing types (List[int], Tuple[str, int]),
481
    nested typing types (List[List[int]], Tuple[List[str], int]), Literal, TypedDict,
482
    and NewType.
483

484
    Args:
485
        object: The object to check.
486
        typing_type: The typing type to check against.
487

488
    Returns:
489
        bool: True if the object is of the specified type, False otherwise.
490
    """
491
    if not is_type(typing_type):
1✔
492
        raise UnsupportedTypeError(typing_type)
1✔
493

494
    if hasattr(typing_type, "__verify_type__"):
1✔
495
        return typing_type.__verify_type__(object)
1✔
496

497
    if typing_type is typing.Type:
1✔
498
        return is_type(object)
×
499

500
    if is_new_type(typing_type):
1✔
501
        typing_type = typing_type.__supertype__
1✔
502

503
    if is_typed_dict(typing_type):
1✔
504
        if not isinstance(object, dict):
1✔
505
            return False
1✔
506

507
        # Only support total=True, check each field
508
        for key, expected_type in typing_type.__annotations__.items():
1✔
509
            # Check if field is Optional (Union with None)
510
            is_optional = (
1✔
511
                hasattr(expected_type, "__origin__")
512
                and expected_type.__origin__ is Union
513
                and type(None) in expected_type.__args__
514
            )
515

516
            if key not in object:
1✔
517
                # Field is missing - only allowed if it's Optional
518
                if not is_optional:
1✔
519
                    return False
1✔
520
            else:
521
                # Field is present - check type
522
                if not isoftype(object[key], expected_type):
1✔
523
                    return False
1✔
524

525
        return True
1✔
526

527
    if typing_type == typing.Any:
1✔
528
        return True
1✔
529

530
    if hasattr(typing_type, "__origin__"):
1✔
531
        origin = typing_type.__origin__
1✔
532
        type_args = typing.get_args(typing_type)
1✔
533

534
        if origin is Literal:
1✔
535
            return object in type_args
1✔
536

537
        if origin is typing.Union:
1✔
538
            return any(isoftype(object, sub_type) for sub_type in type_args)
1✔
539

540
        if not isinstance(object, origin):
1✔
541
            return False
1✔
542
        if origin is list or origin is set:
1✔
543
            return all(isoftype(element, type_args[0]) for element in object)
1✔
544
        if origin is dict:
1✔
545
            return all(
1✔
546
                isoftype(key, type_args[0]) and isoftype(value, type_args[1])
547
                for key, value in object.items()
548
            )
549
        if origin is tuple:
1✔
550
            return all(
1✔
551
                isoftype(element, type_arg)
552
                for element, type_arg in zip(object, type_args)
553
            )
554

555
    return isinstance(object, typing_type)
1✔
556

557

558
def strtype(typing_type) -> str:
1✔
559
    """Converts a typing type to its string representation.
560

561
    Args:
562
        typing_type (Any): The typing type to be converted. This can include standard types,
563
            custom types, or types from the `typing` module, such as `Literal`, `Union`,
564
            `List`, `Dict`, `Tuple`, `TypedDict`, and `NewType`.
565

566
    Returns:
567
        str: The string representation of the provided typing type.
568

569
    Raises:
570
        UnsupportedTypeError: If the provided `typing_type` is not a recognized type.
571

572
    Notes:
573
        - If `typing_type` is `Literal`, `NewType`, or `TypedDict`, the function returns
574
          the name of the type.
575
        - If `typing_type` is `Any`, it returns the string `"Any"`.
576
        - For other typing constructs like `Union`, `List`, `Dict`, and `Tuple`, the function
577
          recursively converts each part of the type to its string representation.
578
        - The function checks the `__origin__` attribute to determine the base type and formats
579
          the type arguments accordingly.
580
    """
581
    if isinstance(typing_type, str):
1✔
582
        return typing_type
×
583

584
    if not is_type(typing_type):
1✔
585
        raise UnsupportedTypeError(typing_type)
×
586

587
    if is_new_type(typing_type) or is_typed_dict(typing_type):
1✔
588
        return typing_type.__name__
1✔
589

590
    if typing_type == typing.Any:
1✔
591
        return "Any"
1✔
592

593
    if hasattr(typing_type, "__origin__"):
1✔
594
        origin = typing_type.__origin__
1✔
595
        type_args = typing.get_args(typing_type)
1✔
596

597
        if type_args[-1] is type(None):
1✔
598
            return (
1✔
599
                "Optional["
600
                + ", ".join([strtype(sub_type) for sub_type in type_args[:-1]])
601
                + "]"
602
            )
603

604
        if origin is Literal:
1✔
605
            return str(typing_type).replace("typing.", "")
1✔
606
        if origin is typing.Union:
1✔
607
            return (
1✔
608
                "Union["
609
                + ", ".join([strtype(sub_type) for sub_type in type_args])
610
                + "]"
611
            )
612
        if origin is list or origin is set:
1✔
613
            return "List[" + strtype(type_args[0]) + "]"
1✔
614
        if origin is set:
1✔
615
            return "Set[" + strtype(type_args[0]) + "]"
×
616
        if origin is dict:
1✔
617
            return "Dict[" + strtype(type_args[0]) + ", " + strtype(type_args[1]) + "]"
1✔
618
        if origin is tuple:
1✔
619
            return (
1✔
620
                "Tuple["
621
                + ", ".join([strtype(sub_type) for sub_type in type_args])
622
                + "]"
623
            )
624

625
    return typing_type.__name__
1✔
626

627

628
# copied from: https://github.com/bojiang/typing_utils/blob/main/typing_utils/__init__.py
629
# liscened under Apache License 2.0
630

631

632
if hasattr(typing, "ForwardRef"):  # python3.8
1✔
633
    ForwardRef = typing.ForwardRef
1✔
634
elif hasattr(typing, "_ForwardRef"):  # python3.6
×
635
    ForwardRef = typing._ForwardRef
×
636
else:
637
    raise NotImplementedError()
×
638

639

640
unknown = None
1✔
641

642

643
BUILTINS_MAPPING = {
1✔
644
    typing.List: list,
645
    typing.Set: set,
646
    typing.Dict: dict,
647
    typing.Tuple: tuple,
648
    typing.ByteString: bytes,  # https://docs.python.org/3/library/typing.html#typing.ByteString
649
    typing.Callable: collections.abc.Callable,
650
    typing.Sequence: collections.abc.Sequence,
651
    type(None): None,
652
}
653

654

655
STATIC_SUBTYPE_MAPPING: typing.Dict[type, typing.Type] = {
1✔
656
    io.TextIOWrapper: typing.TextIO,
657
    io.TextIOBase: typing.TextIO,
658
    io.StringIO: typing.TextIO,
659
    io.BufferedReader: typing.BinaryIO,
660
    io.BufferedWriter: typing.BinaryIO,
661
    io.BytesIO: typing.BinaryIO,
662
}
663

664

665
def optional_all(elements) -> typing.Optional[bool]:
1✔
666
    if all(elements):
1✔
667
        return True
1✔
668
    if all(e is False for e in elements):
1✔
669
        return False
1✔
670
    return unknown
×
671

672

673
def optional_any(elements) -> typing.Optional[bool]:
1✔
674
    if any(elements):
1✔
675
        return True
1✔
676
    if any(e is None for e in elements):
1✔
677
        return unknown
×
678
    return False
1✔
679

680

681
def _hashable(value):
1✔
682
    """Determine whether `value` can be hashed."""
683
    try:
1✔
684
        hash(value)
1✔
685
    except TypeError:
×
686
        return False
×
687
    return True
1✔
688

689

690
get_type_hints = typing.get_type_hints
1✔
691

692
GenericClass = type(typing.List)
1✔
693
UnionClass = type(typing.Union)
1✔
694

695
_Type = typing.Union[None, type, "typing.TypeVar"]
1✔
696
OriginType = typing.Union[None, type]
1✔
697
TypeArgs = typing.Union[type, typing.AbstractSet[type], typing.Sequence[type]]
1✔
698

699

700
def _normalize_aliases(type_: _Type) -> _Type:
1✔
701
    if isinstance(type_, typing.TypeVar):
1✔
702
        return type_
1✔
703

704
    assert _hashable(type_), "_normalize_aliases should only be called on element types"
1✔
705

706
    if type_ in BUILTINS_MAPPING:
1✔
707
        return BUILTINS_MAPPING[type_]
1✔
708
    return type_
1✔
709

710

711
def get_origin(type_):
1✔
712
    """Get the unsubscripted version of a type.
713

714
    This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.
715
    Return None for unsupported types.
716

717
    Examples:
718
        Here are some code examples using `get_origin` from the `typing_utils` module:
719

720
        .. code-block:: python
721

722
            from typing_utils import get_origin
723

724
            # Examples of get_origin usage
725
            get_origin(Literal[42]) is Literal  # True
726
            get_origin(int) is None  # True
727
            get_origin(ClassVar[int]) is ClassVar  # True
728
            get_origin(Generic) is Generic  # True
729
            get_origin(Generic[T]) is Generic  # True
730
            get_origin(Union[T, int]) is Union  # True
731
            get_origin(List[Tuple[T, T]][int]) == list  # True
732

733
    """
734
    if hasattr(typing, "get_origin"):  # python 3.8+
1✔
735
        _getter = typing.get_origin
1✔
736
        ori = _getter(type_)
1✔
737
    elif hasattr(typing.List, "_special"):  # python 3.7
×
738
        if isinstance(type_, GenericClass) and not type_._special:
×
739
            ori = type_.__origin__
×
740
        elif hasattr(type_, "_special") and type_._special:
×
741
            ori = type_
×
742
        elif type_ is typing.Generic:
×
743
            ori = typing.Generic
×
744
        else:
745
            ori = None
×
746
    else:  # python 3.6
747
        if isinstance(type_, GenericClass):
×
748
            ori = type_.__origin__
×
749
            if ori is None:
×
750
                ori = type_
×
751
        elif isinstance(type_, UnionClass):
×
752
            ori = type_.__origin__
×
753
        elif type_ is typing.Generic:
×
754
            ori = typing.Generic
×
755
        else:
756
            ori = None
×
757
    return _normalize_aliases(ori)
1✔
758

759

760
def get_args(type_) -> typing.Tuple:
1✔
761
    """Get type arguments with all substitutions performed.
762

763
    For unions, basic simplifications used by Union constructor are performed.
764

765
    Examples:
766
        Here are some code examples using `get_args` from the `typing_utils` module:
767

768
        .. code-block:: python
769

770
            from typing_utils import get_args
771

772
            # Examples of get_args usage
773
            get_args(Dict[str, int]) == (str, int)  # True
774
            get_args(int) == ()  # True
775
            get_args(Union[int, Union[T, int], str][int]) == (int, str)  # True
776
            get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])  # True
777
            get_args(Callable[[], T][int]) == ([], int)  # True
778
    """
779
    if hasattr(typing, "get_args"):  # python 3.8+
1✔
780
        _getter = typing.get_args
1✔
781
        res = _getter(type_)
1✔
782
    elif hasattr(typing.List, "_special"):  # python 3.7
×
783
        if (
×
784
            isinstance(type_, GenericClass) and not type_._special
785
        ):  # backport for python 3.8
786
            res = type_.__args__
×
787
            if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:
×
788
                res = (list(res[:-1]), res[-1])
×
789
        else:
790
            res = ()
×
791
    else:  # python 3.6
792
        if isinstance(type_, (GenericClass, UnionClass)):  # backport for python 3.8
×
793
            res = type_.__args__
×
794
            if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:
×
795
                res = (list(res[:-1]), res[-1])
×
796
        else:
797
            res = ()
×
798
    return () if res is None else res
1✔
799

800

801
def eval_forward_ref(ref, forward_refs=None):
1✔
802
    """Eval forward_refs in all cPython versions."""
803
    localns = forward_refs or {}
×
804

805
    if hasattr(typing, "_eval_type"):  # python3.8 & python 3.9
×
806
        _eval_type = typing._eval_type
×
807
        return _eval_type(ref, globals(), localns)
×
808

809
    if hasattr(ref, "_eval_type"):  # python3.6
×
810
        _eval_type = ref._eval_type
×
811
        return _eval_type(globals(), localns)
×
812

813
    raise NotImplementedError()
×
814

815

816
class NormalizedType(typing.NamedTuple):
1✔
817
    """Normalized type, made it possible to compare, hash between types."""
818

819
    origin: _Type
1✔
820
    args: typing.Union[tuple, frozenset] = ()
1✔
821

822
    def __eq__(self, other):
1✔
823
        if isinstance(other, NormalizedType):
1✔
824
            if self.origin != other.origin:
1✔
825
                return False
×
826
            if isinstance(self.args, frozenset) and isinstance(other.args, frozenset):
1✔
827
                return self.args <= other.args and other.args <= self.args
1✔
828
            return self.origin == other.origin and self.args == other.args
1✔
829
        if not self.args:
×
830
            return self.origin == other
×
831
        return False
×
832

833
    def __hash__(self) -> int:
1✔
834
        if not self.args:
1✔
835
            return hash(self.origin)
1✔
836
        return hash((self.origin, self.args))
1✔
837

838
    def __repr__(self):
1✔
839
        if not self.args:
×
840
            return f"{self.origin}"
×
841
        return f"{self.origin}[{self.args}])"
×
842

843

844
@lru_cache(maxsize=None)
1✔
845
def _normalize_args(tps: TypeArgs):
1✔
846
    if isinstance(tps, str):
1✔
847
        return tps
1✔
848
    if isinstance(tps, collections.abc.Sequence):
1✔
849
        return tuple(_normalize_args(type_) for type_ in tps)
1✔
850
    if isinstance(tps, collections.abc.Set):
1✔
851
        return frozenset(_normalize_args(type_) for type_ in tps)
1✔
852
    return normalize(tps)
1✔
853

854

855
def normalize(type_: _Type) -> NormalizedType:
1✔
856
    """Convert types to NormalizedType instances."""
857
    args = get_args(type_)
1✔
858
    origin = get_origin(type_)
1✔
859
    if not origin:
1✔
860
        return NormalizedType(_normalize_aliases(type_))
1✔
861
    origin = _normalize_aliases(origin)
1✔
862

863
    if origin is typing.Union:  # sort args when the origin is Union
1✔
864
        args = _normalize_args(frozenset(args))
1✔
865
    else:
866
        args = _normalize_args(args)
1✔
867
    return NormalizedType(origin, args)
1✔
868

869

870
def _is_origin_subtype(left: OriginType, right: OriginType) -> bool:
1✔
871
    if left is right:
1✔
872
        return True
1✔
873

874
    if (
1✔
875
        left is not None
876
        and left in STATIC_SUBTYPE_MAPPING
877
        and right == STATIC_SUBTYPE_MAPPING[left]
878
    ):
879
        return True
×
880

881
    if hasattr(left, "mro"):
1✔
882
        for parent in left.mro():
1✔
883
            if parent == right:
1✔
884
                return True
1✔
885

886
    if isinstance(left, type) and isinstance(right, type):
1✔
887
        return issubclass(left, right)
1✔
888

889
    return left == right
1✔
890

891

892
NormalizedTypeArgs = typing.Union[
1✔
893
    typing.Tuple["NormalizedTypeArgs", ...],
894
    typing.FrozenSet[NormalizedType],
895
    NormalizedType,
896
]
897

898

899
def _is_origin_subtype_args(
1✔
900
    left: NormalizedTypeArgs,
901
    right: NormalizedTypeArgs,
902
    forward_refs: typing.Optional[typing.Mapping[str, type]],
903
) -> typing.Optional[bool]:
904
    if isinstance(left, frozenset):
1✔
905
        if not isinstance(right, frozenset):
1✔
906
            return False
×
907

908
        excluded = left - right
1✔
909
        if not excluded:
1✔
910
            # Union[str, int] <> Union[int, str]
911
            return True
×
912

913
        # Union[list, int] <> Union[typing.Sequence, int]
914
        return all(
1✔
915
            any(_is_normal_subtype(e, r, forward_refs) for r in right) for e in excluded
916
        )
917

918
    if isinstance(left, collections.abc.Sequence) and not isinstance(
1✔
919
        left, NormalizedType
920
    ):
921
        if not isinstance(right, collections.abc.Sequence) or isinstance(
1✔
922
            right, NormalizedType
923
        ):
924
            return False
×
925

926
        if (
1✔
927
            left
928
            and left[-1].origin is not Ellipsis
929
            and right
930
            and right[-1].origin is Ellipsis
931
        ):
932
            # Tuple[type, type] <> Tuple[type, ...]
933
            return all(
×
934
                _is_origin_subtype_args(lft, right[0], forward_refs) for lft in left
935
            )
936

937
        if len(left) != len(right):
1✔
938
            return False
1✔
939

940
        return all(
1✔
941
            lft is not None
942
            and rgt is not None
943
            and _is_origin_subtype_args(lft, rgt, forward_refs)
944
            for lft, rgt in itertools.zip_longest(left, right)
945
        )
946

947
    assert isinstance(left, NormalizedType)
1✔
948
    assert isinstance(right, NormalizedType)
1✔
949

950
    return _is_normal_subtype(left, right, forward_refs)
1✔
951

952

953
@lru_cache(maxsize=None)
1✔
954
def _is_normal_subtype(
1✔
955
    left: NormalizedType,
956
    right: NormalizedType,
957
    forward_refs: typing.Optional[typing.Mapping[str, type]],
958
) -> typing.Optional[bool]:
959
    if isinstance(left.origin, ForwardRef):
1✔
960
        left = normalize(eval_forward_ref(left.origin, forward_refs=forward_refs))
×
961

962
    if isinstance(right.origin, ForwardRef):
1✔
963
        right = normalize(eval_forward_ref(right.origin, forward_refs=forward_refs))
×
964

965
    # Any
966
    if right.origin is typing.Any:
1✔
967
        return True
×
968

969
    # Union
970
    if right.origin is typing.Union and left.origin is typing.Union:
1✔
971
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
972
    if right.origin is typing.Union:
1✔
973
        return optional_any(
1✔
974
            _is_normal_subtype(left, a, forward_refs) for a in right.args
975
        )
976
    if left.origin is typing.Union:
1✔
977
        return optional_all(
1✔
978
            _is_normal_subtype(a, right, forward_refs) for a in left.args
979
        )
980

981
    # TypeVar
982
    if isinstance(left.origin, typing.TypeVar) and isinstance(
1✔
983
        right.origin, typing.TypeVar
984
    ):
985
        if left.origin is right.origin:
×
986
            return True
×
987

988
        left_bound = getattr(left.origin, "__bound__", None)
×
989
        right_bound = getattr(right.origin, "__bound__", None)
×
990
        if right_bound is None or left_bound is None:
×
991
            return unknown
×
992
        return _is_normal_subtype(
×
993
            normalize(left_bound), normalize(right_bound), forward_refs
994
        )
995
    if isinstance(right.origin, typing.TypeVar):
1✔
996
        return unknown
×
997
    if isinstance(left.origin, typing.TypeVar):
1✔
998
        left_bound = getattr(left.origin, "__bound__", None)
×
999
        if left_bound is None:
×
1000
            return unknown
×
1001
        return _is_normal_subtype(normalize(left_bound), right, forward_refs)
×
1002

1003
    if not left.args and not right.args:
1✔
1004
        return _is_origin_subtype(left.origin, right.origin)
1✔
1005

1006
    if not right.args:
1✔
1007
        return _is_origin_subtype(left.origin, right.origin)
1✔
1008

1009
    if _is_origin_subtype(left.origin, right.origin):
1✔
1010
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
1011

1012
    return False
1✔
1013

1014

1015
def issubtype(
1✔
1016
    left: _Type,
1017
    right: _Type,
1018
    forward_refs: typing.Optional[dict] = None,
1019
) -> typing.Optional[bool]:
1020
    """Check that the left argument is a subtype of the right.
1021

1022
    For unions, check if the type arguments of the left is a subset of the right.
1023
    Also works for nested types including ForwardRefs.
1024

1025
    Examples:
1026
        Here are some code examples using `issubtype` from the `typing_utils` module:
1027

1028
        .. code-block:: python
1029

1030
            from typing_utils import issubtype
1031

1032
            # Examples of issubtype checks
1033
            issubtype(typing.List, typing.Any)  # True
1034
            issubtype(list, list)  # True
1035
            issubtype(list, typing.List)  # True
1036
            issubtype(list, typing.Sequence)  # True
1037
            issubtype(typing.List[int], list)  # True
1038
            issubtype(typing.List[typing.List], list)  # True
1039
            issubtype(list, typing.List[int])  # False
1040
            issubtype(list, typing.Union[typing.Tuple, typing.Set])  # False
1041
            issubtype(typing.List[typing.List], typing.List[typing.Sequence])  # True
1042

1043
            # Example with custom JSON type
1044
            JSON = typing.Union[
1045
                int, float, bool, str, None, typing.Sequence["JSON"],
1046
                typing.Mapping[str, "JSON"]
1047
            ]
1048
            issubtype(str, JSON, forward_refs={'JSON': JSON})  # True
1049
            issubtype(typing.Dict[str, str], JSON, forward_refs={'JSON': JSON})  # True
1050
            issubtype(typing.Dict[str, bytes], JSON, forward_refs={'JSON': JSON})  # False
1051
    """
1052
    return _is_normal_subtype(normalize(left), normalize(right), forward_refs)
1✔
1053

1054

1055
def to_float_or_default(v, failure_default=0):
1✔
1056
    try:
1✔
1057
        return float(v)
1✔
1058
    except Exception as e:
1059
        if failure_default is None:
1060
            raise e
1061
        return failure_default
1062

1063

1064
def verify_required_schema(
1✔
1065
    required_schema_dict: Dict[str, type],
1066
    input_dict: Dict[str, Any],
1067
    class_name: str,
1068
    id: Optional[str] = "",
1069
    description: Optional[str] = "",
1070
) -> None:
1071
    """Verifies if passed input_dict has all required fields, and they are of proper types according to required_schema_dict.
1072

1073
    Parameters:
1074
        required_schema_dict (Dict[str, str]):
1075
            Schema where a key is name of a field and a value is a string
1076
            representing a type of its value.
1077
        input_dict (Dict[str, Any]):
1078
            Dict with input fields and their respective values.
1079
    """
1080
    for field_name, data_type in required_schema_dict.items():
1✔
1081
        try:
1✔
1082
            value = input_dict[field_name]
1✔
1083
        except KeyError as e:
1✔
1084
            raise Exception(
1✔
1085
                f"The {class_name} ('{id}') expected a field '{field_name}' which the input instance did not contain.\n"
1086
                f"The input instance fields are  : {list(input_dict.keys())}.\n"
1087
                f"{class_name} description: {description}"
1088
            ) from e
1089

1090
        try:
1✔
1091
            valid = isoftype(value, data_type)
1✔
1092
        except Exception as e:
1093
            raise ValueError(
1094
                f"Passed value {value} of field '{field_name}' is not "
1095
                f"of required type: ({to_type_string(data_type)}) in {class_name} ('{id}').\n"
1096
                f"{class_name} description: {description}\nReason:\n{e}"
1097
            ) from e
1098

1099
        if not valid:
1✔
1100
            raise ValueError(
1101
                f"Passed value {value} of field '{field_name}' is not "
1102
                f"of required type: ({to_type_string(data_type)}) in {class_name} ('{id}').\n"
1103
                f"{class_name} description: {description}"
1104
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc