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

IBM / unitxt / 15581348656

11 Jun 2025 09:34AM UTC coverage: 80.241% (-0.007%) from 80.248%
15581348656

push

github

web-flow
Bluebench fixes (#1828)

* Fix Arena Hard template to use latest llama judge model (used in Bluebench)

Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>

* Update the Arena Hard recipe in Bluebench to use llama-3-3-70b-instruct as judge.

Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>

* Add a requirements section for bluebench

Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>

---------

Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>

1689 of 2081 branches covered (81.16%)

Branch coverage included in aggregate %.

10478 of 13082 relevant lines covered (80.09%)

0.8 hits per line

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

83.05
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
1✔
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:
1✔
138
                candidate_end = string.find("]", candidate_end + 1)
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
        for key, expected_type in typing_type.__annotations__.items():
1✔
507
            if key not in object or not isoftype(object[key], expected_type):
1✔
508
                return False
1✔
509
        return True
1✔
510

511
    if typing_type == typing.Any:
1✔
512
        return True
1✔
513

514
    if hasattr(typing_type, "__origin__"):
1✔
515
        origin = typing_type.__origin__
1✔
516
        type_args = typing.get_args(typing_type)
1✔
517

518
        if origin is Literal:
1✔
519
            return object in type_args
1✔
520

521
        if origin is typing.Union:
1✔
522
            return any(isoftype(object, sub_type) for sub_type in type_args)
1✔
523

524
        if not isinstance(object, origin):
1✔
525
            return False
1✔
526
        if origin is list or origin is set:
1✔
527
            return all(isoftype(element, type_args[0]) for element in object)
1✔
528
        if origin is dict:
1✔
529
            return all(
1✔
530
                isoftype(key, type_args[0]) and isoftype(value, type_args[1])
531
                for key, value in object.items()
532
            )
533
        if origin is tuple:
1✔
534
            return all(
1✔
535
                isoftype(element, type_arg)
536
                for element, type_arg in zip(object, type_args)
537
            )
538

539
    return isinstance(object, typing_type)
1✔
540

541

542
def strtype(typing_type) -> str:
1✔
543
    """Converts a typing type to its string representation.
544

545
    Args:
546
        typing_type (Any): The typing type to be converted. This can include standard types,
547
            custom types, or types from the `typing` module, such as `Literal`, `Union`,
548
            `List`, `Dict`, `Tuple`, `TypedDict`, and `NewType`.
549

550
    Returns:
551
        str: The string representation of the provided typing type.
552

553
    Raises:
554
        UnsupportedTypeError: If the provided `typing_type` is not a recognized type.
555

556
    Notes:
557
        - If `typing_type` is `Literal`, `NewType`, or `TypedDict`, the function returns
558
          the name of the type.
559
        - If `typing_type` is `Any`, it returns the string `"Any"`.
560
        - For other typing constructs like `Union`, `List`, `Dict`, and `Tuple`, the function
561
          recursively converts each part of the type to its string representation.
562
        - The function checks the `__origin__` attribute to determine the base type and formats
563
          the type arguments accordingly.
564
    """
565
    if isinstance(typing_type, str):
1✔
566
        return typing_type
×
567

568
    if not is_type(typing_type):
1✔
569
        raise UnsupportedTypeError(typing_type)
×
570

571
    if is_new_type(typing_type) or is_typed_dict(typing_type):
1✔
572
        return typing_type.__name__
1✔
573

574
    if typing_type == typing.Any:
1✔
575
        return "Any"
1✔
576

577
    if hasattr(typing_type, "__origin__"):
1✔
578
        origin = typing_type.__origin__
1✔
579
        type_args = typing.get_args(typing_type)
1✔
580

581
        if type_args[-1] is type(None):
1✔
582
            return (
1✔
583
                "Optional["
584
                + ", ".join([strtype(sub_type) for sub_type in type_args[:-1]])
585
                + "]"
586
            )
587

588
        if origin is Literal:
1✔
589
            return str(typing_type).replace("typing.", "")
1✔
590
        if origin is typing.Union:
1✔
591
            return (
1✔
592
                "Union["
593
                + ", ".join([strtype(sub_type) for sub_type in type_args])
594
                + "]"
595
            )
596
        if origin is list or origin is set:
1✔
597
            return "List[" + strtype(type_args[0]) + "]"
1✔
598
        if origin is set:
1✔
599
            return "Set[" + strtype(type_args[0]) + "]"
×
600
        if origin is dict:
1✔
601
            return "Dict[" + strtype(type_args[0]) + ", " + strtype(type_args[1]) + "]"
1✔
602
        if origin is tuple:
1✔
603
            return (
1✔
604
                "Tuple["
605
                + ", ".join([strtype(sub_type) for sub_type in type_args])
606
                + "]"
607
            )
608

609
    return typing_type.__name__
1✔
610

611

612
# copied from: https://github.com/bojiang/typing_utils/blob/main/typing_utils/__init__.py
613
# liscened under Apache License 2.0
614

615

616
if hasattr(typing, "ForwardRef"):  # python3.8
1✔
617
    ForwardRef = typing.ForwardRef
1✔
618
elif hasattr(typing, "_ForwardRef"):  # python3.6
×
619
    ForwardRef = typing._ForwardRef
×
620
else:
621
    raise NotImplementedError()
×
622

623

624
unknown = None
1✔
625

626

627
BUILTINS_MAPPING = {
1✔
628
    typing.List: list,
629
    typing.Set: set,
630
    typing.Dict: dict,
631
    typing.Tuple: tuple,
632
    typing.ByteString: bytes,  # https://docs.python.org/3/library/typing.html#typing.ByteString
633
    typing.Callable: collections.abc.Callable,
634
    typing.Sequence: collections.abc.Sequence,
635
    type(None): None,
636
}
637

638

639
STATIC_SUBTYPE_MAPPING: typing.Dict[type, typing.Type] = {
1✔
640
    io.TextIOWrapper: typing.TextIO,
641
    io.TextIOBase: typing.TextIO,
642
    io.StringIO: typing.TextIO,
643
    io.BufferedReader: typing.BinaryIO,
644
    io.BufferedWriter: typing.BinaryIO,
645
    io.BytesIO: typing.BinaryIO,
646
}
647

648

649
def optional_all(elements) -> typing.Optional[bool]:
1✔
650
    if all(elements):
1✔
651
        return True
1✔
652
    if all(e is False for e in elements):
1✔
653
        return False
1✔
654
    return unknown
×
655

656

657
def optional_any(elements) -> typing.Optional[bool]:
1✔
658
    if any(elements):
1✔
659
        return True
1✔
660
    if any(e is None for e in elements):
1✔
661
        return unknown
×
662
    return False
1✔
663

664

665
def _hashable(value):
1✔
666
    """Determine whether `value` can be hashed."""
667
    try:
1✔
668
        hash(value)
1✔
669
    except TypeError:
×
670
        return False
×
671
    return True
1✔
672

673

674
get_type_hints = typing.get_type_hints
1✔
675

676
GenericClass = type(typing.List)
1✔
677
UnionClass = type(typing.Union)
1✔
678

679
_Type = typing.Union[None, type, "typing.TypeVar"]
1✔
680
OriginType = typing.Union[None, type]
1✔
681
TypeArgs = typing.Union[type, typing.AbstractSet[type], typing.Sequence[type]]
1✔
682

683

684
def _normalize_aliases(type_: _Type) -> _Type:
1✔
685
    if isinstance(type_, typing.TypeVar):
1✔
686
        return type_
1✔
687

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

690
    if type_ in BUILTINS_MAPPING:
1✔
691
        return BUILTINS_MAPPING[type_]
1✔
692
    return type_
1✔
693

694

695
def get_origin(type_):
1✔
696
    """Get the unsubscripted version of a type.
697

698
    This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.
699
    Return None for unsupported types.
700

701
    Examples:
702
        Here are some code examples using `get_origin` from the `typing_utils` module:
703

704
        .. code-block:: python
705

706
            from typing_utils import get_origin
707

708
            # Examples of get_origin usage
709
            get_origin(Literal[42]) is Literal  # True
710
            get_origin(int) is None  # True
711
            get_origin(ClassVar[int]) is ClassVar  # True
712
            get_origin(Generic) is Generic  # True
713
            get_origin(Generic[T]) is Generic  # True
714
            get_origin(Union[T, int]) is Union  # True
715
            get_origin(List[Tuple[T, T]][int]) == list  # True
716

717
    """
718
    if hasattr(typing, "get_origin"):  # python 3.8+
1✔
719
        _getter = typing.get_origin
1✔
720
        ori = _getter(type_)
1✔
721
    elif hasattr(typing.List, "_special"):  # python 3.7
×
722
        if isinstance(type_, GenericClass) and not type_._special:
×
723
            ori = type_.__origin__
×
724
        elif hasattr(type_, "_special") and type_._special:
×
725
            ori = type_
×
726
        elif type_ is typing.Generic:
×
727
            ori = typing.Generic
×
728
        else:
729
            ori = None
×
730
    else:  # python 3.6
731
        if isinstance(type_, GenericClass):
×
732
            ori = type_.__origin__
×
733
            if ori is None:
×
734
                ori = type_
×
735
        elif isinstance(type_, UnionClass):
×
736
            ori = type_.__origin__
×
737
        elif type_ is typing.Generic:
×
738
            ori = typing.Generic
×
739
        else:
740
            ori = None
×
741
    return _normalize_aliases(ori)
1✔
742

743

744
def get_args(type_) -> typing.Tuple:
1✔
745
    """Get type arguments with all substitutions performed.
746

747
    For unions, basic simplifications used by Union constructor are performed.
748

749
    Examples:
750
        Here are some code examples using `get_args` from the `typing_utils` module:
751

752
        .. code-block:: python
753

754
            from typing_utils import get_args
755

756
            # Examples of get_args usage
757
            get_args(Dict[str, int]) == (str, int)  # True
758
            get_args(int) == ()  # True
759
            get_args(Union[int, Union[T, int], str][int]) == (int, str)  # True
760
            get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])  # True
761
            get_args(Callable[[], T][int]) == ([], int)  # True
762
    """
763
    if hasattr(typing, "get_args"):  # python 3.8+
1✔
764
        _getter = typing.get_args
1✔
765
        res = _getter(type_)
1✔
766
    elif hasattr(typing.List, "_special"):  # python 3.7
×
767
        if (
×
768
            isinstance(type_, GenericClass) and not type_._special
769
        ):  # backport for python 3.8
770
            res = type_.__args__
×
771
            if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:
×
772
                res = (list(res[:-1]), res[-1])
×
773
        else:
774
            res = ()
×
775
    else:  # python 3.6
776
        if isinstance(type_, (GenericClass, UnionClass)):  # backport for python 3.8
×
777
            res = type_.__args__
×
778
            if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:
×
779
                res = (list(res[:-1]), res[-1])
×
780
        else:
781
            res = ()
×
782
    return () if res is None else res
1✔
783

784

785
def eval_forward_ref(ref, forward_refs=None):
1✔
786
    """Eval forward_refs in all cPython versions."""
787
    localns = forward_refs or {}
×
788

789
    if hasattr(typing, "_eval_type"):  # python3.8 & python 3.9
×
790
        _eval_type = typing._eval_type
×
791
        return _eval_type(ref, globals(), localns)
×
792

793
    if hasattr(ref, "_eval_type"):  # python3.6
×
794
        _eval_type = ref._eval_type
×
795
        return _eval_type(globals(), localns)
×
796

797
    raise NotImplementedError()
×
798

799

800
class NormalizedType(typing.NamedTuple):
1✔
801
    """Normalized type, made it possible to compare, hash between types."""
802

803
    origin: _Type
1✔
804
    args: typing.Union[tuple, frozenset] = ()
1✔
805

806
    def __eq__(self, other):
1✔
807
        if isinstance(other, NormalizedType):
1✔
808
            if self.origin != other.origin:
1✔
809
                return False
×
810
            if isinstance(self.args, frozenset) and isinstance(other.args, frozenset):
1✔
811
                return self.args <= other.args and other.args <= self.args
1✔
812
            return self.origin == other.origin and self.args == other.args
1✔
813
        if not self.args:
×
814
            return self.origin == other
×
815
        return False
×
816

817
    def __hash__(self) -> int:
1✔
818
        if not self.args:
1✔
819
            return hash(self.origin)
1✔
820
        return hash((self.origin, self.args))
1✔
821

822
    def __repr__(self):
1✔
823
        if not self.args:
×
824
            return f"{self.origin}"
×
825
        return f"{self.origin}[{self.args}])"
×
826

827

828
@lru_cache(maxsize=None)
1✔
829
def _normalize_args(tps: TypeArgs):
1✔
830
    if isinstance(tps, str):
1✔
831
        return tps
1✔
832
    if isinstance(tps, collections.abc.Sequence):
1✔
833
        return tuple(_normalize_args(type_) for type_ in tps)
1✔
834
    if isinstance(tps, collections.abc.Set):
1✔
835
        return frozenset(_normalize_args(type_) for type_ in tps)
1✔
836
    return normalize(tps)
1✔
837

838

839
def normalize(type_: _Type) -> NormalizedType:
1✔
840
    """Convert types to NormalizedType instances."""
841
    args = get_args(type_)
1✔
842
    origin = get_origin(type_)
1✔
843
    if not origin:
1✔
844
        return NormalizedType(_normalize_aliases(type_))
1✔
845
    origin = _normalize_aliases(origin)
1✔
846

847
    if origin is typing.Union:  # sort args when the origin is Union
1✔
848
        args = _normalize_args(frozenset(args))
1✔
849
    else:
850
        args = _normalize_args(args)
1✔
851
    return NormalizedType(origin, args)
1✔
852

853

854
def _is_origin_subtype(left: OriginType, right: OriginType) -> bool:
1✔
855
    if left is right:
1✔
856
        return True
1✔
857

858
    if (
1✔
859
        left is not None
860
        and left in STATIC_SUBTYPE_MAPPING
861
        and right == STATIC_SUBTYPE_MAPPING[left]
862
    ):
863
        return True
×
864

865
    if hasattr(left, "mro"):
1✔
866
        for parent in left.mro():
1✔
867
            if parent == right:
1✔
868
                return True
1✔
869

870
    if isinstance(left, type) and isinstance(right, type):
1✔
871
        return issubclass(left, right)
1✔
872

873
    return left == right
1✔
874

875

876
NormalizedTypeArgs = typing.Union[
1✔
877
    typing.Tuple["NormalizedTypeArgs", ...],
878
    typing.FrozenSet[NormalizedType],
879
    NormalizedType,
880
]
881

882

883
def _is_origin_subtype_args(
1✔
884
    left: NormalizedTypeArgs,
885
    right: NormalizedTypeArgs,
886
    forward_refs: typing.Optional[typing.Mapping[str, type]],
887
) -> typing.Optional[bool]:
888
    if isinstance(left, frozenset):
1✔
889
        if not isinstance(right, frozenset):
1✔
890
            return False
×
891

892
        excluded = left - right
1✔
893
        if not excluded:
1✔
894
            # Union[str, int] <> Union[int, str]
895
            return True
×
896

897
        # Union[list, int] <> Union[typing.Sequence, int]
898
        return all(
1✔
899
            any(_is_normal_subtype(e, r, forward_refs) for r in right) for e in excluded
900
        )
901

902
    if isinstance(left, collections.abc.Sequence) and not isinstance(
1✔
903
        left, NormalizedType
904
    ):
905
        if not isinstance(right, collections.abc.Sequence) or isinstance(
1✔
906
            right, NormalizedType
907
        ):
908
            return False
×
909

910
        if (
1✔
911
            left
912
            and left[-1].origin is not Ellipsis
913
            and right
914
            and right[-1].origin is Ellipsis
915
        ):
916
            # Tuple[type, type] <> Tuple[type, ...]
917
            return all(
×
918
                _is_origin_subtype_args(lft, right[0], forward_refs) for lft in left
919
            )
920

921
        if len(left) != len(right):
1✔
922
            return False
1✔
923

924
        return all(
1✔
925
            lft is not None
926
            and rgt is not None
927
            and _is_origin_subtype_args(lft, rgt, forward_refs)
928
            for lft, rgt in itertools.zip_longest(left, right)
929
        )
930

931
    assert isinstance(left, NormalizedType)
1✔
932
    assert isinstance(right, NormalizedType)
1✔
933

934
    return _is_normal_subtype(left, right, forward_refs)
1✔
935

936

937
@lru_cache(maxsize=None)
1✔
938
def _is_normal_subtype(
1✔
939
    left: NormalizedType,
940
    right: NormalizedType,
941
    forward_refs: typing.Optional[typing.Mapping[str, type]],
942
) -> typing.Optional[bool]:
943
    if isinstance(left.origin, ForwardRef):
1✔
944
        left = normalize(eval_forward_ref(left.origin, forward_refs=forward_refs))
×
945

946
    if isinstance(right.origin, ForwardRef):
1✔
947
        right = normalize(eval_forward_ref(right.origin, forward_refs=forward_refs))
×
948

949
    # Any
950
    if right.origin is typing.Any:
1✔
951
        return True
×
952

953
    # Union
954
    if right.origin is typing.Union and left.origin is typing.Union:
1✔
955
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
956
    if right.origin is typing.Union:
1✔
957
        return optional_any(
1✔
958
            _is_normal_subtype(left, a, forward_refs) for a in right.args
959
        )
960
    if left.origin is typing.Union:
1✔
961
        return optional_all(
1✔
962
            _is_normal_subtype(a, right, forward_refs) for a in left.args
963
        )
964

965
    # TypeVar
966
    if isinstance(left.origin, typing.TypeVar) and isinstance(
1✔
967
        right.origin, typing.TypeVar
968
    ):
969
        if left.origin is right.origin:
×
970
            return True
×
971

972
        left_bound = getattr(left.origin, "__bound__", None)
×
973
        right_bound = getattr(right.origin, "__bound__", None)
×
974
        if right_bound is None or left_bound is None:
×
975
            return unknown
×
976
        return _is_normal_subtype(
×
977
            normalize(left_bound), normalize(right_bound), forward_refs
978
        )
979
    if isinstance(right.origin, typing.TypeVar):
1✔
980
        return unknown
×
981
    if isinstance(left.origin, typing.TypeVar):
1✔
982
        left_bound = getattr(left.origin, "__bound__", None)
×
983
        if left_bound is None:
×
984
            return unknown
×
985
        return _is_normal_subtype(normalize(left_bound), right, forward_refs)
×
986

987
    if not left.args and not right.args:
1✔
988
        return _is_origin_subtype(left.origin, right.origin)
1✔
989

990
    if not right.args:
1✔
991
        return _is_origin_subtype(left.origin, right.origin)
1✔
992

993
    if _is_origin_subtype(left.origin, right.origin):
1✔
994
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
995

996
    return False
1✔
997

998

999
def issubtype(
1✔
1000
    left: _Type,
1001
    right: _Type,
1002
    forward_refs: typing.Optional[dict] = None,
1003
) -> typing.Optional[bool]:
1004
    """Check that the left argument is a subtype of the right.
1005

1006
    For unions, check if the type arguments of the left is a subset of the right.
1007
    Also works for nested types including ForwardRefs.
1008

1009
    Examples:
1010
        Here are some code examples using `issubtype` from the `typing_utils` module:
1011

1012
        .. code-block:: python
1013

1014
            from typing_utils import issubtype
1015

1016
            # Examples of issubtype checks
1017
            issubtype(typing.List, typing.Any)  # True
1018
            issubtype(list, list)  # True
1019
            issubtype(list, typing.List)  # True
1020
            issubtype(list, typing.Sequence)  # True
1021
            issubtype(typing.List[int], list)  # True
1022
            issubtype(typing.List[typing.List], list)  # True
1023
            issubtype(list, typing.List[int])  # False
1024
            issubtype(list, typing.Union[typing.Tuple, typing.Set])  # False
1025
            issubtype(typing.List[typing.List], typing.List[typing.Sequence])  # True
1026

1027
            # Example with custom JSON type
1028
            JSON = typing.Union[
1029
                int, float, bool, str, None, typing.Sequence["JSON"],
1030
                typing.Mapping[str, "JSON"]
1031
            ]
1032
            issubtype(str, JSON, forward_refs={'JSON': JSON})  # True
1033
            issubtype(typing.Dict[str, str], JSON, forward_refs={'JSON': JSON})  # True
1034
            issubtype(typing.Dict[str, bytes], JSON, forward_refs={'JSON': JSON})  # False
1035
    """
1036
    return _is_normal_subtype(normalize(left), normalize(right), forward_refs)
1✔
1037

1038

1039
def to_float_or_default(v, failure_default=0):
1✔
1040
    try:
1✔
1041
        return float(v)
1✔
1042
    except Exception as e:
1✔
1043
        if failure_default is None:
1✔
1044
            raise e
1✔
1045
        return failure_default
1✔
1046

1047

1048
def verify_required_schema(
1✔
1049
    required_schema_dict: Dict[str, type],
1050
    input_dict: Dict[str, Any],
1051
    class_name: str,
1052
    id: Optional[str] = "",
1053
    description: Optional[str] = "",
1054
) -> None:
1055
    """Verifies if passed input_dict has all required fields, and they are of proper types according to required_schema_dict.
1056

1057
    Parameters:
1058
        required_schema_dict (Dict[str, str]):
1059
            Schema where a key is name of a field and a value is a string
1060
            representing a type of its value.
1061
        input_dict (Dict[str, Any]):
1062
            Dict with input fields and their respective values.
1063
    """
1064
    for field_name, data_type in required_schema_dict.items():
1✔
1065
        try:
1✔
1066
            value = input_dict[field_name]
1✔
1067
        except KeyError as e:
1✔
1068
            raise Exception(
1✔
1069
                f"The {class_name} ('{id}') expected a field '{field_name}' which the input instance did not contain.\n"
1070
                f"The input instance fields are  : {list(input_dict.keys())}.\n"
1071
                f"{class_name} description: {description}"
1072
            ) from e
1073

1074
        try:
1✔
1075
            valid = isoftype(value, data_type)
1✔
1076
        except Exception as e:
×
1077
            raise ValueError(
×
1078
                f"Passed value {value} of field '{field_name}' is not "
1079
                f"of required type: ({to_type_string(data_type)}) in {class_name} ('{id}').\n"
1080
                f"{class_name} description: {description}\nReason:\n{e}"
1081
            ) from e
1082

1083
        if not valid:
1✔
1084
            raise ValueError(
1✔
1085
                f"Passed value {value} of field '{field_name}' is not "
1086
                f"of required type: ({to_type_string(data_type)}) in {class_name} ('{id}').\n"
1087
                f"{class_name} description: {description}"
1088
            )
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