Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration: Pydantic v2 #2001

Merged
merged 23 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8f098f
pydanticv2: Model.dict() -> Model.dump_model()
Axolotle Nov 20, 2024
f0303aa
pydanticv2: Model.__fields__ -> Model.model_fields
Axolotle Nov 20, 2024
0c74ed6
pydanticv2: Model.construct() -> Model.model_construct()
Axolotle Nov 20, 2024
68e9eb6
pydanticv2: Class Config -> ConfigDict()
Axolotle Nov 20, 2024
aedbcab
pydanticv2: Class Config.schema_extra() -> __get_pydantic_json_schema…
Axolotle Nov 20, 2024
29166b7
pydanticv2: validator() -> field_validator()
Axolotle Nov 20, 2024
ad1073a
pydanticv2: root_validator() -> model_validator()
Axolotle Nov 20, 2024
8c38c8d
pydanticv2: Field's allow_mutation -> frozen
Axolotle Nov 20, 2024
14982e6
pydanticv2: regex -> pattern
Axolotle Nov 20, 2024
d29545d
pydanticv2: fix missing imports
Axolotle Nov 20, 2024
42a9947
pydanticv2: rework custom validators and extra args
Axolotle Nov 20, 2024
8626711
pydanticv2: fix SelectOption and other choices options
Axolotle Nov 20, 2024
3e88242
pydanticv2: fix TimeOption, was no longer parsing numbers
Axolotle Nov 20, 2024
22847c6
pydanticv2: fix DateOption, was no longer parsing numbers
Axolotle Nov 20, 2024
b97230f
pydanticv2: fix PasswordOption, properly handle forbidden fields
Axolotle Nov 20, 2024
7c81896
pydanticv2: 'ask' as optional
Axolotle Nov 20, 2024
aaa87ac
pydanticv2: fix missing imports
Axolotle Nov 20, 2024
353165f
pydanticv2: fix pydantic i18n error key might not be available
Axolotle Nov 20, 2024
4384129
pydanticv2: fix tests boolean handling in string types
Axolotle Nov 20, 2024
8b3bbee
pydanticv2: fix EmailOption tests
Axolotle Nov 20, 2024
7caada2
pydanticv2: fix UrlOption tests
Axolotle Nov 20, 2024
fbc2e9f
pydanticv2: fix missing imports
Axolotle Nov 20, 2024
69514f2
pydanticv2: fix validators return type
Axolotle Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ def app_install(
# Retrieve arguments list for install script
raw_options = manifest["install"]
options, form = ask_questions_and_parse_answers(raw_options, prefilled_answers=args)
args = form.dict(exclude_none=True)
args = form.model_dump(exclude_none=True)

# Validate domain / path availability for webapps
# (ideally this should be handled by the resource system for manifest v >= 2
Expand Down Expand Up @@ -1892,7 +1892,7 @@ def _apply(
previous_settings: dict[str, Any],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
) -> None:
env = {key: str(value) for key, value in form.dict().items()}
env = {key: str(value) for key, value in form.model_dump().items()}
return_content = self._call_config_script("apply", env=env)

# If the script returned validation error
Expand All @@ -1908,7 +1908,7 @@ def _apply(
)

def _run_action(self, form: "FormModel", action_id: str) -> None:
env = {key: str(value) for key, value in form.dict().items()}
env = {key: str(value) for key, value in form.model_dump().items()}
self._call_config_script(action_id, env=env)

def _call_config_script(
Expand Down
6 changes: 4 additions & 2 deletions src/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,9 @@ def _apply(
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None,
) -> None:
next_settings = {
k: v for k, v in form.dict().items() if previous_settings.get(k) != v
k: v
for k, v in form.model_dump().items()
if previous_settings.get(k) != v
}

if "default_app" in next_settings:
Expand Down Expand Up @@ -863,7 +865,7 @@ def _apply(
# that can be read by the portal API.
# FIXME remove those from the config panel saved values?

portal_values = form.dict(include=set(portal_options))
portal_values = form.model_dump(include=set(portal_options))
# Remove logo from values else filename will replace b64 content
if "portal_logo" in portal_values:
portal_values.pop("portal_logo")
Expand Down
4 changes: 2 additions & 2 deletions src/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def reset(
self.config, self.form = self._get_config_panel(prevalidate=True)

# FIXME find a better way to exclude previous settings
previous_settings = self.form.dict()
previous_settings = self.form.model_dump()

for option in self.config.options:
if not option.readonly and (
Expand Down Expand Up @@ -250,7 +250,7 @@ def _apply(
super()._apply(form, config, previous_settings, exclude=self.virtual_settings)
next_settings = {
k: v
for k, v in form.dict(exclude=self.virtual_settings).items()
for k, v in form.model_dump(exclude=self.virtual_settings).items()
if previous_settings.get(k) != v
}

Expand Down
104 changes: 56 additions & 48 deletions src/tests/test_questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,8 @@ class TestString(BaseTest):
scenarios = [
*nones(None, "", output=""),
# basic typed values
(False, "False"),
(True, "True"),
(False, FAIL),
(True, FAIL),
(0, "0"),
(1, "1"),
(-1, "-1"),
Expand Down Expand Up @@ -702,8 +702,8 @@ class TestText(BaseTest):
scenarios = [
*nones(None, "", output=""),
# basic typed values
(False, "False"),
(True, "True"),
(False, FAIL),
(True, FAIL),
(0, "0"),
(1, "1"),
(-1, "-1"),
Expand Down Expand Up @@ -743,14 +743,11 @@ class TestPassword(BaseTest):
}
# fmt: off
scenarios = [
*all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}),
*all_fails([], ["one"], {}, raw_option={"optional": True}, error=AttributeError), # FIXME those fails with AttributeError
*all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}),
*all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}),
*nones(None, "", output=""),
("s3cr3t!!", YunohostError, {"default": "SUPAs3cr3t!!"}), # default is forbidden
*xpass(scenarios=[
("s3cr3t!!", "s3cr3t!!", {"example": "SUPAs3cr3t!!"}), # example is forbidden
], reason="Should fail; example is forbidden"),
("s3cr3t!!", YunohostError, {"example": "SUPAs3cr3t!!"}), # example is forbidden
*xpass(scenarios=[
(" value \n moarc0mpl1cat3d\n ", "value \n moarc0mpl1cat3d"),
(" some_ value", "some_ value"),
Expand Down Expand Up @@ -970,17 +967,17 @@ class TestTime(BaseTest):
# 1337 seconds == 22 minutes
*all_as(1337, "1337", output="00:22"),
# Negative timestamp fails
*all_fails(-1, "-1", error=OverflowError), # FIXME should handle that as a validation error
*all_fails(-1, "-1"),
# *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}),
*all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}),
*nones(None, "", output=""),
# custom valid
*unchanged("00:00", "08:00", "12:19", "20:59", "23:59"),
("3:00", "03:00"),
("23:1", "23:01"),
("22:35:05", "22:35"),
("22:35:03.514", "22:35"),
# custom invalid
("3:00", FAIL),
("23:1", FAIL),
("24:00", FAIL),
("23:005", FAIL),
# readonly
Expand Down Expand Up @@ -1021,16 +1018,16 @@ class TestEmail(BaseTest):
"राम@मोहन.ईन्फो",
"юзер@екзампл.ком",
"θσερ@εχαμπλε.ψομ",
"葉士豪@臺網中心.tw",
"jeff@臺網中心.tw",
"葉士豪@臺網中心.台灣",
"jeff葉@臺網中心.tw",
"ñoñó@example.tld",
"甲斐黒川日本@example.tld",
"чебурашкаящик-с-апельсинами.рф@example.tld",
"उदाहरण.परीक्ष@domain.with.idn.tld",
"ιωάννης@εεττ.gr",
),
("葉士豪@臺網中心.tw", "葉士豪@臺網中心.tw"),
("jeff葉@臺網中心.tw", "jeff葉@臺網中心.tw"),
("葉士豪@臺網中心.台灣", "葉士豪@臺網中心.台灣"),
# invalid email (Hiding because our current regex is very permissive)
*all_fails(
"my@localhost",
Expand Down Expand Up @@ -1148,36 +1145,26 @@ class TestUrl(BaseTest):

*nones(None, "", output=""),
("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"),
(' https://www.example.com \n', 'https://www.example.com'),
(' https://www.example.com \n', 'https://www.example.com/'),
# readonly
("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}),
# rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py
# valid
*unchanged(
# Those are valid but not sure how they will output with pydantic
'http://example.org',
'https://example.org/whatever/next/',
'https://example.org',

'https://foo_bar.example.com/',
'http://example.co.jp',
'http://www.example.com/a%C2%B1b',
'http://www.example.com/~username/',
'http://info.example.com?fred',
'http://info.example.com/?fred',
'http://xn--mgbh0fb.xn--kgbechtv/',
'http://example.com/blue/red%3Fand+green',
'http://www.example.com/?array%5Bkey%5D=value',
'http://xn--rsum-bpad.example.org/',
'http://123.45.67.8/',
'http://123.45.67.8:8329/',
'http://[2001:db8::ff00:42]:8329',
'http://[2001::1]:8329',
'http://[2001:db8::1]/',
'http://www.example.com:8000/foo',
'http://www.cwi.nl:80/%7Eguido/Python.html',
'https://www.python.org/путь',
'http://андрей@example.com',
'https://exam_ple.com/',
'http://twitter.com/@handle/',
'http://11.11.11.11.example.com/action',
Expand All @@ -1188,25 +1175,36 @@ class TestUrl(BaseTest):
'http://example.org/path?query#fragment',
'https://foo_bar.example.com/',
'https://exam_ple.com/',
'HTTP://EXAMPLE.ORG',
'https://example.org',
'https://example.org?a=1&b=2',
'https://example.org#a=3;b=3',
'https://example.xn--p1ai',
'https://example.xn--vermgensberatung-pwb',
'https://example.xn--zfr164b',
'http://localhost/',
'http://localhost:8000/',
'http://example/#',
'http://example/#fragment',
'http://example/?#',
),
*xfail(scenarios=[
('http://test', 'http://test'),
('http://localhost', 'http://localhost'),
('http://localhost/', 'http://localhost/'),
('http://localhost:8000', 'http://localhost:8000'),
('http://localhost:8000/', 'http://localhost:8000/'),
('http://example#', 'http://example#'),
('http://example/#', 'http://example/#'),
('http://example/#fragment', 'http://example/#fragment'),
('http://example/?#', 'http://example/?#'),
], reason="Should this be valid?"),
*[
(url, url + '/')
for url in [
'http://example.org',
'https://example.org',
'http://example.co.jp',
'http://[2001:db8::ff00:42]:8329',
'http://[2001::1]:8329',
'https://example.xn--p1ai',
'https://example.xn--vermgensberatung-pwb',
'https://example.xn--zfr164b',
'http://test',
'http://localhost',
'http://localhost:8000'
]
],
('http://info.example.com?fred', 'http://info.example.com/?fred'),
('http://example#', 'http://example/#'),
('HTTP://EXAMPLE.ORG', 'http://example.org/'),
('https://example.org?a=1&b=2', 'https://example.org/?a=1&b=2'),
('https://example.org#a=3;b=3', 'https://example.org/#a=3;b=3'),
('http://www.cwi.nl:80/%7Eguido/Python.html', 'http://www.cwi.nl/%7Eguido/Python.html'),
('https://www.python.org/путь', 'https://www.python.org/%D0%BF%D1%83%D1%82%D1%8C'),
('http://андрей@example.com', 'http://%D0%B0%D0%BD%D0%B4%D1%80%D0%B5%D0%B9@example.com/'),
# invalid
*all_fails(
'ftp://example.com/',
Expand Down Expand Up @@ -1409,7 +1407,6 @@ class TestSelect(BaseTest):
},
{
"raw_options": [
{"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}},
{"choices": {"-1": "verbose -one", "0": "verbose zero", "1": "verbose one", "10": "verbose ten"}},
],
"scenarios": [
Expand All @@ -1419,9 +1416,20 @@ class TestSelect(BaseTest):
*all_fails("100", 100),
]
},
{
"raw_options": [
{"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}},
],
"scenarios": [
*nones(None, "", output=""),
*unchanged(-1, 0, 1, 10),
*all_fails("-1", "0", "1", "10"),
*all_fails("100", 100),
]
},
# [True, False, None]
*unchanged(True, False, raw_option={"choices": [True, False, None]}), # FIXME we should probably forbid None in choices
(None, FAIL, {"choices": [True, False, None]}),
(None, "", {"choices": [True, False, None]}),
{
# mixed types
"raw_options": [{"choices": ["one", 2, True]}],
Expand All @@ -1438,7 +1446,7 @@ class TestSelect(BaseTest):
"raw_options": [{"choices": ""}, {"choices": []}],
"scenarios": [
# FIXME those should fail at option level (wrong default, dev error)
*all_fails(None, ""),
*all_fails(None, "", error=YunohostError),
*xpass(scenarios=[
("", "", {"optional": True}),
(None, "", {"optional": True}),
Expand Down Expand Up @@ -1891,7 +1899,7 @@ def test_options_query_string():
"time_id": "20:55",
"email_id": "coucou@ynh.org",
"path_id": "/ynh-dev",
"url_id": "https://yunohost.org",
"url_id": "https://yunohost.org/",
"file_id": file_content1,
"select_id": "one",
"tags_id": "one,two",
Expand Down
Loading