Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion base_versions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ chacha20poly1305-reuseable==0.13.2
ifaddr==0.1.7
miniaudio==1.45
protobuf==6.31.1
pydantic==1.10.10
pydantic==2.0.0
requests==2.30.0
srptools==0.2.0
tabulate==0.9.0
Expand Down
4 changes: 2 additions & 2 deletions pyatv/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
from typing import Optional

from pyatv.support.pydantic_compat import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, field_validator

__pdoc__ = {
"InfoSettings.model_config": False,
Expand Down Expand Up @@ -95,7 +95,7 @@ def mac_validator(cls, mac: str) -> str:
raise ValueError(f"{mac} is not a valid MAC address")
return mac

@field_validator("rp_id", always=True)
@field_validator("rp_id")
@classmethod
def fill_missing_rp_id(cls, v):
"""Generate a new random rp_id if it is missing."""
Expand Down
6 changes: 4 additions & 2 deletions pyatv/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import json
from typing import Any, Iterator, List, Sequence, Tuple

from pydantic import BaseModel

from pyatv.const import Protocol
from pyatv.exceptions import DeviceIdMissingError, SettingsError
from pyatv.interface import BaseConfig, Storage
from pyatv.settings import Settings
from pyatv.support.pydantic_compat import BaseModel, model_copy
from pyatv.support.pydantic_compat import model_copy

__pdoc_dev_page__ = "/development/storage"

Expand Down Expand Up @@ -168,7 +170,7 @@ def __iter__(self) -> Iterator[Tuple[str, Any]]:
# If settings are empty for a device (e.e. no settings overridden or credentials
# saved), then the output will just be an empty dict. To not pollute the output
# with those, we do some filtering here.
dumped = self.storage_model.dict(exclude_defaults=True)
dumped = self.storage_model.model_dump(exclude_defaults=True)
dumped["devices"] = [device for device in dumped["devices"] if device != {}]
return iter(dumped.items())

Expand Down
2 changes: 1 addition & 1 deletion pyatv/storage/file_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def load(self) -> None:
_LOGGER.debug("Loading settings from %s", self._filename)
model_json = await self._loop.run_in_executor(None, self._read_file)
raw_data = json.loads(model_json)
self.storage_model = StorageModel.parse_obj(raw_data)
self.storage_model = StorageModel.model_validate(raw_data)

# Update hash based on what we read from file rather than serializing the
# model. The reasonf for this is that pydantic might (because of
Expand Down
29 changes: 13 additions & 16 deletions pyatv/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import functools
import logging
from os import environ, path
from typing import Any, List, Sequence, Union
from typing import Any, List, Sequence, Union, get_args, get_origin
import warnings

from google.protobuf.text_format import MessageToString
from pydantic import BaseModel

import pyatv
from pyatv import exceptions
from pyatv.support.pydantic_compat import BaseModel

_PROTOBUF_LINE_LENGTH = 150
_BINARY_LINE_LENGTH = 512
Expand Down Expand Up @@ -170,25 +170,22 @@ def stringify_model(model: BaseModel) -> Sequence[str]:
It is assumed optional field does not contain other models (only basic types).
"""

def _lookup_type(current_model: BaseModel, type_path: str) -> str:
splitted_path = type_path.split(".", maxsplit=1)
value = current_model.__annotations__[splitted_path[0]]
if len(splitted_path) == 1:
if value.__dict__.get("__origin__") is Union:
return ", ".join(arg.__name__ for arg in value.__args__)
return value.__name__
return _lookup_type(value, splitted_path[1])
def _lookup_type(current_model: BaseModel, field_name: str) -> str:
value = type(current_model).model_fields[field_name].annotation
if value is None:
return "Any"
if get_origin(value) is Union:
return ", ".join(arg.__name__ for arg in get_args(value))
return value.__name__

def _recurse_into(
current_model: BaseModel, prefix: str, output: List[str]
) -> Sequence[str]:
for name, field in dict(current_model).items():
if hasattr(field, "__annotations__"):
_recurse_into(
getattr(current_model, name), (prefix or "") + f"{name}.", output
)
if isinstance(field, BaseModel):
_recurse_into(field, (prefix or "") + f"{name}.", output)
else:
field_type = _lookup_type(model, f"{prefix}{name}")
field_type = _lookup_type(current_model, name)
output.append(f"{prefix}{name} = {field} ({field_type})")
return output

Expand All @@ -208,5 +205,5 @@ def update_model_field(
if len(splitted_path) > 1:
update_model_field(getattr(model, next_field), splitted_path[1], value)
else:
model.parse_obj({field: value})
model.model_validate({field: value})
setattr(model, field, value)
17 changes: 5 additions & 12 deletions pyatv/support/pydantic_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,20 @@
later on.
"""

from typing import Any, Mapping
from typing import Any, Mapping, TypeVar

# pylint: disable=unused-import
from pydantic import BaseModel

try:
from pydantic.v1 import BaseModel, Field, ValidationError # noqa
from pydantic.v1 import validator as field_validator # noqa
except ImportError:
from pydantic import BaseModel, Field, ValidationError # noqa
from pydantic import validator as field_validator # noqa
_ModelT = TypeVar("_ModelT", bound=BaseModel)

# pylint: enable=unused-import


def model_copy(model: BaseModel, /, update: Mapping[str, Any]) -> BaseModel:
def model_copy(model: _ModelT, /, update: Mapping[str, Any]) -> _ModelT:
"""Model copy compatible with pydantic v2.

Seems like pydantic v1 carries over keys with None values even though target model
doesn't have the key. Not the case with v2. This method removes keys with None
values.
"""
return model.copy(
return model.model_copy(
update={key: value for key, value in update.items() if value is not None}
)
2 changes: 1 addition & 1 deletion tests/support/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from unittest.mock import MagicMock, patch

from deepdiff import DeepDiff
from pydantic import BaseModel, Field, ValidationError
import pytest

from pyatv import exceptions
Expand All @@ -23,7 +24,6 @@
stringify_model,
update_model_field,
)
from pyatv.support.pydantic_compat import BaseModel, Field, ValidationError


class DummyException(Exception):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_storage_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def test_scan_inserts_into_storage(unicast_scan, mockfs):
# Compare content to ensure they are exactly the same.
storage2 = await new_storage(STORAGE_FILENAME, loop)
settings2 = await storage2.get_settings(conf)
assert not DeepDiff(settings2.dict(), settings1.dict())
assert not DeepDiff(settings2.model_dump(), settings1.model_dump())


async def test_provides_storage_to_pairing_handler(
Expand Down
Loading