diff --git a/aiida/manage/configuration/config.py b/aiida/manage/configuration/config.py index b785bb7447d..a6ef4bd364c 100644 --- a/aiida/manage/configuration/config.py +++ b/aiida/manage/configuration/config.py @@ -20,7 +20,14 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple import uuid -from pydantic import BaseModel, Field, ValidationError, validator # pylint: disable=no-name-in-module +from pydantic import ( # pylint: disable=no-name-in-module + BaseModel, + ConfigDict, + Field, + ValidationError, + field_serializer, + field_validator, +) from aiida.common.exceptions import ConfigurationError from aiida.common.log import LogLevels @@ -69,6 +76,8 @@ class ConfigVersionSchema(BaseModel): class ProfileOptionsSchema(BaseModel): """Schema for the options of an AiiDA profile.""" + model_config = ConfigDict(use_enum_values=True) + runner__poll__interval: int = Field(60, description='Polling interval in seconds to be used by process runners.') daemon__default_workers: int = Field( 1, description='Default number of workers to be launched by `verdi daemon start`.' @@ -130,39 +139,37 @@ class ProfileOptionsSchema(BaseModel): 5, description='Maximum number of transport task attempts before a Process is Paused.' ) rmq__task_timeout: int = Field(10, description='Timeout in seconds for communications with RabbitMQ.') - storage__sandbox: Optional[str] = Field(description='Absolute path to the directory to store sandbox folders.') + storage__sandbox: Optional[str] = Field( + None, description='Absolute path to the directory to store sandbox folders.' + ) caching__default_enabled: bool = Field(False, description='Enable calculation caching by default.') caching__enabled_for: List[str] = Field([], description='Calculation entry points to enable caching on.') caching__disabled_for: List[str] = Field([], description='Calculation entry points to disable caching on.') - class Config: - use_enum_values = True - - @validator('caching__enabled_for', 'caching__disabled_for') + @field_validator('caching__enabled_for', 'caching__disabled_for') @classmethod def validate_caching_identifier_pattern(cls, value: List[str]) -> List[str]: """Validate the caching identifier patterns.""" from aiida.manage.caching import _validate_identifier_pattern for identifier in value: - try: - _validate_identifier_pattern(identifier=identifier) - except ValueError as exception: - raise ValidationError(str(exception)) from exception + _validate_identifier_pattern(identifier=identifier) return value class GlobalOptionsSchema(ProfileOptionsSchema): """Schema for the global options of an AiiDA instance.""" - autofill__user__email: Optional[str] = Field(description='Default user email to use when creating new profiles.') + autofill__user__email: Optional[str] = Field( + None, description='Default user email to use when creating new profiles.' + ) autofill__user__first_name: Optional[str] = Field( - description='Default user first name to use when creating new profiles.' + None, description='Default user first name to use when creating new profiles.' ) autofill__user__last_name: Optional[str] = Field( - description='Default user last name to use when creating new profiles.' + None, description='Default user last name to use when creating new profiles.' ) autofill__user__institution: Optional[str] = Field( - description='Default user institution to use when creating new profiles.' + None, description='Default user institution to use when creating new profiles.' ) rest_api__profile_switching: bool = Field( False, description='Toggle whether the profile can be specified in requests submitted to the REST API.' @@ -200,21 +207,20 @@ class ProfileSchema(BaseModel): process_control: ProcessControlConfig default_user_email: Optional[str] = None test_profile: bool = False - options: Optional[ProfileOptionsSchema] + options: Optional[ProfileOptionsSchema] = None - class Config: - json_encoders = { - uuid.UUID: lambda u: str(u), # pylint: disable=unnecessary-lambda - } + @field_serializer('uuid') + def serialize_dt(self, value: uuid.UUID, _info): + return str(value) class ConfigSchema(BaseModel): """Schema for the configuration of an AiiDA instance.""" - CONFIG_VERSION: Optional[ConfigVersionSchema] - profiles: Optional[dict[str, ProfileSchema]] - options: Optional[GlobalOptionsSchema] - default_profile: Optional[str] + CONFIG_VERSION: Optional[ConfigVersionSchema] = None + profiles: Optional[dict[str, ProfileSchema]] = None + options: Optional[GlobalOptionsSchema] = None + default_profile: Optional[str] = None class Config: # pylint: disable=too-many-public-methods diff --git a/aiida/manage/configuration/options.py b/aiida/manage/configuration/options.py index bb05d99ef5a..21753e7738d 100644 --- a/aiida/manage/configuration/options.py +++ b/aiida/manage/configuration/options.py @@ -34,7 +34,7 @@ def name(self) -> str: @property def valid_type(self) -> Any: - return self._field.type_ + return self._field.annotation @property def schema(self) -> Dict[str, Any]: @@ -46,12 +46,12 @@ def default(self) -> Any: @property def description(self) -> str: - return self._field.field_info.description + return self._field.description @property def global_only(self) -> bool: from .config import ProfileOptionsSchema - return self._name in ProfileOptionsSchema.__fields__ + return self._name.replace('.', '__') not in ProfileOptionsSchema.model_fields def validate(self, value: Any) -> Any: """Validate a value @@ -63,10 +63,15 @@ def validate(self, value: Any) -> Any: :raise: ConfigValidationError """ - value, validation_error = self._field.validate(value, {}, loc=None) + from pydantic import ValidationError - if validation_error: - raise ConfigurationError(validation_error) + from .config import GlobalOptionsSchema + try: + GlobalOptionsSchema.__pydantic_validator__.validate_assignment( + GlobalOptionsSchema.model_construct(), self.name.replace('.', '__'), value + ) + except ValidationError as exception: + raise ConfigurationError(str(exception)) from exception return value @@ -74,17 +79,17 @@ def validate(self, value: Any) -> Any: def get_option_names() -> List[str]: """Return a list of available option names.""" from .config import GlobalOptionsSchema - return [key.replace('__', '.') for key in GlobalOptionsSchema.__fields__] + return [key.replace('__', '.') for key in GlobalOptionsSchema.model_fields] def get_option(name: str) -> Option: """Return option.""" from .config import GlobalOptionsSchema - options = GlobalOptionsSchema.__fields__ + options = GlobalOptionsSchema.model_fields option_name = name.replace('.', '__') if option_name not in options: raise ConfigurationError(f'the option {name} does not exist') - return Option(name, GlobalOptionsSchema.schema()['properties'][option_name], options[option_name]) + return Option(name, GlobalOptionsSchema.model_json_schema()['properties'][option_name], options[option_name]) def parse_option(option_name: str, option_value: Any) -> Tuple[Option, Any]: diff --git a/environment.yml b/environment.yml index b410a29c5ef..823594c1e09 100644 --- a/environment.yml +++ b/environment.yml @@ -26,7 +26,7 @@ dependencies: - pgsu~=0.2.1 - psutil~=5.6 - psycopg2-binary~=2.8 -- pydantic~=1.10 +- pydantic~=2.3 - pytz~=2021.1 - pyyaml~=6.0 - requests~=2.0 diff --git a/pyproject.toml b/pyproject.toml index a3ce6d893f7..a4b54fe4ad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "pgsu~=0.2.1", "psutil~=5.6", "psycopg2-binary~=2.8", - "pydantic~=1.10", + "pydantic~=2.3", "pytz~=2021.1", "pyyaml~=6.0", "requests~=2.0", diff --git a/requirements/requirements-py-3.10.txt b/requirements/requirements-py-3.10.txt index 99a8938f59e..2af6a12e247 100644 --- a/requirements/requirements-py-3.10.txt +++ b/requirements/requirements-py-3.10.txt @@ -87,7 +87,7 @@ matplotlib-inline==0.1.6 mdit-py-plugins==0.3.5 mdurl==0.1.2 mistune==3.0.1 -monty==2023.5.8 +monty==2023.9.5 mp-api==0.33.3 mpmath==1.3.0 msgpack==1.0.5 @@ -131,10 +131,10 @@ py-cpuinfo==9.0.0 pybtex==0.24.0 pycifrw==4.4.5 pycparser==2.21 -pydantic==1.10.9 +pydantic==2.3.0 pydata-sphinx-theme==0.8.1 pygments==2.15.1 -pymatgen==2023.5.31 +pymatgen==2023.9.10 pympler==0.9 pymysql==0.9.3 pynacl==1.5.0 diff --git a/requirements/requirements-py-3.11.txt b/requirements/requirements-py-3.11.txt index 398e7d5fdcf..7f38661d848 100644 --- a/requirements/requirements-py-3.11.txt +++ b/requirements/requirements-py-3.11.txt @@ -86,7 +86,7 @@ matplotlib-inline==0.1.6 mdit-py-plugins==0.3.5 mdurl==0.1.2 mistune==3.0.1 -monty==2023.5.8 +monty==2023.9.5 mp-api==0.33.3 mpmath==1.3.0 msgpack==1.0.5 @@ -130,10 +130,10 @@ py-cpuinfo==9.0.0 pybtex==0.24.0 pycifrw==4.4.5 pycparser==2.21 -pydantic==1.10.9 +pydantic==2.3.0 pydata-sphinx-theme==0.8.1 pygments==2.15.1 -pymatgen==2023.9.2 +pymatgen==2023.9.10 pympler==0.9 pymysql==0.9.3 pynacl==1.5.0 diff --git a/requirements/requirements-py-3.9.txt b/requirements/requirements-py-3.9.txt index cb62fa681d4..ba502767d85 100644 --- a/requirements/requirements-py-3.9.txt +++ b/requirements/requirements-py-3.9.txt @@ -89,7 +89,7 @@ matplotlib-inline==0.1.6 mdit-py-plugins==0.3.5 mdurl==0.1.2 mistune==3.0.1 -monty==2023.5.8 +monty==2023.9.5 mp-api==0.33.3 mpmath==1.3.0 msgpack==1.0.5 @@ -133,10 +133,10 @@ py-cpuinfo==9.0.0 pybtex==0.24.0 pycifrw==4.4.5 pycparser==2.21 -pydantic==1.10.9 +pydantic==2.3.0 pydata-sphinx-theme==0.8.1 pygments==2.15.1 -pymatgen==2023.5.31 +pymatgen==2023.9.10 pympler==0.9 pymysql==0.9.3 pynacl==1.5.0