Skip to content

Commit 19ad7a3

Browse files
committed
WIP Pydantic v2, move old models to legacy, py3.14
1 parent b94e933 commit 19ad7a3

40 files changed

+762
-149
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
max-parallel: 6
1616
matrix:
1717
os: [ "ubuntu-latest", "windows-latest", "macos-latest" ]
18-
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
18+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ]
1919

2020
runs-on: ${{ matrix.os }}
2121

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ pip install dist/*.whl
3737
- Models for RSS-specific schemas have been moved from `rss_parser.models` to `rss_parser.models.rss`. Generic types remain unchanged
3838
- Date parsing has been improved and now uses pydantic's `validator` instead of `email.utils`, producing better datetime objects where it previously defaulted to `str`
3939

40+
## Legacy Models
41+
42+
Pydantic v1-based models are still available under `rss_parser.models.legacy`. They retain the previous behaviour and re-export the `import_v1_pydantic` helper as `rss_parser.models.legacy.pydantic_proxy.import_v1_pydantic`. You can continue to use them by pointing your parser at the legacy schema:
43+
44+
```python
45+
from rss_parser import RSSParser
46+
from rss_parser.models.legacy.rss import RSS as LegacyRSS
47+
48+
class LegacyRSSParser(RSSParser):
49+
schema = LegacyRSS
50+
```
51+
52+
Tests in this repository run against both the v2 and legacy models to ensure parity.
53+
4054
## Usage
4155

4256
### Quickstart
@@ -163,18 +177,17 @@ If you don't want to deal with these conditions and want to parse something **al
163177
```python
164178
from typing import Optional
165179

180+
from pydantic import Field
181+
166182
from rss_parser.models.rss.item import Item
167183
from rss_parser.models.types.only_list import OnlyList
168184
from rss_parser.models.types.tag import Tag
169-
from rss_parser.pydantic_proxy import import_v1_pydantic
170-
171-
pydantic = import_v1_pydantic()
172185
...
173186

174187

175188
class OptionalChannelElementsMixin(...):
176189
...
177-
items: Optional[OnlyList[Tag[Item]]] = pydantic.Field(alias="item", default=[])
190+
items: Optional[OnlyList[Tag[Item]]] = Field(alias="item", default_factory=list)
178191
```
179192

180193
### Tag Field

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ packages = [{ include = "rss_parser" }, { include = "rss_parser/py.typed" }]
3939

4040
[tool.poetry.dependencies]
4141
python = "^3.9"
42-
pydantic = ">1.9"
42+
pydantic = "<3.0"
4343
xmltodict = "^0.13.0"
4444
types-xmltodict = "^0.14.0.20241009"
4545

@@ -85,6 +85,9 @@ select = [
8585
"RUF", # ruff
8686
]
8787

88+
[tool.ruff.lint.pep8-naming]
89+
ignore-names = ["LegacyRSS"]
90+
8891
[tool.ruff.per-file-ignores]
8992
"tests/**.py" = [
9093
"S101", # Use of assert detected

rss_parser/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from ._parser import AtomParser, BaseParser, RSSParser
22

3-
__all__ = ("BaseParser", "AtomParser", "RSSParser")
3+
__all__ = ("AtomParser", "BaseParser", "RSSParser")

rss_parser/_parser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def parse(
4848
if root_key:
4949
root = root.get(root_key, root)
5050

51+
if hasattr(schema, "model_validate"):
52+
return schema.model_validate(root)
53+
54+
# Pydantic v1 only
5155
return schema.parse_obj(root)
5256

5357

rss_parser/models/__init__.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
1-
"""
2-
Models created according to https://www.rssboard.org/rss-specification.
1+
from __future__ import annotations
32

4-
Some types and validation may be a bit custom to account for broken standards in some RSS feeds.
5-
"""
6-
7-
from json import loads
8-
from typing import TYPE_CHECKING
3+
from pydantic import BaseModel, ConfigDict
94

105
from rss_parser.models.utils import camel_case
11-
from rss_parser.pydantic_proxy import import_v1_pydantic
12-
13-
if TYPE_CHECKING:
14-
from pydantic import v1 as pydantic
15-
else:
16-
pydantic = import_v1_pydantic()
176

187

19-
class XMLBaseModel(pydantic.BaseModel):
20-
class Config:
21-
alias_generator = camel_case
8+
class XMLBaseModel(BaseModel):
9+
model_config = ConfigDict(alias_generator=camel_case)
2210

23-
def json_plain(self, **kw):
11+
def json_plain(self, **kwargs) -> str:
2412
"""
25-
Run pydantic's json with custom encoder to encode Tags as only content.
13+
Serialize the model while flattening Tag instances into their content.
2614
"""
2715
from rss_parser.models.types.tag import Tag # noqa: PLC0415
2816

29-
return self.json(models_as_dict=False, encoder=Tag.flatten_tag_encoder, **kw)
17+
return self.model_dump_json(fallback=Tag.flatten_tag_encoder, **kwargs)
18+
19+
def dict_plain(self, **kwargs):
20+
from rss_parser.models.types.tag import Tag # noqa: PLC0415
21+
22+
return self.model_dump(mode="json", fallback=Tag.flatten_tag_encoder, **kwargs)
23+
3024

31-
def dict_plain(self, **kw):
32-
return loads(self.json_plain(**kw))
25+
__all__ = ("XMLBaseModel",)

rss_parser/models/atom/atom.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from typing import Optional
22

3+
from pydantic import Field
4+
35
from rss_parser.models import XMLBaseModel
46
from rss_parser.models.atom.feed import Feed
57
from rss_parser.models.types.tag import Tag
6-
from rss_parser.pydantic_proxy import import_v1_pydantic
7-
8-
pydantic = import_v1_pydantic()
98

109

1110
class Atom(XMLBaseModel):
1211
"""Atom 1.0"""
1312

14-
version: Optional[Tag[str]] = pydantic.Field(alias="@version")
13+
version: Optional[Tag[str]] = Field(alias="@version", default=None)
1514
feed: Tag[Feed]

rss_parser/models/atom/entry.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
from typing import Optional
22

3+
from pydantic import Field
4+
35
from rss_parser.models import XMLBaseModel
46
from rss_parser.models.atom.person import Person
57
from rss_parser.models.types.date import DateTimeOrStr
68
from rss_parser.models.types.only_list import OnlyList
79
from rss_parser.models.types.tag import Tag
8-
from rss_parser.pydantic_proxy import import_v1_pydantic
9-
10-
pydantic = import_v1_pydantic()
1110

1211

1312
class RequiredAtomEntryMixin(XMLBaseModel):
@@ -22,10 +21,10 @@ class RequiredAtomEntryMixin(XMLBaseModel):
2221

2322

2423
class RecommendedAtomEntryMixin(XMLBaseModel):
25-
authors: Optional[OnlyList[Tag[Person]]] = pydantic.Field(alias="author", default=[])
24+
authors: Optional[OnlyList[Tag[Person]]] = Field(alias="author", default_factory=list)
2625
"Entry authors."
2726

28-
links: Optional[OnlyList[Tag[str]]] = pydantic.Field(alias="link", default=[])
27+
links: Optional[OnlyList[Tag[str]]] = Field(alias="link", default_factory=list)
2928
"The URL of the entry."
3029

3130
content: Optional[Tag[str]] = None
@@ -36,10 +35,10 @@ class RecommendedAtomEntryMixin(XMLBaseModel):
3635

3736

3837
class OptionalAtomEntryMixin(XMLBaseModel):
39-
categories: Optional[OnlyList[Tag[dict]]] = pydantic.Field(alias="category", default=[])
38+
categories: Optional[OnlyList[Tag[dict]]] = Field(alias="category", default_factory=list)
4039
"Specifies a categories that the entry belongs to."
4140

42-
contributors: Optional[OnlyList[Tag[Person]]] = pydantic.Field(alias="contributor", default=[])
41+
contributors: Optional[OnlyList[Tag[Person]]] = Field(alias="contributor", default_factory=list)
4342
"Entry contributors."
4443

4544
rights: Optional[Tag[str]] = None

rss_parser/models/atom/feed.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from typing import Optional
22

3+
from pydantic import Field
4+
35
from rss_parser.models import XMLBaseModel
46
from rss_parser.models.atom.entry import Entry
57
from rss_parser.models.atom.person import Person
68
from rss_parser.models.types.date import DateTimeOrStr
79
from rss_parser.models.types.only_list import OnlyList
810
from rss_parser.models.types.tag import Tag
9-
from rss_parser.pydantic_proxy import import_v1_pydantic
10-
11-
pydantic = import_v1_pydantic()
1211

1312

1413
class RequiredAtomFeedMixin(XMLBaseModel):
@@ -23,21 +22,21 @@ class RequiredAtomFeedMixin(XMLBaseModel):
2322

2423

2524
class RecommendedAtomFeedMixin(XMLBaseModel):
26-
authors: Optional[OnlyList[Tag[Person]]] = pydantic.Field(alias="author", default=[])
25+
authors: Optional[OnlyList[Tag[Person]]] = Field(alias="author", default_factory=list)
2726
"Names one author of the feed. A feed may have multiple author elements."
2827

29-
links: Optional[OnlyList[Tag[str]]] = pydantic.Field(alias="link", default=[])
28+
links: Optional[OnlyList[Tag[str]]] = Field(alias="link", default_factory=list)
3029
"The URL to the feed. A feed may have multiple link elements."
3130

3231

3332
class OptionalAtomFeedMixin(XMLBaseModel):
34-
entries: Optional[OnlyList[Tag[Entry]]] = pydantic.Field(alias="entry", default=[])
33+
entries: Optional[OnlyList[Tag[Entry]]] = Field(alias="entry", default_factory=list)
3534
"The entries in the feed. A feed may have multiple entry elements."
3635

37-
categories: Optional[OnlyList[Tag[dict]]] = pydantic.Field(alias="category", default=[])
36+
categories: Optional[OnlyList[Tag[dict]]] = Field(alias="category", default_factory=list)
3837
"Specifies a categories that the feed belongs to. The feed may have multiple categories elements."
3938

40-
contributors: Optional[OnlyList[Tag[Person]]] = pydantic.Field(alias="contributor", default=[])
39+
contributors: Optional[OnlyList[Tag[Person]]] = Field(alias="contributor", default_factory=list)
4140
"Feed contributors."
4241

4342
generator: Optional[Tag[str]] = None

rss_parser/models/atom/person.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
from rss_parser.models import XMLBaseModel
44
from rss_parser.models.types.tag import Tag
5-
from rss_parser.pydantic_proxy import import_v1_pydantic
6-
7-
pydantic = import_v1_pydantic()
85

96

107
class Person(XMLBaseModel):

0 commit comments

Comments
 (0)