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

IBM / unitxt / 14707293951

28 Apr 2025 12:02PM UTC coverage: 80.149% (+0.1%) from 80.035%
14707293951

push

github

web-flow
Add tool calling support + Berekley Tool Calling Benchmark (simple-v3) (#1764)

* Add tool calling support + Berekley Tool Calling Benchmark

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Update model in evaluate_tool_calling example to granite-3-3-8b-instruct

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Add support for typing.Type in is_type function and update tests

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Test tool calling utils

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Add tests tool calling metric

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Update bfcl with dataset description

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Add title to Berkeley Function Calling Leaderboard description

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Add comprehensive tutorial on tool calling with Unitxt and Berkeley Function Calling Leaderboard dataset

Signed-off-by: elronbandel <elronbandel@gmail.com>

* Update title for Berkeley Function Calling Leaderboard - Simple V3

Signed-off-by: elronbandel <elronbandel@gmail.com>

---------

Signed-off-by: elronbandel <elronbandel@gmail.com>

1643 of 2034 branches covered (80.78%)

Branch coverage included in aggregate %.

10268 of 12827 relevant lines covered (80.05%)

0.8 hits per line

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

83.17
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 is_new_type(new_type) or is_typed_dict(
1✔
29
        new_type
30
    ), "Can register only typing.NewType or typing.TypedDict"
31
    _registered_types[new_type.__name__] = new_type
1✔
32

33

34
Type = typing.Any
1✔
35

36

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

44

45
class GenericTypedDict(TypedDict):
1✔
46
    pass
1✔
47

48

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

59
_generics_types = [type(t) for t in _generics]
1✔
60

61

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

65

66
def is_typed_dict(object):
1✔
67
    return isinstance(object, type(GenericTypedDict))
1✔
68

69

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

80

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

92

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

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

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

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

116

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

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

124
    """
125

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

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

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

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

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

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

184

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

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

193
    Returns:
194
        str: A formatted type string.
195

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

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

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

221

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

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

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

232
    Returns:
233
        typing.Any: The Python type object corresponding to the given type string.
234

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

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

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

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

254

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

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

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

270

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

276

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

286

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

300

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

304

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

308
    Args:
309
        obj:Any
310

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

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

318

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

329
    """
330

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

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

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

391
        return False
1✔
392

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

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

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

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

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

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

472
    return "Any"
1✔
473

474

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

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

482
    Args:
483
        object: The object to check.
484
        typing_type: The typing type to check against.
485

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

492
    if typing_type is typing.Type:
1✔
493
        return is_type(object)
×
494

495
    if is_new_type(typing_type):
1✔
496
        typing_type = typing_type.__supertype__
1✔
497

498
    if is_typed_dict(typing_type):
1✔
499
        if not isinstance(object, dict):
1✔
500
            return False
1✔
501
        for key, expected_type in typing_type.__annotations__.items():
1✔
502
            if key not in object or not isoftype(object[key], expected_type):
1✔
503
                return False
1✔
504
        return True
1✔
505

506
    if typing_type == typing.Any:
1✔
507
        return True
1✔
508

509
    if hasattr(typing_type, "__origin__"):
1✔
510
        origin = typing_type.__origin__
1✔
511
        type_args = typing.get_args(typing_type)
1✔
512

513
        if origin is Literal:
1✔
514
            return object in type_args
1✔
515

516
        if origin is typing.Union:
1✔
517
            return any(isoftype(object, sub_type) for sub_type in type_args)
1✔
518

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

534
    return isinstance(object, typing_type)
1✔
535

536

537
def strtype(typing_type) -> str:
1✔
538
    """Converts a typing type to its string representation.
539

540
    Args:
541
        typing_type (Any): The typing type to be converted. This can include standard types,
542
            custom types, or types from the `typing` module, such as `Literal`, `Union`,
543
            `List`, `Dict`, `Tuple`, `TypedDict`, and `NewType`.
544

545
    Returns:
546
        str: The string representation of the provided typing type.
547

548
    Raises:
549
        UnsupportedTypeError: If the provided `typing_type` is not a recognized type.
550

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

563
    if not is_type(typing_type):
1✔
564
        raise UnsupportedTypeError(typing_type)
×
565

566
    if is_new_type(typing_type) or is_typed_dict(typing_type):
1✔
567
        return typing_type.__name__
1✔
568

569
    if typing_type == typing.Any:
1✔
570
        return "Any"
1✔
571

572
    if hasattr(typing_type, "__origin__"):
1✔
573
        origin = typing_type.__origin__
1✔
574
        type_args = typing.get_args(typing_type)
1✔
575

576
        if type_args[-1] is type(None):
1✔
577
            return (
1✔
578
                "Optional["
579
                + ", ".join([strtype(sub_type) for sub_type in type_args[:-1]])
580
                + "]"
581
            )
582

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

604
    return typing_type.__name__
1✔
605

606

607
# copied from: https://github.com/bojiang/typing_utils/blob/main/typing_utils/__init__.py
608
# liscened under Apache License 2.0
609

610

611
if hasattr(typing, "ForwardRef"):  # python3.8
1✔
612
    ForwardRef = typing.ForwardRef
1✔
613
elif hasattr(typing, "_ForwardRef"):  # python3.6
×
614
    ForwardRef = typing._ForwardRef
×
615
else:
616
    raise NotImplementedError()
×
617

618

619
unknown = None
1✔
620

621

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

633

634
STATIC_SUBTYPE_MAPPING: typing.Dict[type, typing.Type] = {
1✔
635
    io.TextIOWrapper: typing.TextIO,
636
    io.TextIOBase: typing.TextIO,
637
    io.StringIO: typing.TextIO,
638
    io.BufferedReader: typing.BinaryIO,
639
    io.BufferedWriter: typing.BinaryIO,
640
    io.BytesIO: typing.BinaryIO,
641
}
642

643

644
def optional_all(elements) -> typing.Optional[bool]:
1✔
645
    if all(elements):
1✔
646
        return True
1✔
647
    if all(e is False for e in elements):
1✔
648
        return False
1✔
649
    return unknown
×
650

651

652
def optional_any(elements) -> typing.Optional[bool]:
1✔
653
    if any(elements):
1✔
654
        return True
1✔
655
    if any(e is None for e in elements):
1✔
656
        return unknown
×
657
    return False
1✔
658

659

660
def _hashable(value):
1✔
661
    """Determine whether `value` can be hashed."""
662
    try:
1✔
663
        hash(value)
1✔
664
    except TypeError:
×
665
        return False
×
666
    return True
1✔
667

668

669
get_type_hints = typing.get_type_hints
1✔
670

671
GenericClass = type(typing.List)
1✔
672
UnionClass = type(typing.Union)
1✔
673

674
_Type = typing.Union[None, type, "typing.TypeVar"]
1✔
675
OriginType = typing.Union[None, type]
1✔
676
TypeArgs = typing.Union[type, typing.AbstractSet[type], typing.Sequence[type]]
1✔
677

678

679
def _normalize_aliases(type_: _Type) -> _Type:
1✔
680
    if isinstance(type_, typing.TypeVar):
1✔
681
        return type_
1✔
682

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

685
    if type_ in BUILTINS_MAPPING:
1✔
686
        return BUILTINS_MAPPING[type_]
1✔
687
    return type_
1✔
688

689

690
def get_origin(type_):
1✔
691
    """Get the unsubscripted version of a type.
692

693
    This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.
694
    Return None for unsupported types.
695

696
    Examples:
697
        Here are some code examples using `get_origin` from the `typing_utils` module:
698

699
        .. code-block:: python
700

701
            from typing_utils import get_origin
702

703
            # Examples of get_origin usage
704
            get_origin(Literal[42]) is Literal  # True
705
            get_origin(int) is None  # True
706
            get_origin(ClassVar[int]) is ClassVar  # True
707
            get_origin(Generic) is Generic  # True
708
            get_origin(Generic[T]) is Generic  # True
709
            get_origin(Union[T, int]) is Union  # True
710
            get_origin(List[Tuple[T, T]][int]) == list  # True
711

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

738

739
def get_args(type_) -> typing.Tuple:
1✔
740
    """Get type arguments with all substitutions performed.
741

742
    For unions, basic simplifications used by Union constructor are performed.
743

744
    Examples:
745
        Here are some code examples using `get_args` from the `typing_utils` module:
746

747
        .. code-block:: python
748

749
            from typing_utils import get_args
750

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

779

780
def eval_forward_ref(ref, forward_refs=None):
1✔
781
    """Eval forward_refs in all cPython versions."""
782
    localns = forward_refs or {}
×
783

784
    if hasattr(typing, "_eval_type"):  # python3.8 & python 3.9
×
785
        _eval_type = typing._eval_type
×
786
        return _eval_type(ref, globals(), localns)
×
787

788
    if hasattr(ref, "_eval_type"):  # python3.6
×
789
        _eval_type = ref._eval_type
×
790
        return _eval_type(globals(), localns)
×
791

792
    raise NotImplementedError()
×
793

794

795
class NormalizedType(typing.NamedTuple):
1✔
796
    """Normalized type, made it possible to compare, hash between types."""
797

798
    origin: _Type
1✔
799
    args: typing.Union[tuple, frozenset] = ()
1✔
800

801
    def __eq__(self, other):
1✔
802
        if isinstance(other, NormalizedType):
1✔
803
            if self.origin != other.origin:
1✔
804
                return False
×
805
            if isinstance(self.args, frozenset) and isinstance(other.args, frozenset):
1✔
806
                return self.args <= other.args and other.args <= self.args
1✔
807
            return self.origin == other.origin and self.args == other.args
1✔
808
        if not self.args:
×
809
            return self.origin == other
×
810
        return False
×
811

812
    def __hash__(self) -> int:
1✔
813
        if not self.args:
1✔
814
            return hash(self.origin)
1✔
815
        return hash((self.origin, self.args))
1✔
816

817
    def __repr__(self):
1✔
818
        if not self.args:
×
819
            return f"{self.origin}"
×
820
        return f"{self.origin}[{self.args}])"
×
821

822

823
@lru_cache(maxsize=None)
1✔
824
def _normalize_args(tps: TypeArgs):
1✔
825
    if isinstance(tps, str):
1✔
826
        return tps
1✔
827
    if isinstance(tps, collections.abc.Sequence):
1✔
828
        return tuple(_normalize_args(type_) for type_ in tps)
1✔
829
    if isinstance(tps, collections.abc.Set):
1✔
830
        return frozenset(_normalize_args(type_) for type_ in tps)
1✔
831
    return normalize(tps)
1✔
832

833

834
def normalize(type_: _Type) -> NormalizedType:
1✔
835
    """Convert types to NormalizedType instances."""
836
    args = get_args(type_)
1✔
837
    origin = get_origin(type_)
1✔
838
    if not origin:
1✔
839
        return NormalizedType(_normalize_aliases(type_))
1✔
840
    origin = _normalize_aliases(origin)
1✔
841

842
    if origin is typing.Union:  # sort args when the origin is Union
1✔
843
        args = _normalize_args(frozenset(args))
1✔
844
    else:
845
        args = _normalize_args(args)
1✔
846
    return NormalizedType(origin, args)
1✔
847

848

849
def _is_origin_subtype(left: OriginType, right: OriginType) -> bool:
1✔
850
    if left is right:
1✔
851
        return True
1✔
852

853
    if (
1✔
854
        left is not None
855
        and left in STATIC_SUBTYPE_MAPPING
856
        and right == STATIC_SUBTYPE_MAPPING[left]
857
    ):
858
        return True
×
859

860
    if hasattr(left, "mro"):
1✔
861
        for parent in left.mro():
1✔
862
            if parent == right:
1✔
863
                return True
1✔
864

865
    if isinstance(left, type) and isinstance(right, type):
1✔
866
        return issubclass(left, right)
1✔
867

868
    return left == right
1✔
869

870

871
NormalizedTypeArgs = typing.Union[
1✔
872
    typing.Tuple["NormalizedTypeArgs", ...],
873
    typing.FrozenSet[NormalizedType],
874
    NormalizedType,
875
]
876

877

878
def _is_origin_subtype_args(
1✔
879
    left: NormalizedTypeArgs,
880
    right: NormalizedTypeArgs,
881
    forward_refs: typing.Optional[typing.Mapping[str, type]],
882
) -> typing.Optional[bool]:
883
    if isinstance(left, frozenset):
1✔
884
        if not isinstance(right, frozenset):
1✔
885
            return False
×
886

887
        excluded = left - right
1✔
888
        if not excluded:
1✔
889
            # Union[str, int] <> Union[int, str]
890
            return True
×
891

892
        # Union[list, int] <> Union[typing.Sequence, int]
893
        return all(
1✔
894
            any(_is_normal_subtype(e, r, forward_refs) for r in right) for e in excluded
895
        )
896

897
    if isinstance(left, collections.abc.Sequence) and not isinstance(
1✔
898
        left, NormalizedType
899
    ):
900
        if not isinstance(right, collections.abc.Sequence) or isinstance(
1✔
901
            right, NormalizedType
902
        ):
903
            return False
×
904

905
        if (
1✔
906
            left
907
            and left[-1].origin is not Ellipsis
908
            and right
909
            and right[-1].origin is Ellipsis
910
        ):
911
            # Tuple[type, type] <> Tuple[type, ...]
912
            return all(
×
913
                _is_origin_subtype_args(lft, right[0], forward_refs) for lft in left
914
            )
915

916
        if len(left) != len(right):
1✔
917
            return False
1✔
918

919
        return all(
1✔
920
            lft is not None
921
            and rgt is not None
922
            and _is_origin_subtype_args(lft, rgt, forward_refs)
923
            for lft, rgt in itertools.zip_longest(left, right)
924
        )
925

926
    assert isinstance(left, NormalizedType)
1✔
927
    assert isinstance(right, NormalizedType)
1✔
928

929
    return _is_normal_subtype(left, right, forward_refs)
1✔
930

931

932
@lru_cache(maxsize=None)
1✔
933
def _is_normal_subtype(
1✔
934
    left: NormalizedType,
935
    right: NormalizedType,
936
    forward_refs: typing.Optional[typing.Mapping[str, type]],
937
) -> typing.Optional[bool]:
938
    if isinstance(left.origin, ForwardRef):
1✔
939
        left = normalize(eval_forward_ref(left.origin, forward_refs=forward_refs))
×
940

941
    if isinstance(right.origin, ForwardRef):
1✔
942
        right = normalize(eval_forward_ref(right.origin, forward_refs=forward_refs))
×
943

944
    # Any
945
    if right.origin is typing.Any:
1✔
946
        return True
×
947

948
    # Union
949
    if right.origin is typing.Union and left.origin is typing.Union:
1✔
950
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
951
    if right.origin is typing.Union:
1✔
952
        return optional_any(
1✔
953
            _is_normal_subtype(left, a, forward_refs) for a in right.args
954
        )
955
    if left.origin is typing.Union:
1✔
956
        return optional_all(
1✔
957
            _is_normal_subtype(a, right, forward_refs) for a in left.args
958
        )
959

960
    # TypeVar
961
    if isinstance(left.origin, typing.TypeVar) and isinstance(
1✔
962
        right.origin, typing.TypeVar
963
    ):
964
        if left.origin is right.origin:
×
965
            return True
×
966

967
        left_bound = getattr(left.origin, "__bound__", None)
×
968
        right_bound = getattr(right.origin, "__bound__", None)
×
969
        if right_bound is None or left_bound is None:
×
970
            return unknown
×
971
        return _is_normal_subtype(
×
972
            normalize(left_bound), normalize(right_bound), forward_refs
973
        )
974
    if isinstance(right.origin, typing.TypeVar):
1✔
975
        return unknown
×
976
    if isinstance(left.origin, typing.TypeVar):
1✔
977
        left_bound = getattr(left.origin, "__bound__", None)
×
978
        if left_bound is None:
×
979
            return unknown
×
980
        return _is_normal_subtype(normalize(left_bound), right, forward_refs)
×
981

982
    if not left.args and not right.args:
1✔
983
        return _is_origin_subtype(left.origin, right.origin)
1✔
984

985
    if not right.args:
1✔
986
        return _is_origin_subtype(left.origin, right.origin)
1✔
987

988
    if _is_origin_subtype(left.origin, right.origin):
1✔
989
        return _is_origin_subtype_args(left.args, right.args, forward_refs)
1✔
990

991
    return False
1✔
992

993

994
def issubtype(
1✔
995
    left: _Type,
996
    right: _Type,
997
    forward_refs: typing.Optional[dict] = None,
998
) -> typing.Optional[bool]:
999
    """Check that the left argument is a subtype of the right.
1000

1001
    For unions, check if the type arguments of the left is a subset of the right.
1002
    Also works for nested types including ForwardRefs.
1003

1004
    Examples:
1005
        Here are some code examples using `issubtype` from the `typing_utils` module:
1006

1007
        .. code-block:: python
1008

1009
            from typing_utils import issubtype
1010

1011
            # Examples of issubtype checks
1012
            issubtype(typing.List, typing.Any)  # True
1013
            issubtype(list, list)  # True
1014
            issubtype(list, typing.List)  # True
1015
            issubtype(list, typing.Sequence)  # True
1016
            issubtype(typing.List[int], list)  # True
1017
            issubtype(typing.List[typing.List], list)  # True
1018
            issubtype(list, typing.List[int])  # False
1019
            issubtype(list, typing.Union[typing.Tuple, typing.Set])  # False
1020
            issubtype(typing.List[typing.List], typing.List[typing.Sequence])  # True
1021

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

1033

1034
def to_float_or_default(v, failure_default=0):
1✔
1035
    try:
1✔
1036
        return float(v)
1✔
1037
    except Exception as e:
1✔
1038
        if failure_default is None:
1✔
1039
            raise e
1✔
1040
        return failure_default
1✔
1041

1042

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

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

1069
        if not isoftype(value, data_type):
1✔
1070
            raise ValueError(
1✔
1071
                f"Passed value '{value}' of field '{field_name}' is not "
1072
                f"of required type: ({to_type_string(data_type)}) in {class_name} ('{id}').\n"
1073
                f"{class_name} description: {description}"
1074
            )
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