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

oir / startle / 19120850309

06 Nov 2025 12:36AM UTC coverage: 98.785%. Remained the same
19120850309

push

github

web-flow
Handle stringified annotations (#120)

237 of 237 branches covered (100.0%)

Branch coverage included in aggregate %.

1145 of 1162 relevant lines covered (98.54%)

0.99 hits per line

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

95.9
startle/_type_utils.py
1
import inspect
1✔
2
import sys
1✔
3
import types
1✔
4
from typing import (
1✔
5
    TYPE_CHECKING,
6
    Annotated,
7
    Any,
8
    Optional,
9
    TypeAlias,
10
    Union,
11
    get_args,
12
    get_origin,
13
)
14

15
if TYPE_CHECKING:
16
    from typing_extensions import TypeForm
17

18
TypeHint: TypeAlias = "TypeForm[Any]"
1✔
19

20

21
def strip_optional(type_: TypeHint) -> TypeHint:
1✔
22
    """
23
    Strip the Optional type from a type hint. Given T1 | ... | Tn | None,
24
    return T1 | ... | Tn.
25
    """
26
    if get_origin(type_) is Union:
1✔
27
        args = get_args(type_)
1✔
28
        if type(None) in args:
1✔
29
            args = tuple([arg for arg in args if arg is not type(None)])
1✔
30
            if len(args) == 1:
1✔
31
                return args[0]
1✔
32
            else:
33
                return Union[args]  # type: ignore
1✔
34

35
    return type_
1✔
36

37

38
def _strip_unary_outer(type_: TypeHint, outer: Any) -> tuple[bool, TypeHint]:
1✔
39
    """
40
    Strip a unary outer type from a type hint. If given outer[T], return (True, T).
41
    Otherwise, return (False, type_).
42
    """
43
    if outer is None:
1✔
44
        return False, type_
×
45
    if get_origin(type_) is outer:
1✔
46
        args = get_args(type_)
1✔
47
        if args:
1✔
48
            return True, args[0]
1✔
49
    return False, type_
1✔
50

51

52
def _required_t() -> Any:
1✔
53
    if sys.version_info >= (3, 11):
1✔
54
        from typing import Required as TypingRequired
1✔
55

56
        return TypingRequired
1✔
57
    try:
1✔
58
        from typing_extensions import Required as TE_Required
1✔
59

60
        return TE_Required
1✔
61
    except ImportError:
×
62
        return None
×
63

64

65
def _not_required_t() -> Any:
1✔
66
    if sys.version_info >= (3, 11):
1✔
67
        from typing import NotRequired as TypingNotRequired
1✔
68

69
        return TypingNotRequired
1✔
70
    try:
1✔
71
        from typing_extensions import NotRequired as TE_NotRequired
1✔
72

73
        return TE_NotRequired
1✔
74
    except ImportError:
×
75
        return None
×
76

77

78
def strip_not_required(type_: TypeHint) -> tuple[bool, TypeHint]:
1✔
79
    """
80
    Strip NotRequired from a type hint. If given a NotRequired[T], return (True, T).
81
    Otherwise, return (False, type_).
82
    """
83
    match, type_ = _strip_unary_outer(type_, outer=_not_required_t())
1✔
84
    if match:
1✔
85
        return True, type_
1✔
86
    return False, type_
1✔
87

88

89
def strip_required(type_: TypeHint) -> tuple[bool, TypeHint]:
1✔
90
    """
91
    Strip Required from a type hint. If given a Required[T], return (True, T).
92
    Otherwise, return (False, type_).
93
    """
94
    match, type_ = _strip_unary_outer(type_, outer=_required_t())
1✔
95
    if match:
1✔
96
        return True, type_
1✔
97
    return False, type_
1✔
98

99

100
def strip_annotated(type_: TypeHint) -> TypeHint:
1✔
101
    """
102
    Strip the Annotated type from a type hint. Given Annotated[T, ...], return T.
103
    """
104
    _, type_ = _strip_unary_outer(type_, outer=Annotated)
1✔
105
    return type_
1✔
106

107

108
def resolve_type_alias(type_: TypeHint) -> TypeHint:
1✔
109
    """
110
    Resolve type aliases to their underlying types.
111
    """
112
    if sys.version_info >= (3, 12):
1✔
113
        from typing import TypeAliasType
1✔
114

115
        if isinstance(type_, TypeAliasType):
1✔
116
            return type_.__value__
1✔
117
    return type_
1✔
118

119

120
def normalize_union_type(annotation: TypeHint) -> TypeHint:
1✔
121
    """
122
    Normalize a type annotation by unifying Union and Optional types.
123
    """
124
    origin = get_origin(annotation)
1✔
125
    args = get_args(annotation)
1✔
126
    if origin is Union or origin is types.UnionType:
1✔
127
        if type(None) in args:
1✔
128
            args = tuple([arg for arg in args if arg is not type(None)])
1✔
129
            if len(args) == 1:
1✔
130
                return Optional[args[0]]  # type: ignore
1✔
131
            else:
132
                return Union[args + tuple([type(None)])]  # type: ignore
1✔
133
        else:
134
            return Union[tuple(args)]  # type: ignore
1✔
135
    return annotation
1✔
136

137

138
def normalize_annotation(annotation: TypeHint) -> TypeHint:
1✔
139
    """
140
    Normalize a type annotation by stripping Annotated, resolving type aliases,
141
    and unifying Union and Optional types.
142
    """
143
    prev: Any = None
1✔
144
    curr: Any = annotation
1✔
145
    while prev != curr:
1✔
146
        prev = curr
1✔
147
        curr = strip_annotated(curr)
1✔
148
        curr = resolve_type_alias(curr)
1✔
149
        curr = normalize_union_type(curr)
1✔
150
    return curr
1✔
151

152

153
def shorten_type_annotation(annotation: TypeHint) -> str:
1✔
154
    origin = get_origin(annotation)
1✔
155
    if origin is None:
1✔
156
        # It's a simple type, return its name
157
        if inspect.isclass(annotation):
1✔
158
            return annotation.__name__
1✔
159
        return repr(annotation)
1✔
160

161
    if origin is Union or origin is types.UnionType:
1✔
162
        args = get_args(annotation)
1✔
163
        if type(None) in args:
1✔
164
            args = tuple([arg for arg in args if arg is not type(None)])
1✔
165
            if len(args) == 1:
1✔
166
                return f"{shorten_type_annotation(args[0])} | None"
1✔
167
            return " | ".join(shorten_type_annotation(arg) for arg in args) + " | None"
1✔
168
        else:
169
            return " | ".join(shorten_type_annotation(arg) for arg in args)
1✔
170

171
    # It's a generic type, process its arguments
172
    args = get_args(annotation)
1✔
173
    if args:
1✔
174
        args_str = ", ".join(shorten_type_annotation(arg) for arg in args)
1✔
175
        return f"{origin.__name__}[{args_str}]"
1✔
176

177
    return repr(annotation)
1✔
178

179

180
def is_typeddict(type_: type) -> bool:
1✔
181
    """
182
    Return True if the given type is a TypedDict class.
183
    """
184

185
    # we only use __annotations__, so merely checking for that
186
    # and dict subclassing. TODO: maybe narrow this down further?
187
    return (
1✔
188
        isinstance(type_, type)
189
        and issubclass(type_, dict)
190
        and hasattr(type_, "__annotations__")  # type: ignore
191
    )
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