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

IBM / unitxt / 14911405902

08 May 2025 04:31PM UTC coverage: 80.074% (-0.07%) from 80.14%
14911405902

Pull #1773

github

web-flow
Merge e96fbbe15 into 2d15f20af
Pull Request #1773: Simplify tool calling base types

1645 of 2037 branches covered (80.76%)

Branch coverage included in aggregate %.

10250 of 12818 relevant lines covered (79.97%)

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 is_new_type(new_type) or is_typed_dict(
1✔
29
        new_type
30
    ) or hasattr(new_type, "__verify_type__"), "Can register only typing.NewType or typing.TypedDict or object with __verify_type__ class function"
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 hasattr(typing_type, "__verify_type__"):
1✔
493
        return typing_type.__verify_type__(object)
1✔
494

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

498
    if is_new_type(typing_type):
1✔
499
        typing_type = typing_type.__supertype__
1✔
500

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

509
    if typing_type == typing.Any:
1✔
510
        return True
1✔
511

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

516
        if origin is Literal:
1✔
517
            return object in type_args
1✔
518

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

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

537
    return isinstance(object, typing_type)
1✔
538

539

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

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

548
    Returns:
549
        str: The string representation of the provided typing type.
550

551
    Raises:
552
        UnsupportedTypeError: If the provided `typing_type` is not a recognized type.
553

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

566
    if not is_type(typing_type):
1✔
567
        raise UnsupportedTypeError(typing_type)
×
568

569
    if is_new_type(typing_type) or is_typed_dict(typing_type):
1✔
570
        return typing_type.__name__
1✔
571

572
    if typing_type == typing.Any:
1✔
573
        return "Any"
1✔
574

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

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

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

607
    return typing_type.__name__
1✔
608

609

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

613

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

621

622
unknown = None
1✔
623

624

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

636

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

646

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

654

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

662

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

671

672
get_type_hints = typing.get_type_hints
1✔
673

674
GenericClass = type(typing.List)
1✔
675
UnionClass = type(typing.Union)
1✔
676

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

681

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

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

688
    if type_ in BUILTINS_MAPPING:
1✔
689
        return BUILTINS_MAPPING[type_]
1✔
690
    return type_
1✔
691

692

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

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

699
    Examples:
700
        Here are some code examples using `get_origin` from the `typing_utils` module:
701

702
        .. code-block:: python
703

704
            from typing_utils import get_origin
705

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

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

741

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

745
    For unions, basic simplifications used by Union constructor are performed.
746

747
    Examples:
748
        Here are some code examples using `get_args` from the `typing_utils` module:
749

750
        .. code-block:: python
751

752
            from typing_utils import get_args
753

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

782

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

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

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

795
    raise NotImplementedError()
×
796

797

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

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

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

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

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

825

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

836

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

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

851

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

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

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

868
    if isinstance(left, type) and isinstance(right, type):
1✔
869
        return issubclass(left, right)
1✔
870

871
    return left == right
1✔
872

873

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

880

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

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

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

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

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

919
        if len(left) != len(right):
1✔
920
            return False
1✔
921

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

929
    assert isinstance(left, NormalizedType)
1✔
930
    assert isinstance(right, NormalizedType)
1✔
931

932
    return _is_normal_subtype(left, right, forward_refs)
1✔
933

934

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

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

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

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

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

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

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

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

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

994
    return False
1✔
995

996

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

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

1007
    Examples:
1008
        Here are some code examples using `issubtype` from the `typing_utils` module:
1009

1010
        .. code-block:: python
1011

1012
            from typing_utils import issubtype
1013

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

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

1036

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

1045

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

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

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

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