Skip to content

Commit 5435ba7

Browse files
committed
Merge branch 'main' into 10629-fix-enum
2 parents 48de7c6 + 2419981 commit 5435ba7

File tree

5 files changed

+309
-221
lines changed

5 files changed

+309
-221
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 24 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -492,104 +492,29 @@ class Url(SupportsAllComparisons):
492492
by Mozilla.
493493
"""
494494

495-
def __init__(self, url: str) -> None:
496-
"""Initializes the `Url`.
497-
498-
Args:
499-
url: String representation of a URL.
500-
501-
Returns:
502-
A new `Url` instance.
503-
504-
Raises:
505-
ValidationError: If the URL is invalid.
506-
"""
507-
495+
def __init__(self, url: str) -> None: ...
508496
def __new__(cls, url: str) -> Self: ...
509497
@property
510-
def scheme(self) -> str:
511-
"""
512-
The scheme part of the URL.
513-
514-
e.g. `https` in `https://user:pass@host:port/path?query#fragment`
515-
"""
498+
def scheme(self) -> str: ...
516499
@property
517-
def username(self) -> str | None:
518-
"""
519-
The username part of the URL, or `None`.
520-
521-
e.g. `user` in `https://user:pass@host:port/path?query#fragment`
522-
"""
500+
def username(self) -> str | None: ...
523501
@property
524-
def password(self) -> str | None:
525-
"""
526-
The password part of the URL, or `None`.
527-
528-
e.g. `pass` in `https://user:pass@host:port/path?query#fragment`
529-
"""
502+
def password(self) -> str | None: ...
530503
@property
531-
def host(self) -> str | None:
532-
"""
533-
The host part of the URL, or `None`.
534-
535-
If the URL must be punycode encoded, this is the encoded host, e.g if the input URL is `https://£££.com`,
536-
`host` will be `xn--9aaa.com`
537-
"""
538-
def unicode_host(self) -> str | None:
539-
"""
540-
The host part of the URL as a unicode string, or `None`.
541-
542-
e.g. `host` in `https://user:pass@host:port/path?query#fragment`
543-
544-
If the URL must be punycode encoded, this is the decoded host, e.g if the input URL is `https://£££.com`,
545-
`unicode_host()` will be `£££.com`
546-
"""
504+
def host(self) -> str | None: ...
505+
def unicode_host(self) -> str | None: ...
547506
@property
548-
def port(self) -> int | None:
549-
"""
550-
The port part of the URL, or `None`.
551-
552-
e.g. `port` in `https://user:pass@host:port/path?query#fragment`
553-
"""
507+
def port(self) -> int | None: ...
554508
@property
555-
def path(self) -> str | None:
556-
"""
557-
The path part of the URL, or `None`.
558-
559-
e.g. `/path` in `https://user:pass@host:port/path?query#fragment`
560-
"""
509+
def path(self) -> str | None: ...
561510
@property
562-
def query(self) -> str | None:
563-
"""
564-
The query part of the URL, or `None`.
565-
566-
e.g. `query` in `https://user:pass@host:port/path?query#fragment`
567-
"""
568-
def query_params(self) -> list[tuple[str, str]]:
569-
"""
570-
The query part of the URL as a list of key-value pairs.
571-
572-
e.g. `[('foo', 'bar')]` in `https://user:pass@host:port/path?foo=bar#fragment`
573-
"""
511+
def query(self) -> str | None: ...
512+
def query_params(self) -> list[tuple[str, str]]: ...
574513
@property
575-
def fragment(self) -> str | None:
576-
"""
577-
The fragment part of the URL, or `None`.
578-
579-
e.g. `fragment` in `https://user:pass@host:port/path?query#fragment`
580-
"""
581-
def unicode_string(self) -> str:
582-
"""
583-
The URL as a unicode string, unlike `__str__()` this will not punycode encode the host.
584-
585-
If the URL must be punycode encoded, this is the decoded string, e.g if the input URL is `https://£££.com`,
586-
`unicode_string()` will be `https://£££.com`
587-
"""
514+
def fragment(self) -> str | None: ...
515+
def unicode_string(self) -> str: ...
588516
def __repr__(self) -> str: ...
589-
def __str__(self) -> str:
590-
"""
591-
The URL as a string, this will punycode encode the host if required.
592-
"""
517+
def __str__(self) -> str: ...
593518
def __deepcopy__(self, memo: dict) -> str: ...
594519
@classmethod
595520
def build(
@@ -603,23 +528,7 @@ class Url(SupportsAllComparisons):
603528
path: str | None = None,
604529
query: str | None = None,
605530
fragment: str | None = None,
606-
) -> Self:
607-
"""
608-
Build a new `Url` instance from its component parts.
609-
610-
Args:
611-
scheme: The scheme part of the URL.
612-
username: The username part of the URL, or omit for no username.
613-
password: The password part of the URL, or omit for no password.
614-
host: The host part of the URL.
615-
port: The port part of the URL, or omit for no port.
616-
path: The path part of the URL, or omit for no path.
617-
query: The query part of the URL, or omit for no query.
618-
fragment: The fragment part of the URL, or omit for no fragment.
619-
620-
Returns:
621-
An instance of URL
622-
"""
531+
) -> Self: ...
623532

624533
class MultiHostUrl(SupportsAllComparisons):
625534
"""
@@ -629,82 +538,21 @@ class MultiHostUrl(SupportsAllComparisons):
629538
by Mozilla.
630539
"""
631540

632-
def __init__(self, url: str) -> None:
633-
"""Initializes the `MultiHostUrl`.
634-
635-
Args:
636-
url: String representation of a URL.
637-
638-
Returns:
639-
A new `MultiHostUrl` instance.
640-
641-
Raises:
642-
ValidationError: If the URL is invalid.
643-
"""
644-
541+
def __init__(self, url: str) -> None: ...
645542
def __new__(cls, url: str) -> Self: ...
646543
@property
647-
def scheme(self) -> str:
648-
"""
649-
The scheme part of the URL.
650-
651-
e.g. `https` in `https://foo.com,bar.com/path?query#fragment`
652-
"""
544+
def scheme(self) -> str: ...
653545
@property
654-
def path(self) -> str | None:
655-
"""
656-
The path part of the URL, or `None`.
657-
658-
e.g. `/path` in `https://foo.com,bar.com/path?query#fragment`
659-
"""
546+
def path(self) -> str | None: ...
660547
@property
661-
def query(self) -> str | None:
662-
"""
663-
The query part of the URL, or `None`.
664-
665-
e.g. `query` in `https://foo.com,bar.com/path?query#fragment`
666-
"""
667-
def query_params(self) -> list[tuple[str, str]]:
668-
"""
669-
The query part of the URL as a list of key-value pairs.
670-
671-
e.g. `[('foo', 'bar')]` in `https://foo.com,bar.com/path?query#fragment`
672-
"""
548+
def query(self) -> str | None: ...
549+
def query_params(self) -> list[tuple[str, str]]: ...
673550
@property
674-
def fragment(self) -> str | None:
675-
"""
676-
The fragment part of the URL, or `None`.
677-
678-
e.g. `fragment` in `https://foo.com,bar.com/path?query#fragment`
679-
"""
680-
def hosts(self) -> list[MultiHostHost]:
681-
'''
682-
683-
The hosts of the `MultiHostUrl` as [`MultiHostHost`][pydantic_core.MultiHostHost] typed dicts.
684-
685-
```py
686-
from pydantic_core import MultiHostUrl
687-
688-
mhu = MultiHostUrl('https://foo.com:123,foo:[email protected]/path')
689-
print(mhu.hosts())
690-
"""
691-
[
692-
{'username': None, 'password': None, 'host': 'foo.com', 'port': 123},
693-
{'username': 'foo', 'password': 'bar', 'host': 'bar.com', 'port': 443}
694-
]
695-
```
696-
Returns:
697-
A list of dicts, each representing a host.
698-
'''
699-
def unicode_string(self) -> str:
700-
"""
701-
The URL as a unicode string, unlike `__str__()` this will not punycode encode the hosts.
702-
"""
551+
def fragment(self) -> str | None: ...
552+
def hosts(self) -> list[MultiHostHost]: ...
553+
def unicode_string(self) -> str: ...
703554
def __repr__(self) -> str: ...
704-
def __str__(self) -> str:
705-
"""
706-
The URL as a string, this will punycode encode the hosts if required.
707-
"""
555+
def __str__(self) -> str: ...
708556
def __deepcopy__(self, memo: dict) -> Self: ...
709557
@classmethod
710558
def build(
@@ -719,27 +567,7 @@ class MultiHostUrl(SupportsAllComparisons):
719567
path: str | None = None,
720568
query: str | None = None,
721569
fragment: str | None = None,
722-
) -> Self:
723-
"""
724-
Build a new `MultiHostUrl` instance from its component parts.
725-
726-
This method takes either `hosts` - a list of `MultiHostHost` typed dicts, or the individual components
727-
`username`, `password`, `host` and `port`.
728-
729-
Args:
730-
scheme: The scheme part of the URL.
731-
hosts: Multiple hosts to build the URL from.
732-
username: The username part of the URL.
733-
password: The password part of the URL.
734-
host: The host part of the URL.
735-
port: The port part of the URL.
736-
path: The path part of the URL.
737-
query: The query part of the URL, or omit for no query.
738-
fragment: The fragment part of the URL, or omit for no fragment.
739-
740-
Returns:
741-
An instance of `MultiHostUrl`
742-
"""
570+
) -> Self: ...
743571

744572
@final
745573
class SchemaError(Exception):

src/serializers/type_serializers/union.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::build_tools::py_schema_err;
99
use crate::common::union::{Discriminator, SMALL_UNION_THRESHOLD};
1010
use crate::definitions::DefinitionsBuilder;
1111
use crate::tools::{truncate_safe_repr, SchemaDict};
12+
use crate::PydanticSerializationUnexpectedValue;
1213

1314
use super::{
1415
infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerCheck,
@@ -89,7 +90,8 @@ fn to_python(
8990
}
9091
}
9192

92-
if retry_with_lax_check {
93+
// If extra.check is SerCheck::Strict, we're in a nested union
94+
if extra.check != SerCheck::Strict && retry_with_lax_check {
9395
new_extra.check = SerCheck::Lax;
9496
for comb_serializer in choices {
9597
if let Ok(v) = comb_serializer.to_python(value, include, exclude, &new_extra) {
@@ -98,8 +100,17 @@ fn to_python(
98100
}
99101
}
100102

101-
for err in &errors {
102-
extra.warnings.custom_warning(err.to_string());
103+
// If extra.check is SerCheck::None, we're in a top-level union. We should thus raise the warnings
104+
if extra.check == SerCheck::None {
105+
for err in &errors {
106+
extra.warnings.custom_warning(err.to_string());
107+
}
108+
}
109+
// Otherwise, if we've encountered errors, return them to the parent union, which should take
110+
// care of the formatting for us
111+
else if !errors.is_empty() {
112+
let message = errors.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n");
113+
return Err(PydanticSerializationUnexpectedValue::new_err(Some(message)));
103114
}
104115

105116
infer_to_python(value, include, exclude, extra)
@@ -122,7 +133,8 @@ fn json_key<'a>(
122133
}
123134
}
124135

125-
if retry_with_lax_check {
136+
// If extra.check is SerCheck::Strict, we're in a nested union
137+
if extra.check != SerCheck::Strict && retry_with_lax_check {
126138
new_extra.check = SerCheck::Lax;
127139
for comb_serializer in choices {
128140
if let Ok(v) = comb_serializer.json_key(key, &new_extra) {
@@ -131,10 +143,18 @@ fn json_key<'a>(
131143
}
132144
}
133145

134-
for err in &errors {
135-
extra.warnings.custom_warning(err.to_string());
146+
// If extra.check is SerCheck::None, we're in a top-level union. We should thus raise the warnings
147+
if extra.check == SerCheck::None {
148+
for err in &errors {
149+
extra.warnings.custom_warning(err.to_string());
150+
}
151+
}
152+
// Otherwise, if we've encountered errors, return them to the parent union, which should take
153+
// care of the formatting for us
154+
else if !errors.is_empty() {
155+
let message = errors.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n");
156+
return Err(PydanticSerializationUnexpectedValue::new_err(Some(message)));
136157
}
137-
138158
infer_json_key(key, extra)
139159
}
140160

@@ -160,7 +180,8 @@ fn serde_serialize<S: serde::ser::Serializer>(
160180
}
161181
}
162182

163-
if retry_with_lax_check {
183+
// If extra.check is SerCheck::Strict, we're in a nested union
184+
if extra.check != SerCheck::Strict && retry_with_lax_check {
164185
new_extra.check = SerCheck::Lax;
165186
for comb_serializer in choices {
166187
if let Ok(v) = comb_serializer.to_python(value, include, exclude, &new_extra) {
@@ -169,8 +190,14 @@ fn serde_serialize<S: serde::ser::Serializer>(
169190
}
170191
}
171192

172-
for err in &errors {
173-
extra.warnings.custom_warning(err.to_string());
193+
// If extra.check is SerCheck::None, we're in a top-level union. We should thus raise the warnings
194+
if extra.check == SerCheck::None {
195+
for err in &errors {
196+
extra.warnings.custom_warning(err.to_string());
197+
}
198+
} else {
199+
// NOTE: if this function becomes recursive at some point, an `Err(_)` containing the errors
200+
// will have to be returned here
174201
}
175202

176203
infer_serialize(value, serializer, include, exclude, extra)

src/validators/model_fields.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,16 @@ impl Validator for ModelFieldsValidator {
421421
let new_extra = match &self.extra_behavior {
422422
ExtraBehavior::Allow => {
423423
let non_extra_data = PyDict::new_bound(py);
424-
self.fields.iter().for_each(|f| {
425-
let popped_value = PyAnyMethods::get_item(&**new_data, &f.name).unwrap();
426-
new_data.del_item(&f.name).unwrap();
427-
non_extra_data.set_item(&f.name, popped_value).unwrap();
428-
});
424+
self.fields.iter().try_for_each(|f| -> PyResult<()> {
425+
let Some(popped_value) = new_data.get_item(&f.name)? else {
426+
// field not present in __dict__ for some reason; let the rest of the
427+
// validation pipeline handle it later
428+
return Ok(());
429+
};
430+
new_data.del_item(&f.name)?;
431+
non_extra_data.set_item(&f.name, popped_value)?;
432+
Ok(())
433+
})?;
429434
let new_extra = new_data.copy()?;
430435
new_data.clear();
431436
new_data.update(non_extra_data.as_mapping())?;

0 commit comments

Comments
 (0)