Skip to content

Commit

Permalink
Update to pydantic 2
Browse files Browse the repository at this point in the history
The pydantic usage here is pretty light, so the changes needed aren't
that dramatic.

* The dataclass `__post_init_post_parse__` method is no longer supported
  and simply `__post_init__` should be used.

* `TypeError` is no longer converted to `ValidationError` in validators,
  so just use `ValueError`.

* The `@validator` decorator has changed to `@field_decorator` with a
  minor change in parameters.

* Validator error messages now include the error type, which means
  `Value error` for all of our validators.

* The `ValidationError.errors()` detail list now has a custom
  `ErrorDetail` type. This exposes the fact that the `loc` attribute is
  actually a tuple of `str` and `int`. Mypy flags that as an issue if an
  `int` is passed to `str.join`. We don't actually have any cases where
  an `int` would occur, but copy a converter from the documentation so
  it's handled correctly in case that does happen.

This also allows dropping the mypy upper version pin as updated Pydantic
contains the necessary compatibility fix.

https://docs.pydantic.dev/2.7/migration/
  • Loading branch information
dbnicholson committed May 22, 2024
1 parent 2b447e7 commit 5879080
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 89 deletions.
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ python_version = "3.11"
flake8 = "*"
flake8-bugbear = "*"
freezegun = "*"
mypy = ">=0.720, < 0.920"
mypy = ">=0.720"
pytest = "*"
pytest-aiohttp = "*"
pytest-asyncio = "*"
Expand All @@ -18,7 +18,7 @@ types-toml = "*"
[packages]
aiohttp = ">= 3.5.0"
aioredis = "~= 1.3"
pydantic = ">= 1.1, < 1.9"
pydantic = "~= 2.7"
toml = "*"

[scripts]
Expand Down
193 changes: 131 additions & 62 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 24 additions & 10 deletions azafea_metrics_proxy/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import dataclasses
import logging
import os
from typing import Any, Dict, List, MutableMapping
from typing import Any, List, MutableMapping

from pydantic.class_validators import validator
from pydantic import field_validator, ValidationError
from pydantic.dataclasses import dataclass
from pydantic.error_wrappers import ValidationError
from pydantic_core import ErrorDetails

import toml

Expand All @@ -40,18 +40,32 @@


class InvalidConfigurationError(Exception):
def __init__(self, errors: List[Dict[str, Any]]) -> None:
def __init__(self, errors: List[ErrorDetails]) -> None:
self.errors = errors

def __str__(self) -> str:
msg = ['Invalid configuration:']

for e in self.errors:
loc = e['loc']
msg.append(f"* {'.'.join(loc)}: {e['msg']}")
loc = self._loc_str(e)
msg.append(f"* {loc}: {e['msg']}")

return '\n'.join(msg)

# The loc field in ErrorDetails is a tuple of strings and ints.
# https://docs.pydantic.dev/latest/errors/errors/#customize-error-messages
@staticmethod
def _loc_str(details: ErrorDetails) -> str:
path = ''
for i, element in enumerate(details['loc']):
if isinstance(element, str):
if i > 0:
path += '.'
path += element
else: # pragma: no cover (our config does not use lists)
path += f'[{element}]'
return path


class NoSuchConfigurationError(AttributeError):
pass
Expand All @@ -66,7 +80,7 @@ def __getattr__(self, name: str) -> Any:
class Main(_Base):
verbose: bool = False

@validator('verbose', pre=True)
@field_validator('verbose', mode='before')
def verbose_is_boolean(cls, value: Any) -> bool:
return is_boolean(value)

Expand All @@ -78,11 +92,11 @@ class Redis(_Base):
password: str = DEFAULT_PASSWORD
ssl: bool = False

@validator('host', pre=True)
@field_validator('host', mode='before')
def host_is_non_empty_string(cls, value: Any) -> str:
return is_non_empty_string(value)

@validator('port', pre=True)
@field_validator('port', mode='before')
def port_is_strictly_positive_integer(cls, value: Any) -> int:
return is_strictly_positive_integer(value)

Expand All @@ -92,7 +106,7 @@ class Config(_Base):
main: Main = dataclasses.field(default_factory=Main)
redis: Redis = dataclasses.field(default_factory=Redis)

def __post_init_post_parse__(self) -> None:
def __post_init__(self) -> None:
self.warn_about_default_passwords()

@classmethod
Expand Down
Loading

0 comments on commit 5879080

Please sign in to comment.