diff --git a/docs/source/overview/configuration.rst b/docs/source/overview/configuration.rst index c8000f0..e701c7a 100644 --- a/docs/source/overview/configuration.rst +++ b/docs/source/overview/configuration.rst @@ -30,9 +30,9 @@ A full list of available settings and their defaults is provided below. .. important:: The top level ``file_systems`` field is a nested field and entries - should adhere to the :ref:`#/definitions/FileSystemSchema` schema outlined below. + should adhere to the :ref:`#/$defs/filesystemschema` schema outlined below. .. pydantic:: quota_notifier.settings.SettingsSchema -.. _#/definitions/FileSystemSchema: +.. _#/$defs/filesystemschema: .. pydantic:: quota_notifier.settings.FileSystemSchema diff --git a/pyproject.toml b/pyproject.toml index 5a3b60b..47da968 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ notifier = "quota_notifier.cli:Application.execute" [tool.poetry.dependencies] python = ">=3.8" -pydantic = "1.10.9" +pydantic-settings = "2.0.1" sqlalchemy = "2.0.17" [tool.poetry.group.tests] diff --git a/quota_notifier/settings.py b/quota_notifier/settings.py index cb8b4eb..ccf31fd 100644 --- a/quota_notifier/settings.py +++ b/quota_notifier/settings.py @@ -13,7 +13,8 @@ from tempfile import NamedTemporaryFile from typing import Any, List, Literal, Optional, Set, Tuple, Union -from pydantic import BaseSettings, Field, validator +from pydantic import field_validator, Field +from pydantic_settings import BaseSettings DEFAULT_DB_PATH = Path.cwd().resolve() / 'notifier_data.db' @@ -42,7 +43,8 @@ class FileSystemSchema(BaseSettings): title='Notification Thresholds', description='Usage percentages to issue notifications for.') - @validator('name') + @field_validator('name') + @classmethod def validate_name(cls, value: str) -> str: """Ensure the given name is not blank @@ -59,7 +61,8 @@ def validate_name(cls, value: str) -> str: return stripped - @validator('path') + @field_validator('path') + @classmethod def validate_path(cls, value: Path) -> Path: """Ensure the given system path exists @@ -78,7 +81,8 @@ def validate_path(cls, value: Path) -> Path: return value - @validator('thresholds') + @field_validator('thresholds') + @classmethod def validate_thresholds(cls, value: list) -> list: """Validate threshold values are between 0 and 100 (exclusive) @@ -188,7 +192,8 @@ class SettingsSchema(BaseSettings): default=False, description='Disable database commits and email notifications. Useful for development and testing.') - @validator('file_systems') + @field_validator('file_systems') + @classmethod def validate_unique_file_systems(cls, value: List[FileSystemSchema]) -> List[FileSystemSchema]: """Ensure file systems have unique names/paths diff --git a/tests/settings/test_applicationsettings.py b/tests/settings/test_applicationsettings.py index 249341e..191c4ae 100644 --- a/tests/settings/test_applicationsettings.py +++ b/tests/settings/test_applicationsettings.py @@ -62,7 +62,7 @@ def test_invalid_file(self) -> None: with path_obj.open('w') as io: json.dump(settings, io) - with self.assertRaisesRegex(ValidationError, 'extra fields not permitted'): + with self.assertRaisesRegex(Exception, 'Extra inputs are not permitted'): ApplicationSettings.set_from_file(path_obj) diff --git a/tests/settings/test_filesystemschema.py b/tests/settings/test_filesystemschema.py index ba608df..2ee5bff 100644 --- a/tests/settings/test_filesystemschema.py +++ b/tests/settings/test_filesystemschema.py @@ -17,11 +17,11 @@ class NameValidation(DefaultSetupTeardown, TestCase): def test_blank_name_error(self) -> None: """Test a ``ValueError`` is raised for empty/blank names""" - with self.assertRaisesRegex(ValueError, 'File system name cannot be blank'): + with self.assertRaisesRegex(Exception, 'File system name cannot be blank'): FileSystemSchema(name='') for char in string.whitespace: - with self.assertRaisesRegex(ValueError, 'File system name cannot be blank'): + with self.assertRaisesRegex(Exception, 'File system name cannot be blank'): FileSystemSchema(name=char) def test_whitespace_is_stripped(self) -> None: @@ -61,7 +61,7 @@ def test_valid_types_pass() -> None: def test_invalid_type_error(self) -> None: """Test a ``ValueError`` is raised for invalid types""" - with self.assertRaisesRegex(ValidationError, 'type\n unexpected value;'): + with self.assertRaisesRegex(Exception, 'type\n Input should be '): FileSystemSchema(type='fake_type') @@ -78,29 +78,29 @@ def test_intermediate_values_pass(self) -> None: def test_empty_list_fails(self) -> None: """Test an empty collection of thresholds fails validation""" - with self.assertRaisesRegex(ValidationError, 'At least one threshold must be specified'): + with self.assertRaisesRegex(Exception, 'At least one threshold must be specified'): FileSystemSchema(thresholds=[]) def test_zero_percent(self) -> None: """Test the value ``0`` fails validation""" - with self.assertRaisesRegex(ValidationError, 'must be greater than 0 and less than 100'): + with self.assertRaisesRegex(Exception, 'must be greater than 0 and less than 100'): FileSystemSchema(thresholds=[0, 50]) def test_100_percent(self) -> None: """Test the value ``100`` fails validation""" - with self.assertRaisesRegex(ValidationError, 'must be greater than 0 and less than 100'): + with self.assertRaisesRegex(Exception, 'must be greater than 0 and less than 100'): FileSystemSchema(thresholds=[50, 100]) def test_negative_percent(self) -> None: """Test negative values fail validation""" - with self.assertRaisesRegex(ValidationError, 'must be greater than 0 and less than 100'): + with self.assertRaisesRegex(Exception, 'must be greater than 0 and less than 100'): FileSystemSchema(thresholds=[-1, 50]) def test_over_100_percent(self) -> None: """Test values over ``100`` fail validation""" - with self.assertRaisesRegex(ValidationError, 'must be greater than 0 and less than 100'): + with self.assertRaisesRegex(Exception, 'must be greater than 0 and less than 100'): FileSystemSchema(thresholds=[50, 101]) diff --git a/tests/settings/test_settingsschema.py b/tests/settings/test_settingsschema.py index 3592347..99b0d03 100644 --- a/tests/settings/test_settingsschema.py +++ b/tests/settings/test_settingsschema.py @@ -8,37 +8,6 @@ from tests.base import DefaultSetupTeardown -class BlacklistValidation(DefaultSetupTeardown, TestCase): - """Test validation for the ``uid_blacklist`` and ``gid_blacklist`` fields""" - - def test_error_on_id_range_len_1(self) -> None: - """Test a ``ValueError`` is raised for UID and GID ranges with 1 element""" - - with self.assertRaisesRegex(ValueError, 'actual_length=1; expected_length=2'): - SettingsSchema(uid_blacklist=[[1], ]) - - with self.assertRaisesRegex(ValueError, 'actual_length=1; expected_length=2'): - SettingsSchema(gid_blacklist=[[1], ]) - - def test_error_on_id_range_len_3(self) -> None: - """Test a ``ValueError`` is raised for UID and GID ranges with 3 elements""" - - with self.assertRaisesRegex(ValueError, 'actual_length=3; expected_length=2'): - SettingsSchema(uid_blacklist=[[1, 2, 3], ]) - - with self.assertRaisesRegex(ValueError, 'actual_length=3; expected_length=2'): - SettingsSchema(gid_blacklist=[[1, 2, 3], ]) - - def test_error_on_account_names(self) -> None: - """Test a useful error message is raised when users provide account names instead of IDs""" - - with self.assertRaisesRegex(ValueError, 'value is not a valid integer'): - SettingsSchema(uid_blacklist=['root', ]) - - with self.assertRaisesRegex(ValueError, 'value is not a valid integer'): - SettingsSchema(gid_blacklist=['root', ]) - - class FileSystemValidation(DefaultSetupTeardown, TestCase): """Test validation for the ``file_systems`` field"""