File: test_types_namedtuple.py

package info (click to toggle)
pydantic 2.12.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,628 kB
  • sloc: python: 75,989; javascript: 181; makefile: 115; sh: 38
file content (267 lines) | stat: -rw-r--r-- 7,572 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
from collections import namedtuple
from typing import Generic, NamedTuple, Optional, TypeVar

import pytest
from typing_extensions import NamedTuple as TypingExtensionsNamedTuple

from pydantic import BaseModel, ConfigDict, PositiveInt, TypeAdapter, ValidationError
from pydantic.errors import PydanticSchemaGenerationError


def test_namedtuple_simple():
    Position = namedtuple('Pos', 'x y')

    class Model(BaseModel):
        pos: Position

    model = Model(pos=('1', 2))
    assert isinstance(model.pos, Position)
    assert model.pos.x == '1'
    assert model.pos == Position('1', 2)

    model = Model(pos={'x': '1', 'y': 2})
    assert model.pos == Position('1', 2)


def test_namedtuple():
    class Event(NamedTuple):
        a: int
        b: int
        c: int
        d: str

    class Model(BaseModel):
        # pos: Position
        event: Event

    model = Model(event=(b'1', '2', 3, 'qwe'))
    assert isinstance(model.event, Event)
    assert model.event == Event(1, 2, 3, 'qwe')
    assert repr(model) == "Model(event=Event(a=1, b=2, c=3, d='qwe'))"

    with pytest.raises(ValidationError) as exc_info:
        Model(pos=('1', 2), event=['qwe', '2', 3, 'qwe'])
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_parsing',
            'loc': ('event', 0),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'qwe',
        }
    ]


def test_namedtuple_schema():
    class Position1(NamedTuple):
        x: int
        y: int

    Position2 = namedtuple('Position2', 'x y')

    class Model(BaseModel):
        pos1: Position1
        pos2: Position2
        pos3: tuple[int, int]

    assert Model.model_json_schema() == {
        'title': 'Model',
        'type': 'object',
        '$defs': {
            'Position1': {
                'maxItems': 2,
                'minItems': 2,
                'prefixItems': [{'title': 'X', 'type': 'integer'}, {'title': 'Y', 'type': 'integer'}],
                'type': 'array',
            },
            'Position2': {
                'maxItems': 2,
                'minItems': 2,
                'prefixItems': [{'title': 'X'}, {'title': 'Y'}],
                'type': 'array',
            },
        },
        'properties': {
            'pos1': {'$ref': '#/$defs/Position1'},
            'pos2': {'$ref': '#/$defs/Position2'},
            'pos3': {
                'maxItems': 2,
                'minItems': 2,
                'prefixItems': [{'type': 'integer'}, {'type': 'integer'}],
                'title': 'Pos3',
                'type': 'array',
            },
        },
        'required': ['pos1', 'pos2', 'pos3'],
    }


def test_namedtuple_right_length():
    class Point(NamedTuple):
        x: int
        y: int

    class Model(BaseModel):
        p: Point

    assert isinstance(Model(p=(1, 2)), Model)

    with pytest.raises(ValidationError) as exc_info:
        Model(p=(1, 2, 3))
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'unexpected_positional_argument',
            'loc': ('p', 2),
            'msg': 'Unexpected positional argument',
            'input': 3,
        }
    ]


def test_namedtuple_postponed_annotation():
    """
    https://github.com/pydantic/pydantic/issues/2760
    """

    class Tup(NamedTuple):
        v: 'PositiveInt'

    class Model(BaseModel):
        t: Tup

    # The effect of issue #2760 is that this call raises a `PydanticUserError` even though the type declared on `Tup.v`
    # references a binding in this module's global scope.
    with pytest.raises(ValidationError):
        Model.model_validate({'t': [-1]})


def test_namedtuple_different_module(create_module) -> None:
    """https://github.com/pydantic/pydantic/issues/10336"""

    @create_module
    def other_module():
        from typing import NamedTuple

        TestIntOtherModule = int

        class Tup(NamedTuple):
            f: 'TestIntOtherModule'

    class Model(BaseModel):
        tup: other_module.Tup

    assert Model(tup={'f': 1}).tup.f == 1


def test_namedtuple_arbitrary_type():
    class CustomClass:
        pass

    class Tup(NamedTuple):
        c: CustomClass

    class Model(BaseModel):
        x: Tup

        model_config = ConfigDict(arbitrary_types_allowed=True)

    data = {'x': Tup(c=CustomClass())}
    model = Model.model_validate(data)
    assert isinstance(model.x.c, CustomClass)

    with pytest.raises(PydanticSchemaGenerationError):

        class ModelNoArbitraryTypes(BaseModel):
            x: Tup


def test_recursive_namedtuple():
    class MyNamedTuple(NamedTuple):
        x: int
        y: Optional['MyNamedTuple']

    ta = TypeAdapter(MyNamedTuple)
    assert ta.validate_python({'x': 1, 'y': {'x': 2, 'y': None}}) == (1, (2, None))

    with pytest.raises(ValidationError) as exc_info:
        ta.validate_python({'x': 1, 'y': {'x': 2, 'y': {'x': 'a', 'y': None}}})
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'a',
            'loc': ('y', 'y', 'x'),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'type': 'int_parsing',
        }
    ]


def test_recursive_generic_namedtuple():
    # Need to use TypingExtensionsNamedTuple to make it work with Python <3.11
    T = TypeVar('T')

    class MyNamedTuple(TypingExtensionsNamedTuple, Generic[T]):
        x: T
        y: Optional['MyNamedTuple[T]']

    ta = TypeAdapter(MyNamedTuple[int])
    assert ta.validate_python({'x': 1, 'y': {'x': 2, 'y': None}}) == (1, (2, None))

    with pytest.raises(ValidationError) as exc_info:
        ta.validate_python({'x': 1, 'y': {'x': 2, 'y': {'x': 'a', 'y': None}}})
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'a',
            'loc': ('y', 'y', 'x'),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'type': 'int_parsing',
        }
    ]


def test_namedtuple_defaults():
    class NT(NamedTuple):
        x: int
        y: int = 33

    assert TypeAdapter(NT).validate_python([1]) == (1, 33)
    assert TypeAdapter(NT).validate_python({'x': 22}) == (22, 33)


def test_eval_type_backport():
    class MyNamedTuple(NamedTuple):
        foo: 'list[int | str]'

    class Model(BaseModel):
        t: MyNamedTuple

    assert Model(t=([1, '2'],)).model_dump() == {'t': ([1, '2'],)}

    with pytest.raises(ValidationError) as exc_info:
        Model(t=('not a list',))
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'list_type',
            'loc': ('t', 0),
            'msg': 'Input should be a valid list',
            'input': 'not a list',
        }
    ]
    with pytest.raises(ValidationError) as exc_info:
        Model(t=([{'not a str or int'}],))
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_type',
            'loc': ('t', 0, 0, 'int'),
            'msg': 'Input should be a valid integer',
            'input': {'not a str or int'},
        },
        {
            'type': 'string_type',
            'loc': ('t', 0, 0, 'str'),
            'msg': 'Input should be a valid string',
            'input': {'not a str or int'},
        },
    ]