From f1afdb2beea45189e3b538e9b7c9043445dde064 Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Fri, 11 Oct 2024 17:00:16 +0100 Subject: [PATCH 1/4] Remove support for Python 3.9 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/ci_cd_updated_main.yml | 4 ++-- .pre-commit-config.yaml | 3 +-- pyproject.toml | 3 +-- requirements-client.txt | 3 +-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17a726aba..a99dcaafb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: env: PYTEST_ADDOPTS: "--color=yes" - LINTING_PY_VERSION: "3.9" # The version of Python to use for linting (typically the minimum supported) + LINTING_PY_VERSION: "3.10" # The version of Python to use for linting (typically the minimum supported) # Cancel running workflows when additional changes are pushed # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-a-fallback-value @@ -126,7 +126,7 @@ jobs: fail-fast: false max-parallel: 4 matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] services: mongo: diff --git a/.github/workflows/ci_cd_updated_main.yml b/.github/workflows/ci_cd_updated_main.yml index 3d152063d..20aa847f6 100644 --- a/.github/workflows/ci_cd_updated_main.yml +++ b/.github/workflows/ci_cd_updated_main.yml @@ -42,11 +42,11 @@ jobs: fetch-depth: 0 submodules: true - - name: Set up Python 3.9 + - name: Set up Python 3.10 if: steps.release_check.outputs.release_run == 'false' uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.10' - name: Install dependencies if: steps.release_check.outputs.release_run == 'false' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cce17c9f..0b2327b70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ ci: submodules: true default_language_version: - python: python3.9 + python: python3.10 # pre-commit hooks repos: @@ -32,7 +32,6 @@ repos: rev: v3.17.0 hooks: - id: pyupgrade - args: ["--py39-plus"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.6.3' diff --git a/pyproject.toml b/pyproject.toml index 15d3d73af..b26cb3bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -24,7 +23,7 @@ classifiers = [ "Topic :: Database :: Database Engines/Servers", "Topic :: Database :: Front-Ends", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "lark~=1.1", "pydantic[email]~=2.2", diff --git a/requirements-client.txt b/requirements-client.txt index 678c8e91c..c0561333d 100644 --- a/requirements-client.txt +++ b/requirements-client.txt @@ -2,5 +2,4 @@ aiida-core==2.6.2 ase==3.23.0 jarvis-tools==2024.8.30 numpy>=1.20 -pymatgen==2024.8.9; python_version == '3.9' -pymatgen==2024.10.3; python_version > '3.9' +pymatgen==2024.10.3 From a3e724fcee46f7f56c6c3a5d509f8d5fc7d8dd4f Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Fri, 11 Oct 2024 17:01:22 +0100 Subject: [PATCH 2/4] `pyupgrade --py310-plus` --- .pre-commit-config.yaml | 1 + optimade/adapters/base.py | 7 +- optimade/adapters/structures/adapter.py | 2 +- optimade/adapters/structures/aiida.py | 3 +- optimade/adapters/structures/pymatgen.py | 5 +- optimade/adapters/structures/utils.py | 11 +- optimade/client/client.py | 150 +++++++++--------- optimade/client/utils.py | 3 +- optimade/exceptions.py | 10 +- optimade/filterparser/lark_parser.py | 7 +- .../filtertransformers/base_transformer.py | 14 +- optimade/filtertransformers/elasticsearch.py | 24 ++- optimade/filtertransformers/mongo.py | 4 +- optimade/models/baseinfo.py | 4 +- optimade/models/entries.py | 20 +-- optimade/models/index_metadb.py | 6 +- optimade/models/jsonapi.py | 70 ++++---- optimade/models/links.py | 10 +- optimade/models/optimade_json.py | 42 +++-- optimade/models/references.py | 58 +++---- optimade/models/responses.py | 20 +-- optimade/models/structures.py | 77 ++++----- optimade/models/utils.py | 10 +- optimade/server/config.py | 30 ++-- .../entry_collections/entry_collections.py | 18 +-- optimade/server/entry_collections/mongo.py | 8 +- optimade/server/exception_handlers.py | 7 +- optimade/server/main.py | 3 +- optimade/server/mappers/entries.py | 10 +- optimade/server/middleware.py | 12 +- optimade/server/query_params.py | 6 +- optimade/server/routers/utils.py | 22 +-- optimade/server/schemas.py | 6 +- optimade/utils.py | 8 +- optimade/validator/utils.py | 27 ++-- optimade/validator/validator.py | 44 +++-- optimade/warnings.py | 4 +- tasks.py | 4 +- tests/filtertransformers/test_base.py | 2 +- tests/models/test_utils.py | 2 +- tests/server/conftest.py | 24 +-- tests/server/query_params/conftest.py | 7 +- tests/server/routers/test_utils.py | 9 +- tests/server/test_client.py | 5 +- 44 files changed, 393 insertions(+), 423 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b2327b70..ecd8ac96d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,7 @@ repos: rev: v3.17.0 hooks: - id: pyupgrade + args: [--py310-plus] - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.6.3' diff --git a/optimade/adapters/base.py b/optimade/adapters/base.py index 5e43db56c..fd393db90 100644 --- a/optimade/adapters/base.py +++ b/optimade/adapters/base.py @@ -20,8 +20,9 @@ """ import re +from collections.abc import Callable from json import JSONDecodeError -from typing import Any, Callable, Optional, Union +from typing import Any from pydantic import BaseModel @@ -106,7 +107,7 @@ def convert(self, format: str) -> Any: return self._converted[format] @classmethod - def ingest_from(cls, data: Any, format: Optional[str] = None) -> Any: + def ingest_from(cls, data: Any, format: str | None = None) -> Any: """Convert desired format to OPTIMADE format. Parameters: @@ -182,7 +183,7 @@ def from_url(cls, url: str) -> Any: @staticmethod def _get_model_attributes( - starting_instances: Union[tuple[BaseModel, ...], list[BaseModel]], name: str + starting_instances: tuple[BaseModel, ...] | list[BaseModel], name: str ) -> Any: """Helper method for retrieving the OPTIMADE model's attribute, supporting "."-nested attributes""" for res in starting_instances: diff --git a/optimade/adapters/structures/adapter.py b/optimade/adapters/structures/adapter.py index f7641c437..41980cc2f 100644 --- a/optimade/adapters/structures/adapter.py +++ b/optimade/adapters/structures/adapter.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable from optimade.adapters.base import EntryAdapter from optimade.models import StructureResource diff --git a/optimade/adapters/structures/aiida.py b/optimade/adapters/structures/aiida.py index b26b2f23b..dcb7f0c0c 100644 --- a/optimade/adapters/structures/aiida.py +++ b/optimade/adapters/structures/aiida.py @@ -8,7 +8,6 @@ This conversion function relies on the [`aiida-core`](https://github.com/aiidateam/aiida-core) package. """ -from typing import Optional from warnings import warn from optimade.adapters.structures.utils import pad_cell, species_from_species_at_sites @@ -49,7 +48,7 @@ def get_aiida_structure_data(optimade_structure: OptimadeStructure) -> Structure structure = StructureData(cell=lattice_vectors) # If species not provided, infer data from species_at_sites - species: Optional[list[OptimadeStructureSpecies]] = attributes.species + species: list[OptimadeStructureSpecies] | None = attributes.species if not species: species = species_from_species_at_sites(attributes.species_at_sites) # type: ignore[arg-type] diff --git a/optimade/adapters/structures/pymatgen.py b/optimade/adapters/structures/pymatgen.py index e3609c228..6dec89bbe 100644 --- a/optimade/adapters/structures/pymatgen.py +++ b/optimade/adapters/structures/pymatgen.py @@ -8,7 +8,6 @@ For more information on the pymatgen code see [their documentation](https://pymatgen.org). """ -from typing import Optional, Union from warnings import warn from optimade.adapters.structures.utils import ( @@ -39,7 +38,7 @@ ) -def get_pymatgen(optimade_structure: OptimadeStructure) -> Union[Structure, Molecule]: +def get_pymatgen(optimade_structure: OptimadeStructure) -> Structure | Molecule: """Get pymatgen `Structure` or `Molecule` from OPTIMADE structure. This function will return either a pymatgen `Structure` or `Molecule` based @@ -109,7 +108,7 @@ def _get_molecule(optimade_structure: OptimadeStructure) -> Molecule: def _pymatgen_species( nsites: int, - species: Optional[list[OptimadeStructureSpecies]], + species: list[OptimadeStructureSpecies] | None, species_at_sites: list[str], ) -> list[dict[str, float]]: """ diff --git a/optimade/adapters/structures/utils.py b/optimade/adapters/structures/utils.py index 39a57e360..da186f3f9 100644 --- a/optimade/adapters/structures/utils.py +++ b/optimade/adapters/structures/utils.py @@ -5,7 +5,6 @@ """ from collections.abc import Iterable -from typing import Optional from optimade.models.structures import Species as OptimadeStructureSpecies from optimade.models.structures import Vector3D @@ -165,7 +164,7 @@ def unit_vector(x: Vector3D) -> Vector3D: def cellpar_to_cell( cellpar: list[float], ab_normal: tuple[int, int, int] = (0, 0, 1), - a_direction: Optional[tuple[int, int, int]] = None, + a_direction: tuple[int, int, int] | None = None, ) -> list[Vector3D]: """Return a 3x3 cell matrix from `cellpar=[a,b,c,alpha,beta,gamma]`. @@ -278,9 +277,9 @@ def cellpar_to_cell( def _pad_iter_of_iters( iterable: Iterable[Iterable], - padding: Optional[float] = None, - outer: Optional[type] = None, - inner: Optional[type] = None, + padding: float | None = None, + outer: type | None = None, + inner: type | None = None, ) -> tuple[Iterable[Iterable], bool]: """Turn any null/None values into a float in given iterable of iterables""" try: @@ -309,7 +308,7 @@ def _pad_iter_of_iters( def pad_cell( lattice_vectors: tuple[Vector3D, Vector3D, Vector3D], - padding: Optional[float] = None, + padding: float | None = None, ) -> tuple: # Setting this properly makes MkDocs fail. """Turn any `null`/`None` values into a `float` in given `tuple` of [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors]. diff --git a/optimade/client/client.py b/optimade/client/client.py index 2a6663970..96513b670 100644 --- a/optimade/client/client.py +++ b/optimade/client/client.py @@ -11,8 +11,8 @@ import math import time from collections import defaultdict -from collections.abc import Iterable -from typing import Any, Callable, Optional, Union +from collections.abc import Callable, Iterable +from typing import Any from urllib.parse import urlparse # External deps that are only used in the client code @@ -58,7 +58,7 @@ class OptimadeClient: """ - base_urls: Union[str, Iterable[str]] + base_urls: str | Iterable[str] """A list (or any iterable) of OPTIMADE base URLs to query.""" all_results: dict[str, dict[str, dict[str, QueryResults]]] = defaultdict(dict) @@ -71,7 +71,7 @@ class OptimadeClient: the number of results from each base URL for that particular filter. """ - max_results_per_provider: Optional[int] = None + max_results_per_provider: int | None = None """Maximum number of results to downlod per provider. If None, will download all. """ @@ -93,7 +93,7 @@ class OptimadeClient: use_async: bool """Whether or not to make all requests asynchronously using asyncio.""" - callbacks: Optional[list[Callable[[str, dict], Union[None, dict]]]] = None + callbacks: list[Callable[[str, dict], None | dict]] | None = None """A list of callbacks to execute after each successful request, used to e.g., write to a file, add results to a database or perform additional filtering. @@ -118,22 +118,20 @@ class OptimadeClient: skip_ssl: bool = False """Whether to skip SSL verification.""" - _excluded_providers: Optional[set[str]] = None + _excluded_providers: set[str] | None = None """A set of providers IDs excluded from future queries.""" - _included_providers: Optional[set[str]] = None + _included_providers: set[str] | None = None """A set of providers IDs included from future queries.""" - _excluded_databases: Optional[set[str]] = None + _excluded_databases: set[str] | None = None """A set of child database URLs excluded from future queries.""" - __current_endpoint: Optional[str] = None + __current_endpoint: str | None = None """Used internally when querying via `client.structures.get()` to set the chosen endpoint. Should be reset to `None` outside of all `get()` calls.""" - _http_client: Optional[Union[type[httpx.AsyncClient], type[requests.Session]]] = ( - None - ) + _http_client: type[httpx.AsyncClient] | type[requests.Session] | None = None """Override the HTTP client class, primarily used for testing.""" __strict_async: bool = False @@ -148,21 +146,19 @@ class OptimadeClient: def __init__( self, - base_urls: Optional[Union[str, Iterable[str]]] = None, + base_urls: str | Iterable[str] | None = None, max_results_per_provider: int = 1000, - headers: Optional[dict] = None, - http_timeout: Optional[Union[httpx.Timeout, float]] = None, + headers: dict | None = None, + http_timeout: httpx.Timeout | float | None = None, max_attempts: int = 5, use_async: bool = True, silent: bool = False, - exclude_providers: Optional[list[str]] = None, - include_providers: Optional[list[str]] = None, - exclude_databases: Optional[list[str]] = None, - http_client: Optional[ - Union[type[httpx.AsyncClient], type[requests.Session]] - ] = None, + exclude_providers: list[str] | None = None, + include_providers: list[str] | None = None, + exclude_databases: list[str] | None = None, + http_client: None | (type[httpx.AsyncClient] | type[requests.Session]) = None, verbosity: int = 0, - callbacks: Optional[list[Callable[[str, dict], Union[None, dict]]]] = None, + callbacks: list[Callable[[str, dict], None | dict]] | None = None, skip_ssl: bool = False, ): """Create the OPTIMADE client object. @@ -296,10 +292,10 @@ def __getattribute__(self, name): def get( self, - filter: Optional[str] = None, - endpoint: Optional[str] = None, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, + filter: str | None = None, + endpoint: str | None = None, + response_fields: list[str] | None = None, + sort: str | None = None, ) -> dict[str, dict[str, dict[str, dict]]]: """Gets the results from the endpoint and filter across the defined OPTIMADE APIs. @@ -355,8 +351,8 @@ def get( return {endpoint: {filter: {k: results[k].asdict() for k in results}}} def count( - self, filter: Optional[str] = None, endpoint: Optional[str] = None - ) -> dict[str, dict[str, dict[str, Optional[int]]]]: + self, filter: str | None = None, endpoint: str | None = None + ) -> dict[str, dict[str, dict[str, int | None]]]: """Counts the number of results for the filter, requiring only 1 request per provider by making use of the `meta->data_returned` key. If missing, attempts will be made to perform an exponential/binary @@ -423,7 +419,7 @@ def count( return {endpoint: {filter: count_results}} def binary_search_count( - self, filter: str, endpoint: str, base_url: str, results: Optional[dict] = None + self, filter: str, endpoint: str, base_url: str, results: dict | None = None ) -> int: """In cases where `data_returned` is not available (due to database limitations or otherwise), iteratively probe the final page of results available for a filter using @@ -453,7 +449,7 @@ def binary_search_count( ) def _binary_search_count_async( - self, filter: str, endpoint: str, base_url: str, result: Optional[dict] = None + self, filter: str, endpoint: str, base_url: str, result: dict | None = None ) -> int: """Run a series of asynchronously queries on a given API to find the number of results for a filter. @@ -530,10 +526,10 @@ def _binary_search_count_async( @staticmethod def _update_probe_and_window( - window: Optional[tuple[int, Optional[int]]] = None, - last_probe: Optional[int] = None, - below: Optional[bool] = None, - ) -> tuple[tuple[int, Optional[int]], int]: + window: tuple[int, int | None] | None = None, + last_probe: int | None = None, + below: bool | None = None, + ) -> tuple[tuple[int, int | None], int]: """Sets the new range, trial value and exit condition for exponential/binary search. When converged, returns the same value three times. @@ -650,10 +646,10 @@ def _execute_queries( self, filter: str, endpoint: str, - page_limit: Optional[int], + page_limit: int | None, paginate: bool, - response_fields: Optional[list[str]], - sort: Optional[str], + response_fields: list[str] | None, + sort: str | None, ) -> dict[str, QueryResults]: """Executes the queries over the base URLs either asynchronously or serially, depending on the `self.use_async` setting. @@ -718,11 +714,11 @@ def get_one( endpoint: str, filter: str, base_url: str, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, - page_limit: Optional[int] = None, + response_fields: list[str] | None = None, + sort: str | None = None, + page_limit: int | None = None, paginate: bool = True, - other_params: Optional[dict[str, Any]] = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """Executes the query synchronously on one API. @@ -768,12 +764,12 @@ async def _get_all_async( self, endpoint: str, filter: str, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, - page_limit: Optional[int] = None, + response_fields: list[str] | None = None, + sort: str | None = None, + page_limit: int | None = None, paginate: bool = True, - base_urls: Optional[Iterable[str]] = None, - other_params: Optional[dict[str, Any]] = None, + base_urls: Iterable[str] | None = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """Executes the query asynchronously across all defined APIs. @@ -819,12 +815,12 @@ def _get_all( self, endpoint: str, filter: str, - page_limit: Optional[int] = None, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, + page_limit: int | None = None, + response_fields: list[str] | None = None, + sort: str | None = None, paginate: bool = True, - base_urls: Optional[Iterable[str]] = None, - other_params: Optional[dict[str, Any]] = None, + base_urls: Iterable[str] | None = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """Executes the query synchronously across all defined APIs. @@ -871,11 +867,11 @@ async def get_one_async( endpoint: str, filter: str, base_url: str, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, - page_limit: Optional[int] = None, + response_fields: list[str] | None = None, + sort: str | None = None, + page_limit: int | None = None, paginate: bool = True, - other_params: Optional[dict[str, Any]] = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """Executes the query asynchronously on one API. @@ -929,11 +925,11 @@ async def _get_one_async( endpoint: str, filter: str, base_url: str, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, - page_limit: Optional[int] = None, + response_fields: list[str] | None = None, + sort: str | None = None, + page_limit: int | None = None, paginate: bool = True, - other_params: Optional[dict[str, Any]] = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """See [`OptimadeClient.get_one_async`][optimade.client.OptimadeClient.get_one_async].""" next_url, _task = self._setup( @@ -993,11 +989,11 @@ def _get_one( endpoint: str, filter: str, base_url: str, - sort: Optional[str] = None, - page_limit: Optional[int] = None, - response_fields: Optional[list[str]] = None, + sort: str | None = None, + page_limit: int | None = None, + response_fields: list[str] | None = None, paginate: bool = True, - other_params: Optional[dict[str, Any]] = None, + other_params: dict[str, Any] | None = None, ) -> dict[str, QueryResults]: """See [`OptimadeClient.get_one`][optimade.client.OptimadeClient.get_one].""" next_url, _task = self._setup( @@ -1061,10 +1057,10 @@ def _setup( endpoint: str, base_url: str, filter: str, - page_limit: Optional[int], - response_fields: Optional[list[str]], - sort: Optional[str], - other_params: Optional[dict[str, Any]] = None, + page_limit: int | None, + response_fields: list[str] | None, + sort: str | None, + other_params: dict[str, Any] | None = None, ) -> tuple[str, TaskID]: """Constructs the first query URL and creates the progress bar task. @@ -1091,13 +1087,13 @@ def _setup( def _build_url( self, base_url: str, - endpoint: Optional[str] = "structures", - version: Optional[str] = None, - filter: Optional[str] = None, - response_fields: Optional[list[str]] = None, - sort: Optional[str] = None, - page_limit: Optional[int] = None, - other_params: Optional[dict[str, Any]] = None, + endpoint: str | None = "structures", + version: str | None = None, + filter: str | None = None, + response_fields: list[str] | None = None, + sort: str | None = None, + page_limit: int | None = None, + other_params: dict[str, Any] | None = None, ) -> str: """Builds the URL to query based on the passed parameters. @@ -1177,7 +1173,7 @@ def _check_filter(self, filter: str, endpoint: str) -> None: raise RuntimeError(exc) from None def _handle_response( - self, response: Union[httpx.Response, requests.Response], _task: TaskID + self, response: httpx.Response | requests.Response, _task: TaskID ) -> tuple[dict[str, Any], str]: """Handle the response from the server. @@ -1257,8 +1253,8 @@ def _teardown(self, _task: TaskID, num_results: int) -> None: ) def _execute_callbacks( - self, results: dict, response: Union[httpx.Response, requests.Response] - ) -> Union[None, dict]: + self, results: dict, response: httpx.Response | requests.Response + ) -> None | dict: """Execute any callbacks registered with the client. Parameters: diff --git a/optimade/client/utils.py b/optimade/client/utils.py index 6aa9f3d43..c8a10e401 100644 --- a/optimade/client/utils.py +++ b/optimade/client/utils.py @@ -1,7 +1,6 @@ import sys from contextlib import contextmanager from dataclasses import asdict, dataclass, field -from typing import Union from rich.console import Console from rich.progress import ( @@ -34,7 +33,7 @@ class TooManyRequestsException(RecoverableHTTPError): class QueryResults: """A container dataclass for the results from a given query.""" - data: Union[dict, list[dict]] = field(default_factory=list, init=False) # type: ignore[assignment] + data: dict | list[dict] = field(default_factory=list, init=False) # type: ignore[assignment] errors: list[str] = field(default_factory=list, init=False) links: dict = field(default_factory=dict, init=False) included: list[dict] = field(default_factory=list, init=False) diff --git a/optimade/exceptions.py b/optimade/exceptions.py index 4274c1c8d..5b5917ce5 100644 --- a/optimade/exceptions.py +++ b/optimade/exceptions.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Any, Optional +from typing import Any __all__ = ( "OptimadeHTTPException", @@ -33,12 +33,10 @@ class OptimadeHTTPException(Exception, ABC): status_code: int title: str - detail: Optional[str] = None - headers: Optional[dict[str, Any]] = None + detail: str | None = None + headers: dict[str, Any] | None = None - def __init__( - self, detail: Optional[str] = None, headers: Optional[dict] = None - ) -> None: + def __init__(self, detail: str | None = None, headers: dict | None = None) -> None: if self.status_code is None: raise AttributeError( "HTTPException class {self.__class__.__name__} is missing required `status_code` attribute." diff --git a/optimade/filterparser/lark_parser.py b/optimade/filterparser/lark_parser.py index 67d159916..607547978 100644 --- a/optimade/filterparser/lark_parser.py +++ b/optimade/filterparser/lark_parser.py @@ -5,7 +5,6 @@ """ from pathlib import Path -from typing import Optional from lark import Lark, Tree @@ -50,7 +49,7 @@ class LarkParser: """ def __init__( - self, version: Optional[tuple[int, int, int]] = None, variant: str = "default" + self, version: tuple[int, int, int] | None = None, variant: str = "default" ): """For a given version and variant, try to load the corresponding grammar. @@ -81,8 +80,8 @@ def __init__( with open(AVAILABLE_PARSERS[version][variant]) as f: self.lark = Lark(f, maybe_placeholders=False) - self.tree: Optional[Tree] = None - self.filter: Optional[str] = None + self.tree: Tree | None = None + self.filter: str | None = None def parse(self, filter_: str) -> Tree: """Parse a filter string into a `lark.Tree`. diff --git a/optimade/filtertransformers/base_transformer.py b/optimade/filtertransformers/base_transformer.py index 1936b37c5..3a984c29c 100644 --- a/optimade/filtertransformers/base_transformer.py +++ b/optimade/filtertransformers/base_transformer.py @@ -16,7 +16,7 @@ from optimade.warnings import UnknownProviderProperty if TYPE_CHECKING: # pragma: no cover - from typing import Union + pass __all__ = ( "BaseTransformer", @@ -43,13 +43,13 @@ class Quantity: """ name: str - backend_field: Optional[str] + backend_field: str | None length_quantity: Optional["Quantity"] def __init__( self, name: str, - backend_field: Optional[str] = None, + backend_field: str | None = None, length_quantity: Optional["Quantity"] = None, ): """Initialise the `quantity` from it's name and aliases. @@ -82,8 +82,8 @@ class BaseTransformer(Transformer, abc.ABC): """ - mapper: Optional[type[BaseResourceMapper]] = None - operator_map: dict[str, Optional[str]] = { + mapper: type[BaseResourceMapper] | None = None + operator_map: dict[str, str | None] = { "<": None, "<=": None, ">": None, @@ -106,7 +106,7 @@ class BaseTransformer(Transformer, abc.ABC): _quantity_type: type[Quantity] = Quantity _quantities = None - def __init__(self, mapper: Optional[type[BaseResourceMapper]] = None): + def __init__(self, mapper: type[BaseResourceMapper] | None = None): """Initialise the transformer object, optionally loading in a resource mapper for use when post-processing. @@ -288,7 +288,7 @@ def signed_int(self, number): def number(self, number): """number: SIGNED_INT | SIGNED_FLOAT""" if TYPE_CHECKING: # pragma: no cover - type_: Union[type[int], type[float]] + type_: type[int] | type[float] if number.type == "SIGNED_INT": type_ = int diff --git a/optimade/filtertransformers/elasticsearch.py b/optimade/filtertransformers/elasticsearch.py index acbe57024..41c919d2b 100644 --- a/optimade/filtertransformers/elasticsearch.py +++ b/optimade/filtertransformers/elasticsearch.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional from elasticsearch_dsl import Field, Integer, Keyword, Q, Text from lark import v_args @@ -35,18 +35,18 @@ class ElasticsearchQuantity(Quantity): """ name: str - backend_field: Optional[str] + backend_field: str | None length_quantity: Optional["ElasticsearchQuantity"] - elastic_mapping_type: Optional[Field] + elastic_mapping_type: Field | None has_only_quantity: Optional["ElasticsearchQuantity"] nested_quantity: Optional["ElasticsearchQuantity"] def __init__( self, name: str, - backend_field: Optional[str] = None, + backend_field: str | None = None, length_quantity: Optional["ElasticsearchQuantity"] = None, - elastic_mapping_type: Optional[Field] = None, + elastic_mapping_type: Field | None = None, has_only_quantity: Optional["ElasticsearchQuantity"] = None, nested_quantity: Optional["ElasticsearchQuantity"] = None, ): @@ -102,16 +102,14 @@ class ElasticTransformer(BaseTransformer): def __init__( self, mapper: type[BaseResourceMapper], - quantities: Optional[dict[str, Quantity]] = None, + quantities: dict[str, Quantity] | None = None, ): if quantities is not None: self.quantities = quantities super().__init__(mapper=mapper) - def _field( - self, quantity: Union[str, Quantity], nested: Optional[Quantity] = None - ) -> str: + def _field(self, quantity: str | Quantity, nested: Quantity | None = None) -> str: """Used to unwrap from `property` to the string backend field name. If passed a `Quantity` (or a derived `ElasticsearchQuantity`), this method @@ -149,10 +147,10 @@ def _field( def _query_op( self, - quantity: Union[ElasticsearchQuantity, str], + quantity: ElasticsearchQuantity | str, op: str, - value: Union[str, float, int], - nested: Optional[ElasticsearchQuantity] = None, + value: str | float | int, + nested: ElasticsearchQuantity | None = None, ) -> Q: """Return a range, match, or term query for the given quantity, comparison operator, and value. @@ -462,7 +460,7 @@ def signed_int(self, number): def number(self, number): # number: SIGNED_INT | SIGNED_FLOAT if TYPE_CHECKING: # pragma: no cover - type_: Union[type[int], type[float]] + type_: type[int] | type[float] if number.type == "SIGNED_INT": type_ = int diff --git a/optimade/filtertransformers/mongo.py b/optimade/filtertransformers/mongo.py index 84402f943..f252b987f 100755 --- a/optimade/filtertransformers/mongo.py +++ b/optimade/filtertransformers/mongo.py @@ -6,7 +6,7 @@ import copy import itertools import warnings -from typing import Any, Union +from typing import Any from lark import Token, v_args @@ -567,7 +567,7 @@ def replace_str_date_with_datetime(subdict, prop, expr): ) -def recursive_postprocessing(filter_: Union[dict, list], condition, replacement): +def recursive_postprocessing(filter_: dict | list, condition, replacement): """Recursively descend into the query, checking each dictionary (contained in a list, or as an entry in another dictionary) for the condition passed. If the condition is true, apply the diff --git a/optimade/models/baseinfo.py b/optimade/models/baseinfo.py index 355425584..f9479b24b 100644 --- a/optimade/models/baseinfo.py +++ b/optimade/models/baseinfo.py @@ -1,5 +1,5 @@ import re -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal from pydantic import AnyHttpUrl, BaseModel, field_validator, model_validator @@ -101,7 +101,7 @@ class BaseInfoAttributes(BaseModel): ), ] is_index: Annotated[ - Optional[bool], + bool | None, StrictField( description="If true, this is an index meta-database base URL (see section Index Meta-Database). " "If this member is not provided, the client MUST assume this is not an index meta-database base URL " diff --git a/optimade/models/entries.py b/optimade/models/entries.py index 414b4dde8..e9a0075bb 100644 --- a/optimade/models/entries.py +++ b/optimade/models/entries.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Annotated, Any, ClassVar, Literal, Optional, Union +from typing import Annotated, Any, ClassVar, Literal from pydantic import BaseModel, field_validator @@ -27,7 +27,7 @@ class TypedRelationship(Relationship): @field_validator("data", mode="after") @classmethod def check_rel_type( - cls, data: Union[BaseRelationshipResource, list[BaseRelationshipResource]] + cls, data: BaseRelationshipResource | list[BaseRelationshipResource] ) -> list[BaseRelationshipResource]: if not isinstance(data, list): # All relationships at this point are empty-to-many relationships in JSON:API: @@ -52,14 +52,14 @@ class EntryRelationships(Relationships): """This model wraps the JSON API Relationships to include type-specific top level keys.""" references: Annotated[ - Optional[ReferenceRelationship], + ReferenceRelationship | None, StrictField( description="Object containing links to relationships with entries of the `references` type.", ), ] = None structures: Annotated[ - Optional[StructureRelationship], + StructureRelationship | None, StrictField( description="Object containing links to relationships with entries of the `structures` type.", ), @@ -70,7 +70,7 @@ class EntryResourceAttributes(Attributes): """Contains key-value pairs representing the entry's properties.""" immutable_id: Annotated[ - Optional[str], + str | None, OptimadeField( description="""The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to "the latest version" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future. @@ -89,7 +89,7 @@ class EntryResourceAttributes(Attributes): ] = None last_modified: Annotated[ - Optional[datetime], + datetime | None, OptimadeField( description="""Date and time representing when the entry was last modified. @@ -172,7 +172,7 @@ class EntryResource(Resource): ] relationships: Annotated[ - Optional[EntryRelationships], + EntryRelationships | None, StrictField( description="""A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships). The OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object.""", @@ -187,7 +187,7 @@ class EntryInfoProperty(BaseModel): ] unit: Annotated[ - Optional[str], + str | None, StrictField( description="""The physical unit of the entry property. This MUST be a valid representation of units according to version 2.1 of [The Unified Code for Units of Measure](https://unitsofmeasure.org/ucum.html). @@ -196,7 +196,7 @@ class EntryInfoProperty(BaseModel): ] = None sortable: Annotated[ - Optional[bool], + bool | None, StrictField( description="""Defines whether the entry property can be used for sorting with the "sort" parameter. If the entry listing endpoint supports sorting, this key MUST be present for sortable properties with value `true`.""", @@ -204,7 +204,7 @@ class EntryInfoProperty(BaseModel): ] = None type: Annotated[ - Optional[DataType], + DataType | None, StrictField( title="Type", description="""The type of the property's value. diff --git a/optimade/models/index_metadb.py b/optimade/models/index_metadb.py index 83733f5fe..8520c1217 100644 --- a/optimade/models/index_metadb.py +++ b/optimade/models/index_metadb.py @@ -1,4 +1,4 @@ -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal from pydantic import BaseModel @@ -35,7 +35,7 @@ class IndexRelationship(BaseModel): """Index Meta-Database relationship""" data: Annotated[ - Optional[RelatedLinksResource], + RelatedLinksResource | None, StrictField( description="""[JSON API resource linkage](http://jsonapi.org/format/1.0/#document-links). It MUST be either `null` or contain a single Links identifier object with the fields `id` and `type`""", @@ -48,7 +48,7 @@ class IndexInfoResource(BaseInfoResource): attributes: IndexInfoAttributes relationships: Annotated[ # type: ignore[assignment] - Optional[dict[Literal["default"], IndexRelationship]], + dict[Literal["default"], IndexRelationship] | None, StrictField( title="Relationships", description="""Reference to the Links identifier object under the `links` endpoint that the provider has chosen as their 'default' OPTIMADE API database. diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py index dc21314ba..312060845 100644 --- a/optimade/models/jsonapi.py +++ b/optimade/models/jsonapi.py @@ -45,7 +45,7 @@ class Link(BaseModel): AnyUrl, StrictField(description="a string containing the link's URL.") ] meta: Annotated[ - Optional[Meta], + Meta | None, StrictField( description="a meta object containing non-standard meta-information about the link.", ), @@ -62,7 +62,7 @@ class JsonApi(BaseModel): "1.0" ) meta: Annotated[ - Optional[Meta], StrictField(description="Non-standard meta information") + Meta | None, StrictField(description="Non-standard meta information") ] = None @@ -72,24 +72,24 @@ class ToplevelLinks(BaseModel): model_config = ConfigDict(extra="allow") self: Annotated[ - Optional[JsonLinkType], StrictField(description="A link to itself") + JsonLinkType | None, StrictField(description="A link to itself") ] = None related: Annotated[ - Optional[JsonLinkType], StrictField(description="A related resource link") + JsonLinkType | None, StrictField(description="A related resource link") ] = None # Pagination first: Annotated[ - Optional[JsonLinkType], StrictField(description="The first page of data") + JsonLinkType | None, StrictField(description="The first page of data") ] = None last: Annotated[ - Optional[JsonLinkType], StrictField(description="The last page of data") + JsonLinkType | None, StrictField(description="The last page of data") ] = None prev: Annotated[ - Optional[JsonLinkType], StrictField(description="The previous page of data") + JsonLinkType | None, StrictField(description="The previous page of data") ] = None next: Annotated[ - Optional[JsonLinkType], StrictField(description="The next page of data") + JsonLinkType | None, StrictField(description="The next page of data") ] = None @model_validator(mode="after") @@ -113,7 +113,7 @@ class ErrorLinks(BaseModel): """A Links object specific to Error objects""" about: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="A link that leads to further details about this particular occurrence of the problem.", ), @@ -124,14 +124,14 @@ class ErrorSource(BaseModel): """an object containing references to the source of the error""" pointer: Annotated[ - Optional[str], + str | None, StrictField( description="a JSON Pointer [RFC6901] to the associated entity in the request document " '[e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].', ), ] = None parameter: Annotated[ - Optional[str], + str | None, StrictField( description="a string indicating which URI query parameter caused the error.", ), @@ -142,47 +142,47 @@ class Error(BaseModel): """An error response""" id: Annotated[ - Optional[str], + str | None, StrictField( description="A unique identifier for this particular occurrence of the problem.", ), ] = None links: Annotated[ - Optional[ErrorLinks], StrictField(description="A links object storing about") + ErrorLinks | None, StrictField(description="A links object storing about") ] = None status: Annotated[ - Optional[Annotated[str, BeforeValidator(str)]], + Annotated[str, BeforeValidator(str)] | None, StrictField( description="the HTTP status code applicable to this problem, expressed as a string value.", ), ] = None code: Annotated[ - Optional[str], + str | None, StrictField( description="an application-specific error code, expressed as a string value.", ), ] = None title: Annotated[ - Optional[str], + str | None, StrictField( description="A short, human-readable summary of the problem. " "It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.", ), ] = None detail: Annotated[ - Optional[str], + str | None, StrictField( description="A human-readable explanation specific to this occurrence of the problem.", ), ] = None source: Annotated[ - Optional[ErrorSource], + ErrorSource | None, StrictField( description="An object containing references to the source of the error" ), ] = None meta: Annotated[ - Optional[Meta], + Meta | None, StrictField( description="a meta object containing non-standard meta-information about the error.", ), @@ -232,7 +232,7 @@ class RelationshipLinks(BaseModel): """ self: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="""A link for the relationship itself (a 'relationship link'). This link allows the client to directly manipulate the relationship. @@ -241,7 +241,7 @@ class RelationshipLinks(BaseModel): ), ] = None related: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="A [related resource link](https://jsonapi.org/format/1.0/#document-resource-object-related-resource-links).", ), @@ -260,17 +260,17 @@ class Relationship(BaseModel): """Representation references from the resource object in which it's defined to other resource objects.""" links: Annotated[ - Optional[RelationshipLinks], + RelationshipLinks | None, StrictField( description="a links object containing at least one of the following: self, related", ), ] = None data: Annotated[ - Optional[Union[BaseResource, list[BaseResource]]], + BaseResource | list[BaseResource] | None, StrictField(description="Resource linkage"), ] = None meta: Annotated[ - Optional[Meta], + Meta | None, StrictField( description="a meta object that contains non-standard meta-information about the relationship.", ), @@ -308,7 +308,7 @@ class ResourceLinks(BaseModel): """A Resource Links object""" self: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="A link that identifies the resource represented by the resource object.", ), @@ -342,25 +342,25 @@ class Resource(BaseResource): """Resource objects appear in a JSON API document to represent resources.""" links: Annotated[ - Optional[ResourceLinks], + ResourceLinks | None, StrictField( description="a links object containing links related to the resource." ), ] = None meta: Annotated[ - Optional[Meta], + Meta | None, StrictField( description="a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.", ), ] = None attributes: Annotated[ - Optional[Attributes], + Attributes | None, StrictField( description="an attributes object representing some of the resource’s data.", ), ] = None relationships: Annotated[ - Optional[Relationships], + Relationships | None, StrictField( description="""[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships) describing relationships between the resource and other JSON API resources.""", @@ -372,31 +372,31 @@ class Response(BaseModel): """A top-level response.""" data: Annotated[ - Optional[Union[None, Resource, list[Resource]]], + None | Resource | list[Resource] | None, StrictField(description="Outputted Data", uniqueItems=True), ] = None meta: Annotated[ - Optional[Meta], + Meta | None, StrictField( description="A meta object containing non-standard information related to the Success", ), ] = None errors: Annotated[ - Optional[list[Error]], + list[Error] | None, StrictField(description="A list of unique errors", uniqueItems=True), ] = None included: Annotated[ - Optional[list[Resource]], + list[Resource] | None, StrictField( description="A list of unique included resources", uniqueItems=True ), ] = None links: Annotated[ - Optional[ToplevelLinks], + ToplevelLinks | None, StrictField(description="Links associated with the primary data or errors"), ] = None jsonapi: Annotated[ - Optional[JsonApi], + JsonApi | None, StrictField(description="Information about the JSON API used"), ] = None diff --git a/optimade/models/links.py b/optimade/models/links.py index 2605f4737..f8898e5a8 100644 --- a/optimade/models/links.py +++ b/optimade/models/links.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal from pydantic import model_validator @@ -47,14 +47,14 @@ class LinksResourceAttributes(Attributes): ), ] base_url: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="JSON API links object, pointing to the base URL for this implementation", ), ] homepage: Annotated[ - Optional[JsonLinkType], + JsonLinkType | None, StrictField( description="JSON API links object, pointing to a homepage URL for this implementation", ), @@ -70,7 +70,7 @@ class LinksResourceAttributes(Attributes): ] aggregate: Annotated[ - Optional[Aggregate], + Aggregate | None, StrictField( title="Aggregate", description="""A string indicating whether a client that is following links to aggregate results from different OPTIMADE implementations should follow this link or not. @@ -87,7 +87,7 @@ class LinksResourceAttributes(Attributes): ] = Aggregate.OK no_aggregate_reason: Annotated[ - Optional[str], + str | None, StrictField( description="""An OPTIONAL human-readable string indicating the reason for suggesting not to aggregate results following the link. It SHOULD NOT be present if `aggregate`=`ok`.""", diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py index 1aace40ed..a3d39d089 100644 --- a/optimade/models/optimade_json.py +++ b/optimade/models/optimade_json.py @@ -2,7 +2,7 @@ from datetime import datetime from enum import Enum -from typing import Annotated, Any, Literal, Optional, Union +from typing import Annotated, Any, Literal, Optional from pydantic import BaseModel, ConfigDict, EmailStr, Field, model_validator @@ -50,9 +50,7 @@ def get_values(cls) -> list[str]: return sorted(_.value for _ in cls) @classmethod - def from_python_type( - cls, python_type: Union[type, str, object] - ) -> Optional["DataType"]: + def from_python_type(cls, python_type: type | str | object) -> Optional["DataType"]: """Get OPTIMADE data type from a Python type""" mapping = { "bool": cls.BOOLEAN, @@ -221,7 +219,7 @@ class Provider(BaseModel): ] homepage: Annotated[ - Optional[jsonapi.JsonLinkType], + jsonapi.JsonLinkType | None, StrictField( description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) " "pointing to homepage of the database provider, either " @@ -242,37 +240,37 @@ class Implementation(BaseModel): """Information on the server implementation""" name: Annotated[ - Optional[str], StrictField(description="name of the implementation") + str | None, StrictField(description="name of the implementation") ] = None version: Annotated[ - Optional[str], + str | None, StrictField(description="version string of the current implementation"), ] = None homepage: Annotated[ - Optional[jsonapi.JsonLinkType], + jsonapi.JsonLinkType | None, StrictField( description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the homepage of the implementation.", ), ] = None source_url: Annotated[ - Optional[jsonapi.JsonLinkType], + jsonapi.JsonLinkType | None, StrictField( description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation source, either downloadable archive or version control system.", ), ] = None maintainer: Annotated[ - Optional[ImplementationMaintainer], + ImplementationMaintainer | None, StrictField( description="A dictionary providing details about the maintainer of the implementation.", ), ] = None issue_tracker: Annotated[ - Optional[jsonapi.JsonLinkType], + jsonapi.JsonLinkType | None, StrictField( description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation's issue tracker.", ), @@ -313,7 +311,7 @@ class ResponseMeta(jsonapi.Meta): # start of "SHOULD" fields for meta response optimade_schema: Annotated[ - Optional[jsonapi.JsonLinkType], + jsonapi.JsonLinkType | None, StrictField( alias="schema", description="""A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) that points to a schema for the response. @@ -324,14 +322,14 @@ class ResponseMeta(jsonapi.Meta): ] = None time_stamp: Annotated[ - Optional[datetime], + datetime | None, StrictField( description="A timestamp containing the date and time at which the query was executed.", ), ] = None data_returned: Annotated[ - Optional[int], + int | None, StrictField( description="An integer containing the total number of data resource objects returned for the current `filter` query, independent of pagination.", ge=0, @@ -339,7 +337,7 @@ class ResponseMeta(jsonapi.Meta): ] = None provider: Annotated[ - Optional[Provider], + Provider | None, StrictField( description="information on the database provider of the implementation." ), @@ -347,28 +345,28 @@ class ResponseMeta(jsonapi.Meta): # start of "MAY" fields for meta response data_available: Annotated[ - Optional[int], + int | None, StrictField( description="An integer containing the total number of data resource objects available in the database for the endpoint.", ), ] = None last_id: Annotated[ - Optional[str], + str | None, StrictField(description="a string containing the last ID returned"), ] = None response_message: Annotated[ - Optional[str], StrictField(description="response string from the server") + str | None, StrictField(description="response string from the server") ] = None implementation: Annotated[ - Optional[Implementation], + Implementation | None, StrictField(description="a dictionary describing the server implementation"), ] = None warnings: Annotated[ - Optional[list[Warnings]], + list[Warnings] | None, StrictField( description="""A list of warning resource objects representing non-critical errors or warnings. A warning resource object is defined similarly to a [JSON API error object](http://jsonapi.org/format/1.0/#error-objects), but MUST also include the field `type`, which MUST have the value `"warning"`. @@ -419,7 +417,7 @@ class BaseRelationshipResource(jsonapi.BaseResource): """Minimum requirements to represent a relationship resource""" meta: Annotated[ - Optional[BaseRelationshipMeta], + BaseRelationshipMeta | None, StrictField( description="Relationship meta field. MUST contain 'description' if supplied.", ), @@ -430,6 +428,6 @@ class Relationship(jsonapi.Relationship): """Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource.""" data: Annotated[ - Optional[Union[BaseRelationshipResource, list[BaseRelationshipResource]]], + BaseRelationshipResource | list[BaseRelationshipResource] | None, StrictField(description="Resource linkage", uniqueItems=True), ] = None diff --git a/optimade/models/references.py b/optimade/models/references.py index e238b5dd6..9b87220b8 100644 --- a/optimade/models/references.py +++ b/optimade/models/references.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, Literal, Optional +from typing import Annotated, Any, Literal from pydantic import AnyUrl, BaseModel, field_validator @@ -21,7 +21,7 @@ class Person(BaseModel): ] firstname: Annotated[ - Optional[str], + str | None, OptimadeField( description="""First name of the person.""", support=SupportLevel.OPTIONAL, @@ -30,7 +30,7 @@ class Person(BaseModel): ] = None lastname: Annotated[ - Optional[str], + str | None, OptimadeField( description="""Last name of the person.""", support=SupportLevel.OPTIONAL, @@ -48,7 +48,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): """ authors: Annotated[ - Optional[list[Person]], + list[Person] | None, OptimadeField( description="List of person objects containing the authors of the reference.", support=SupportLevel.OPTIONAL, @@ -57,7 +57,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None editors: Annotated[ - Optional[list[Person]], + list[Person] | None, OptimadeField( description="List of person objects containing the editors of the reference.", support=SupportLevel.OPTIONAL, @@ -66,7 +66,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None doi: Annotated[ - Optional[str], + str | None, OptimadeField( description="The digital object identifier of the reference.", support=SupportLevel.OPTIONAL, @@ -75,7 +75,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None url: Annotated[ - Optional[AnyUrl], + AnyUrl | None, OptimadeField( description="The URL of the reference.", support=SupportLevel.OPTIONAL, @@ -84,7 +84,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None address: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -93,7 +93,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None annote: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -102,7 +102,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None booktitle: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -111,7 +111,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None chapter: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -120,7 +120,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None crossref: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -129,7 +129,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None edition: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -138,7 +138,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None howpublished: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -147,7 +147,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None institution: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -156,7 +156,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None journal: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -165,7 +165,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None key: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -174,7 +174,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None month: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -183,7 +183,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None note: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -192,7 +192,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None number: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -201,7 +201,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None organization: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -210,7 +210,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None pages: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -219,7 +219,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None publisher: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -228,7 +228,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None school: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -237,7 +237,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None series: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -246,7 +246,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None title: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -255,7 +255,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None bib_type: Annotated[ - Optional[str], + str | None, OptimadeField( description="Type of the reference, corresponding to the **type** property in the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -264,7 +264,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None volume: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, @@ -273,7 +273,7 @@ class ReferenceResourceAttributes(EntryResourceAttributes): ] = None year: Annotated[ - Optional[str], + str | None, OptimadeField( description="Meaning of property matches the BiBTeX specification.", support=SupportLevel.OPTIONAL, diff --git a/optimade/models/responses.py b/optimade/models/responses.py index c7cf72855..9ed00b150 100644 --- a/optimade/models/responses.py +++ b/optimade/models/responses.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Any from pydantic import model_validator @@ -70,14 +70,14 @@ class InfoResponse(Success): class EntryResponseOne(Success): data: Annotated[ - Optional[Union[EntryResource, dict[str, Any]]], + EntryResource | dict[str, Any] | None, StrictField( description="The single entry resource returned by this query.", union_mode="left_to_right", ), ] = None # type: ignore[assignment] included: Annotated[ - Optional[Union[list[EntryResource], list[dict[str, Any]]]], + list[EntryResource] | list[dict[str, Any]] | None, StrictField( description="A list of unique included OPTIMADE entry resources.", uniqueItems=True, @@ -88,7 +88,7 @@ class EntryResponseOne(Success): class EntryResponseMany(Success): data: Annotated[ # type: ignore[assignment] - Union[list[EntryResource], list[dict[str, Any]]], + list[EntryResource] | list[dict[str, Any]], StrictField( description="List of unique OPTIMADE entry resource objects.", uniqueItems=True, @@ -96,7 +96,7 @@ class EntryResponseMany(Success): ), ] included: Annotated[ - Optional[Union[list[EntryResource], list[dict[str, Any]]]], + list[EntryResource] | list[dict[str, Any]] | None, StrictField( description="A list of unique included OPTIMADE entry resources.", uniqueItems=True, @@ -107,7 +107,7 @@ class EntryResponseMany(Success): class LinksResponse(EntryResponseMany): data: Annotated[ - Union[list[LinksResource], list[dict[str, Any]]], + list[LinksResource] | list[dict[str, Any]], StrictField( description="List of unique OPTIMADE links resource objects.", uniqueItems=True, @@ -118,7 +118,7 @@ class LinksResponse(EntryResponseMany): class StructureResponseOne(EntryResponseOne): data: Annotated[ - Optional[Union[StructureResource, dict[str, Any]]], + StructureResource | dict[str, Any] | None, StrictField( description="A single structures entry resource.", union_mode="left_to_right", @@ -128,7 +128,7 @@ class StructureResponseOne(EntryResponseOne): class StructureResponseMany(EntryResponseMany): data: Annotated[ - Union[list[StructureResource], list[dict[str, Any]]], + list[StructureResource] | list[dict[str, Any]], StrictField( description="List of unique OPTIMADE structures entry resource objects.", uniqueItems=True, @@ -139,7 +139,7 @@ class StructureResponseMany(EntryResponseMany): class ReferenceResponseOne(EntryResponseOne): data: Annotated[ - Optional[Union[ReferenceResource, dict[str, Any]]], + ReferenceResource | dict[str, Any] | None, StrictField( description="A single references entry resource.", union_mode="left_to_right", @@ -149,7 +149,7 @@ class ReferenceResponseOne(EntryResponseOne): class ReferenceResponseMany(EntryResponseMany): data: Annotated[ - Union[list[ReferenceResource], list[dict[str, Any]]], + list[ReferenceResource] | list[dict[str, Any]], StrictField( description="List of unique OPTIMADE references entry resource objects.", uniqueItems=True, diff --git a/optimade/models/structures.py b/optimade/models/structures.py index f9ba8aa6e..bdedd24e8 100644 --- a/optimade/models/structures.py +++ b/optimade/models/structures.py @@ -1,7 +1,7 @@ import re import warnings from enum import Enum, IntEnum -from typing import TYPE_CHECKING, Annotated, Literal, Optional, Union +from typing import TYPE_CHECKING, Annotated, Literal, Optional from pydantic import BaseModel, BeforeValidator, Field, field_validator, model_validator @@ -120,7 +120,7 @@ class Species(BaseModel): ] mass: Annotated[ - Optional[list[float]], + list[float] | None, OptimadeField( description="""If present MUST be a list of floats expressed in a.m.u. Elements denoting vacancies MUST have masses equal to 0.""", @@ -131,7 +131,7 @@ class Species(BaseModel): ] = None original_name: Annotated[ - Optional[str], + str | None, OptimadeField( description="""Can be any valid Unicode string, and SHOULD contain (if specified) the name of the species that is used internally in the source database. @@ -142,7 +142,7 @@ class Species(BaseModel): ] = None attached: Annotated[ - Optional[list[str]], + list[str] | None, OptimadeField( description="""If provided MUST be a list of length 1 or more of strings of chemical symbols for the elements attached to this site, or "X" for a non-chemical element.""", support=SupportLevel.OPTIONAL, @@ -151,7 +151,7 @@ class Species(BaseModel): ] = None nattached: Annotated[ - Optional[list[int]], + list[int] | None, OptimadeField( description="""If provided MUST be a list of length 1 or more of integers indicating the number of attached atoms of the kind specified in the value of the :field:`attached` key.""", support=SupportLevel.OPTIONAL, @@ -161,8 +161,8 @@ class Species(BaseModel): @field_validator("concentration", "mass", mode="after") def validate_concentration_and_mass( - cls, value: Optional[list[float]], info: "ValidationInfo" - ) -> Optional[list[float]]: + cls, value: list[float] | None, info: "ValidationInfo" + ) -> list[float] | None: if not value: return value @@ -181,8 +181,8 @@ def validate_concentration_and_mass( @field_validator("attached", "nattached", mode="after") @classmethod def validate_minimum_list_length( - cls, value: Optional[Union[list[str], list[int]]] - ) -> Optional[Union[list[str], list[int]]]: + cls, value: list[str] | list[int] | None + ) -> list[str] | list[int] | None: if value is not None and len(value) < 1: raise ValueError( "The list's length MUST be 1 or more, instead it was found to be " @@ -285,7 +285,7 @@ class StructureResourceAttributes(EntryResourceAttributes): """This class contains the Field for the attributes used to represent a structure, e.g. unit cell, atoms, positions.""" elements: Annotated[ - Optional[list[str]], + list[str] | None, OptimadeField( description="""The chemical symbols of the different elements present in the structure. @@ -313,7 +313,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None nelements: Annotated[ - Optional[int], + int | None, OptimadeField( description="""Number of different elements in the structure as an integer. @@ -337,7 +337,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None elements_ratios: Annotated[ - Optional[list[float]], + list[float] | None, OptimadeField( description="""Relative proportions of different elements in the structure. @@ -364,7 +364,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None chemical_formula_descriptive: Annotated[ - Optional[str], + str | None, OptimadeField( description="""The chemical formula for a structure as a string in a form chosen by the API implementation. @@ -394,7 +394,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None chemical_formula_reduced: Annotated[ - Optional[str], + str | None, OptimadeField( description="""The reduced chemical formula for a structure as a string with element symbols and integer chemical proportion numbers. The proportion number MUST be omitted if it is 1. @@ -426,7 +426,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None chemical_formula_hill: Annotated[ - Optional[str], + str | None, OptimadeField( description="""The chemical formula for a structure in [Hill form](https://dx.doi.org/10.1021/ja02046a005) with element symbols followed by integer chemical proportion numbers. The proportion number MUST be omitted if it is 1. @@ -459,7 +459,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None chemical_formula_anonymous: Annotated[ - Optional[str], + str | None, OptimadeField( description="""The anonymous formula is the `chemical_formula_reduced`, but where the elements are instead first ordered by their chemical proportion number, and then, in order left to right, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and so on. @@ -483,7 +483,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None dimension_types: Annotated[ - Optional[list[Periodicity]], + list[Periodicity] | None, OptimadeField( min_length=3, max_length=3, @@ -511,7 +511,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None nperiodic_dimensions: Annotated[ - Optional[int], + int | None, OptimadeField( description="""An integer specifying the number of periodic dimensions in the structure, equivalent to the number of non-zero entries in `dimension_types`. @@ -535,7 +535,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None lattice_vectors: Annotated[ - Optional[list[Vector3D_unknown]], + list[Vector3D_unknown] | None, OptimadeField( min_length=3, max_length=3, @@ -565,7 +565,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None cartesian_site_positions: Annotated[ - Optional[list[Vector3D]], + list[Vector3D] | None, OptimadeField( description="""Cartesian positions of each site in the structure. A site is usually used to describe positions of atoms; what atoms can be encountered at a given site is conveyed by the `species_at_sites` property, and the species themselves are described in the `species` property. @@ -588,7 +588,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None nsites: Annotated[ - Optional[int], + int | None, OptimadeField( description="""An integer specifying the length of the `cartesian_site_positions` property. @@ -610,7 +610,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None species: Annotated[ - Optional[list[Species]], + list[Species] | None, OptimadeField( description="""A list describing the species of the sites of this structure. Species can represent pure chemical elements, virtual-crystal atoms representing a statistical occupation of a given site by multiple chemical elements, and/or a location to which there are attached atoms, i.e., atoms whose precise location are unknown beyond that they are attached to that position (frequently used to indicate hydrogen atoms attached to another element, e.g., a carbon with three attached hydrogens might represent a methyl group, -CH3). @@ -681,7 +681,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None species_at_sites: Annotated[ - Optional[list[str]], + list[str] | None, OptimadeField( description="""Name of the species at each site (where values for sites are specified with the same order of the property `cartesian_site_positions`). The properties of the species are found in the property `species`. @@ -707,7 +707,7 @@ class StructureResourceAttributes(EntryResourceAttributes): ] = None assemblies: Annotated[ - Optional[list[Assembly]], + list[Assembly] | None, OptimadeField( description="""A description of groups of sites that are statistically correlated. @@ -871,8 +871,8 @@ def warn_on_missing_correlated_fields(self) -> "StructureResourceAttributes": @field_validator("chemical_formula_reduced", "chemical_formula_hill", mode="after") @classmethod def check_ordered_formula( - cls, value: Optional[str], info: "ValidationInfo" - ) -> Optional[str]: + cls, value: str | None, info: "ValidationInfo" + ) -> str | None: if value is None: return value @@ -904,7 +904,7 @@ def check_ordered_formula( @field_validator("chemical_formula_anonymous", mode="after") @classmethod - def check_anonymous_formula(cls, value: Optional[str]) -> Optional[str]: + def check_anonymous_formula(cls, value: str | None) -> str | None: if value is None: return value @@ -934,8 +934,8 @@ def check_anonymous_formula(cls, value: Optional[str]) -> Optional[str]: ) @classmethod def check_reduced_formulae( - cls, value: Optional[str], info: "ValidationInfo" - ) -> Optional[str]: + cls, value: str | None, info: "ValidationInfo" + ) -> str | None: if value is None: return value @@ -950,9 +950,7 @@ def check_reduced_formulae( @field_validator("elements", mode="after") @classmethod - def elements_must_be_alphabetical( - cls, value: Optional[list[str]] - ) -> Optional[list[str]]: + def elements_must_be_alphabetical(cls, value: list[str] | None) -> list[str] | None: if value is None: return value @@ -962,9 +960,7 @@ def elements_must_be_alphabetical( @field_validator("elements_ratios", mode="after") @classmethod - def ratios_must_sum_to_one( - cls, value: Optional[list[float]] - ) -> Optional[list[float]]: + def ratios_must_sum_to_one(cls, value: list[float] | None) -> list[float] | None: if value is None: return value @@ -1005,10 +1001,9 @@ def check_dimensions_types_dependencies(self) -> "StructureResourceAttributes": @classmethod def null_values_for_whole_vector( cls, - value: Optional[ - Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)] - ], - ) -> Optional[Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)]]: + value: None + | (Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)]), + ) -> Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)] | None: if value is None: return value @@ -1061,9 +1056,7 @@ def validate_species_at_sites(self) -> "StructureResourceAttributes": @field_validator("species", mode="after") @classmethod - def validate_species( - cls, value: Optional[list[Species]] - ) -> Optional[list[Species]]: + def validate_species(cls, value: list[Species] | None) -> list[Species] | None: if value is None: return value diff --git a/optimade/models/utils.py b/optimade/models/utils.py index 44f3109c5..fde64c6e7 100644 --- a/optimade/models/utils.py +++ b/optimade/models/utils.py @@ -5,7 +5,7 @@ import warnings from enum import Enum from functools import reduce -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any from pydantic import Field from pydantic_core import PydanticUndefined @@ -40,7 +40,7 @@ class SupportLevel(Enum): def StrictField( default: "Any" = PydanticUndefined, *, - description: Optional[str] = None, + description: str | None = None, **kwargs: "Any", ) -> Any: """A wrapper around `pydantic.Field` that does the following: @@ -123,9 +123,9 @@ def StrictField( def OptimadeField( default: "Any" = PydanticUndefined, *, - support: Optional[Union[str, SupportLevel]] = None, - queryable: Optional[Union[str, SupportLevel]] = None, - unit: Optional[str] = None, + support: str | SupportLevel | None = None, + queryable: str | SupportLevel | None = None, + unit: str | None = None, **kwargs, ) -> Any: """A wrapper around `pydantic.Field` that adds OPTIMADE-specific diff --git a/optimade/server/config.py b/optimade/server/config.py index 8fdc5666c..29ba11a1f 100644 --- a/optimade/server/config.py +++ b/optimade/server/config.py @@ -3,7 +3,7 @@ import warnings from enum import Enum from pathlib import Path -from typing import Annotated, Any, Literal, Optional, Union +from typing import Annotated, Any, Literal import yaml from pydantic import AnyHttpUrl, Field, field_validator, model_validator @@ -171,7 +171,7 @@ class ServerConfig(BaseSettings): ] = True insert_from_jsonl: Annotated[ - Optional[Path], + Path | None, Field( description=( "The absolute path to an OPTIMADE JSONL file to use to initialize the database. " @@ -181,7 +181,7 @@ class ServerConfig(BaseSettings): ] = None use_real_mongo: Annotated[ - Optional[bool], + bool | None, Field(description="DEPRECATED: force usage of MongoDB over any other backend."), ] = None @@ -193,7 +193,7 @@ class ServerConfig(BaseSettings): ] = SupportedBackend.MONGOMOCK elastic_hosts: Annotated[ - Optional[Union[str, list[str], dict[str, Any], list[dict[str, Any]]]], + str | list[str] | dict[str, Any] | list[dict[str, Any]] | None, Field( description="Host settings to pass through to the `Elasticsearch` class." ), @@ -249,7 +249,7 @@ class ServerConfig(BaseSettings): ), ] = "test_server" root_path: Annotated[ - Optional[str], + str | None, Field( description=( "Sets the FastAPI app `root_path` parameter. This can be used to serve the" @@ -261,7 +261,7 @@ class ServerConfig(BaseSettings): ), ] = None base_url: Annotated[ - Optional[str], Field(description="Base URL for this implementation") + str | None, Field(description="Base URL for this implementation") ] = None implementation: Annotated[ Implementation, @@ -279,7 +279,7 @@ class ServerConfig(BaseSettings): homepage="https://optimade.org/optimade-python-tools", ) index_base_url: Annotated[ - Optional[AnyHttpUrl], + AnyHttpUrl | None, Field( description=( "An optional link to the base URL for the index meta-database of the " @@ -305,7 +305,7 @@ class ServerConfig(BaseSettings): provider_fields: Annotated[ dict[ Literal["links", "references", "structures"], - list[Union[str, dict[Literal["name", "type", "unit", "description"], str]]], + list[str | dict[Literal["name", "type", "unit", "description"], str]], ], Field( description=( @@ -347,7 +347,7 @@ class ServerConfig(BaseSettings): ] = Path(__file__).parent.joinpath("index_links.json") is_index: Annotated[ - Optional[bool], + bool | None, Field( description=( "A runtime setting to dynamically switch between index meta-database and " @@ -359,7 +359,7 @@ class ServerConfig(BaseSettings): ] = False schema_url: Annotated[ - Optional[Union[str, AnyHttpUrl]], + str | AnyHttpUrl | None, Field( description=( "A URL that will be provided in the `meta->schema` field for every response" @@ -368,7 +368,7 @@ class ServerConfig(BaseSettings): ] = f"https://schemas.optimade.org/openapi/v{__api_version__}/optimade.json" custom_landing_page: Annotated[ - Optional[Union[str, Path]], + str | Path | None, Field( description=( "The location of a custom landing page (Jinja template) to use for the API." @@ -377,7 +377,7 @@ class ServerConfig(BaseSettings): ] = None index_schema_url: Annotated[ - Optional[Union[str, AnyHttpUrl]], + str | AnyHttpUrl | None, Field( description=( "A URL that will be provided in the `meta->schema` field for every " @@ -396,7 +396,7 @@ class ServerConfig(BaseSettings): ), ] = Path("/var/log/optimade/") validate_query_parameters: Annotated[ - Optional[bool], + bool | None, Field( description=( "If True, the server will check whether the query parameters given in the " @@ -406,7 +406,7 @@ class ServerConfig(BaseSettings): ] = True validate_api_response: Annotated[ - Optional[bool], + bool | None, Field( description=( "If False, data from the database will not undergo validation before being" @@ -417,7 +417,7 @@ class ServerConfig(BaseSettings): @field_validator("insert_from_jsonl", mode="before") @classmethod - def check_jsonl_path(cls, value: Any) -> Optional[Path]: + def check_jsonl_path(cls, value: Any) -> Path | None: """Check that the path to the JSONL file is valid.""" if value in ("null", ""): return None diff --git a/optimade/server/entry_collections/entry_collections.py b/optimade/server/entry_collections/entry_collections.py index 342fa03a3..308842cef 100644 --- a/optimade/server/entry_collections/entry_collections.py +++ b/optimade/server/entry_collections/entry_collections.py @@ -3,7 +3,7 @@ import warnings from abc import ABC, abstractmethod from collections.abc import Iterable -from typing import Any, Optional, Union +from typing import Any from lark import Transformer @@ -128,7 +128,7 @@ def insert(self, data: list[EntryResource]) -> None: """ @abstractmethod - def count(self, **kwargs: Any) -> Optional[int]: + def count(self, **kwargs: Any) -> int | None: """Returns the number of entries matching the query specified by the keyword arguments. @@ -138,10 +138,10 @@ def count(self, **kwargs: Any) -> Optional[int]: """ def find( - self, params: Union[EntryListingQueryParams, SingleEntryQueryParams] + self, params: EntryListingQueryParams | SingleEntryQueryParams ) -> tuple[ - Optional[Union[dict[str, Any], list[dict[str, Any]]]], - Optional[int], + dict[str, Any] | list[dict[str, Any]] | None, + int | None, bool, set[str], set[str], @@ -203,7 +203,7 @@ def find( detail=f"Unrecognised OPTIMADE field(s) in requested `response_fields`: {bad_optimade_fields}." ) - results: Optional[Union[list[dict[str, Any]], dict[str, Any]]] = None + results: list[dict[str, Any]] | dict[str, Any] | None = None if raw_results: results = [self.resource_mapper.map_back(doc) for doc in raw_results] @@ -233,7 +233,7 @@ def find( @abstractmethod def _run_db_query( self, criteria: dict[str, Any], single_entry: bool = False - ) -> tuple[list[dict[str, Any]], Optional[int], bool]: + ) -> tuple[list[dict[str, Any]], int | None, bool]: """Run the query on the backend and collect the results. Arguments: @@ -301,7 +301,7 @@ def get_attribute_fields(self) -> set[str]: return set(annotation.model_fields) # type: ignore[attr-defined] def handle_query_params( - self, params: Union[EntryListingQueryParams, SingleEntryQueryParams] + self, params: EntryListingQueryParams | SingleEntryQueryParams ) -> dict[str, Any]: """Parse and interpret the backend-agnostic query parameter models into a dictionary that can be used by the specific backend. @@ -468,7 +468,7 @@ def parse_sort_params(self, sort_params: str) -> Iterable[tuple[str, int]]: def get_next_query_params( self, params: EntryListingQueryParams, - results: Optional[Union[dict[str, Any], list[dict[str, Any]]]], + results: dict[str, Any] | list[dict[str, Any]] | None, ) -> dict[str, list[str]]: """Provides url query pagination parameters that will be used in the next link. diff --git a/optimade/server/entry_collections/mongo.py b/optimade/server/entry_collections/mongo.py index 3036aa729..77d2c694a 100644 --- a/optimade/server/entry_collections/mongo.py +++ b/optimade/server/entry_collections/mongo.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any from optimade.filtertransformers.mongo import MongoTransformer from optimade.models import EntryResource @@ -68,7 +68,7 @@ def __len__(self) -> int: """Returns the total number of entries in the collection.""" return self.collection.estimated_document_count() - def count(self, **kwargs: Any) -> Union[int, None]: + def count(self, **kwargs: Any) -> int | None: """Returns the number of entries matching the query specified by the keyword arguments, or `None` if the count timed out. @@ -104,7 +104,7 @@ def insert(self, data: list[EntryResource]) -> None: self.collection.insert_many(data) def handle_query_params( - self, params: Union[EntryListingQueryParams, SingleEntryQueryParams] + self, params: EntryListingQueryParams | SingleEntryQueryParams ) -> dict[str, Any]: """Parse and interpret the backend-agnostic query parameter models into a dictionary that can be used by MongoDB. @@ -143,7 +143,7 @@ def handle_query_params( def _run_db_query( self, criteria: dict[str, Any], single_entry: bool = False - ) -> tuple[list[dict[str, Any]], Optional[int], bool]: + ) -> tuple[list[dict[str, Any]], int | None, bool]: """Run the query on the backend and collect the results. Arguments: diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py index c79eaa637..68c6d29a4 100644 --- a/optimade/server/exception_handlers.py +++ b/optimade/server/exception_handlers.py @@ -1,6 +1,5 @@ import traceback -from collections.abc import Iterable -from typing import Callable, Optional, Union +from collections.abc import Callable, Iterable from fastapi import Request from fastapi.encoders import jsonable_encoder @@ -19,7 +18,7 @@ def general_exception( request: Request, exc: Exception, status_code: int = 500, # A status_code in `exc` will take precedence - errors: Optional[list[OptimadeError]] = None, + errors: list[OptimadeError] | None = None, ) -> JSONAPIResponse: """Handle an exception @@ -80,7 +79,7 @@ def general_exception( def http_exception_handler( request: Request, - exc: Union[StarletteHTTPException, OptimadeHTTPException], + exc: StarletteHTTPException | OptimadeHTTPException, ) -> JSONAPIResponse: """Handle a general HTTP Exception from Starlette diff --git a/optimade/server/main.py b/optimade/server/main.py index 73b4a0140..f563f3655 100644 --- a/optimade/server/main.py +++ b/optimade/server/main.py @@ -10,7 +10,6 @@ import warnings from contextlib import asynccontextmanager from pathlib import Path -from typing import Optional from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -82,7 +81,7 @@ async def lifespan(app: FastAPI): if CONFIG.insert_test_data or CONFIG.insert_from_jsonl: from optimade.utils import insert_from_jsonl - def _insert_test_data(endpoint: Optional[str] = None): + def _insert_test_data(endpoint: str | None = None): import bson.json_util from bson.objectid import ObjectId diff --git a/optimade/server/mappers/entries.py b/optimade/server/mappers/entries.py index 386172b56..8f499f9b5 100644 --- a/optimade/server/mappers/entries.py +++ b/optimade/server/mappers/entries.py @@ -1,7 +1,7 @@ import warnings from collections.abc import Iterable from functools import lru_cache -from typing import Any, Optional, Union +from typing import Any from optimade.models.entries import EntryResource @@ -29,7 +29,7 @@ def __init__(self, func): self.__doc__ = func.__doc__ self.__wrapped__ = func - def __get__(self, _: Any, owner: Optional[type] = None) -> Any: + def __get__(self, _: Any, owner: type | None = None) -> Any: return self.__wrapped__(owner) @@ -188,7 +188,7 @@ def all_length_aliases(cls) -> tuple[tuple[str, str], ...]: @classmethod @lru_cache(maxsize=128) - def length_alias_for(cls, field: str) -> Optional[str]: + def length_alias_for(cls, field: str) -> str | None: """Returns the length alias for the particular field, or `None` if no such alias is found. @@ -366,8 +366,8 @@ def map_back(cls, doc: dict) -> dict: @classmethod def deserialize( - cls, results: Union[dict, Iterable[dict]] - ) -> Union[list[EntryResource], EntryResource]: + cls, results: dict | Iterable[dict] + ) -> list[EntryResource] | EntryResource: """Converts the raw database entries for this class into serialized models, mapping the data along the way. diff --git a/optimade/server/middleware.py b/optimade/server/middleware.py index dab1e8390..75b4fae60 100644 --- a/optimade/server/middleware.py +++ b/optimade/server/middleware.py @@ -11,7 +11,7 @@ import urllib.parse import warnings from collections.abc import Generator, Iterable -from typing import Optional, TextIO, Union +from typing import TextIO from starlette.datastructures import URL as StarletteURL from starlette.middleware.base import BaseHTTPMiddleware @@ -113,7 +113,7 @@ class HandleApiHint(BaseHTTPMiddleware): """Handle `api_hint` query parameter.""" @staticmethod - def handle_api_hint(api_hint: list[str]) -> Union[None, str]: + def handle_api_hint(api_hint: list[str]) -> None | str: """Handle `api_hint` parameter value. There are several scenarios that can play out, when handling the `api_hint` @@ -314,12 +314,12 @@ class AddWarnings(BaseHTTPMiddleware): def showwarning( self, - message: Union[Warning, str], + message: Warning | str, category: type[Warning], filename: str, lineno: int, - file: Optional[TextIO] = None, - line: Optional[str] = None, + file: TextIO | None = None, + line: str | None = None, ) -> None: """ Hook to write a warning to a file using the built-in `warnings` lib. @@ -411,7 +411,7 @@ def showwarning( ) @staticmethod - def chunk_it_up(content: Union[str, bytes], chunk_size: int) -> Generator: + def chunk_it_up(content: str | bytes, chunk_size: int) -> Generator: """Return generator for string in chunks of size `chunk_size`. Parameters: diff --git a/optimade/server/query_params.py b/optimade/server/query_params.py index 7ce409246..e37bcd8c4 100644 --- a/optimade/server/query_params.py +++ b/optimade/server/query_params.py @@ -1,6 +1,6 @@ from abc import ABC from collections.abc import Iterable -from typing import Annotated, Optional +from typing import Annotated from warnings import warn from fastapi import Query @@ -195,7 +195,7 @@ def __init__( ), ] = "json", email_address: Annotated[ - Optional[EmailStr], + EmailStr | None, Query( description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`", ), @@ -336,7 +336,7 @@ def __init__( ), ] = "json", email_address: Annotated[ - Optional[EmailStr], + EmailStr | None, Query( description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`", ), diff --git a/optimade/server/routers/utils.py b/optimade/server/routers/utils.py index 760e569f1..1d0a32446 100644 --- a/optimade/server/routers/utils.py +++ b/optimade/server/routers/utils.py @@ -1,7 +1,7 @@ import re import urllib.parse from datetime import datetime -from typing import Any, Optional, Union +from typing import Any from fastapi import Request from fastapi.responses import JSONResponse @@ -47,11 +47,11 @@ class JSONAPIResponse(JSONResponse): def meta_values( - url: Union[urllib.parse.ParseResult, urllib.parse.SplitResult, StarletteURL, str], - data_returned: Optional[int], + url: urllib.parse.ParseResult | urllib.parse.SplitResult | StarletteURL | str, + data_returned: int | None, data_available: int, more_data_available: bool, - schema: Optional[str] = None, + schema: str | None = None, **kwargs, ) -> ResponseMeta: """Helper to initialize the meta values""" @@ -84,7 +84,7 @@ def meta_values( def handle_response_fields( - results: Union[list[EntryResource], EntryResource, list[dict], dict], + results: list[EntryResource] | EntryResource | list[dict] | dict, exclude_fields: set[str], include_fields: set[str], ) -> list[dict[str, Any]]: @@ -130,10 +130,10 @@ def handle_response_fields( def get_included_relationships( - results: Union[EntryResource, list[EntryResource], dict, list[dict]], + results: EntryResource | list[EntryResource] | dict | list[dict], ENTRY_COLLECTIONS: dict[str, EntryCollection], include_param: list[str], -) -> list[Union[EntryResource, dict[str, Any]]]: +) -> list[EntryResource | dict[str, Any]]: """Filters the included relationships and makes the appropriate compound request to include them in the response. @@ -192,7 +192,7 @@ def get_included_relationships( included: dict[ str, - Union[list[EntryResource], list[dict[str, Any]]], + list[EntryResource] | list[dict[str, Any]], ] = {} for entry_type in endpoint_includes: compound_filter = " OR ".join( @@ -218,9 +218,9 @@ def get_included_relationships( def get_base_url( - parsed_url_request: Union[ - urllib.parse.ParseResult, urllib.parse.SplitResult, StarletteURL, str - ], + parsed_url_request: ( + urllib.parse.ParseResult | urllib.parse.SplitResult | StarletteURL | str + ), ) -> str: """Get base URL for current server diff --git a/optimade/server/schemas.py b/optimade/server/schemas.py index a5cede3f0..93d2d0248 100644 --- a/optimade/server/schemas.py +++ b/optimade/server/schemas.py @@ -42,7 +42,7 @@ """ from optimade.exceptions import POSSIBLE_ERRORS - ERROR_RESPONSES: Optional[dict[int, dict[str, Any]]] = { + ERROR_RESPONSES: dict[int, dict[str, Any]] | None = { err.status_code: {"model": ErrorResponse, "description": err.title} for err in POSSIBLE_ERRORS } @@ -52,8 +52,8 @@ def retrieve_queryable_properties( schema: type[EntryResource], - queryable_properties: Optional[Iterable[str]] = None, - entry_type: Optional[str] = None, + queryable_properties: Iterable[str] | None = None, + entry_type: str | None = None, ) -> "QueryableProperties": """Recursively loops through a pydantic model, returning a dictionary of all the OPTIMADE-queryable properties of that model. diff --git a/optimade/utils.py b/optimade/utils.py index e315ae485..45829726c 100644 --- a/optimade/utils.py +++ b/optimade/utils.py @@ -176,7 +176,7 @@ def get_providers(add_mongo_id: bool = False) -> list: def get_child_database_links( provider: LinksResource, obey_aggregate: bool = True, - headers: Optional[dict] = None, + headers: dict | None = None, skip_ssl: bool = False, ) -> list[LinksResource]: """For a provider, return a list of available child databases. @@ -243,9 +243,9 @@ def get_child_database_links( def get_all_databases( - include_providers: Optional[Container[str]] = None, - exclude_providers: Optional[Container[str]] = None, - exclude_databases: Optional[Container[str]] = None, + include_providers: Container[str] | None = None, + exclude_providers: Container[str] | None = None, + exclude_databases: Container[str] | None = None, progress: "Optional[rich.Progress]" = None, skip_ssl: bool = False, ) -> Iterable[str]: diff --git a/optimade/validator/utils.py b/optimade/validator/utils.py index 265ee0c85..fde882adb 100644 --- a/optimade/validator/utils.py +++ b/optimade/validator/utils.py @@ -18,7 +18,8 @@ import time import traceback as tb import urllib.parse -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any import requests from pydantic import Field, ValidationError @@ -89,7 +90,7 @@ class ValidatorResults: ) verbosity: int = 0 - def add_success(self, summary: str, success_type: Optional[str] = None): + def add_success(self, summary: str, success_type: str | None = None): """Register a validation success to the results class. Parameters: @@ -117,9 +118,7 @@ def add_success(self, summary: str, success_type: Optional[str] = None): elif self.verbosity == 0: pretty_print(".", end="", flush=True) # type: ignore[operator] - def add_failure( - self, summary: str, message: str, failure_type: Optional[str] = None - ): + def add_failure(self, summary: str, message: str, failure_type: str | None = None): """Register a validation failure to the results class with corresponding summary, message and type. @@ -168,9 +167,9 @@ def __init__( self, base_url: str, max_retries: int = 5, - headers: Optional[dict[str, str]] = None, - timeout: Optional[float] = DEFAULT_CONN_TIMEOUT, - read_timeout: Optional[float] = DEFAULT_READ_TIMEOUT, + headers: dict[str, str] | None = None, + timeout: float | None = DEFAULT_CONN_TIMEOUT, + read_timeout: float | None = DEFAULT_READ_TIMEOUT, ) -> None: """Initialises the Client with the given `base_url` without testing if it is valid. @@ -193,8 +192,8 @@ def __init__( """ self.base_url: str = base_url - self.last_request: Optional[str] = None - self.response: Optional[requests.Response] = None + self.last_request: str | None = None + self.response: requests.Response | None = None self.max_retries = max_retries self.headers = headers or {} if "User-Agent" not in self.headers: @@ -290,7 +289,7 @@ def test_case(test_fn: Callable[..., tuple[Any, str]]): def wrapper( validator, *args, - request: Optional[str] = None, + request: str | None = None, optional: bool = False, multistage: bool = False, **kwargs, @@ -371,7 +370,7 @@ def wrapper( if not isinstance(result, ValidationError): message += traceback.split("\n") - failure_type: Optional[str] = None + failure_type: str | None = None if isinstance(result, InternalError): summary = f"{display_request} - {test_fn.__name__} - failed with internal error" failure_type = "internal" @@ -410,13 +409,13 @@ class ValidatorLinksResponse(Success): class ValidatorEntryResponseOne(Success): meta: ResponseMeta = Field(...) data: EntryResource = Field(...) - included: Optional[list[dict[str, Any]]] = Field(None) # type: ignore[assignment] + included: list[dict[str, Any]] | None = Field(None) # type: ignore[assignment] class ValidatorEntryResponseMany(Success): meta: ResponseMeta = Field(...) data: list[EntryResource] = Field(...) - included: Optional[list[dict[str, Any]]] = Field(None) # type: ignore[assignment] + included: list[dict[str, Any]] | None = Field(None) # type: ignore[assignment] class ValidatorReferenceResponseOne(ValidatorEntryResponseOne): diff --git a/optimade/validator/validator.py b/optimade/validator/validator.py index c0526d3e0..5b79936b8 100644 --- a/optimade/validator/validator.py +++ b/optimade/validator/validator.py @@ -12,7 +12,7 @@ class that can be pointed at an OPTIMADE implementation and validated import re import sys import urllib.parse -from typing import Any, Literal, Optional, Union +from typing import Any, Literal import requests @@ -56,22 +56,22 @@ class ImplementationValidator: """ - valid: Optional[bool] + valid: bool | None def __init__( self, - client: Optional[Any] = None, - base_url: Optional[str] = None, + client: Any | None = None, + base_url: str | None = None, verbosity: int = 0, respond_json: bool = False, page_limit: int = 4, max_retries: int = 5, run_optional_tests: bool = True, fail_fast: bool = False, - as_type: Optional[str] = None, + as_type: str | None = None, index: bool = False, minimal: bool = False, - http_headers: Optional[dict[str, str]] = None, + http_headers: dict[str, str] | None = None, timeout: float = DEFAULT_CONN_TIMEOUT, read_timeout: float = DEFAULT_READ_TIMEOUT, ): @@ -352,7 +352,7 @@ def validate_implementation(self): self.print_summary() @test_case - def _recurse_through_endpoint(self, endp: str) -> tuple[Optional[bool], str]: + def _recurse_through_endpoint(self, endp: str) -> tuple[bool | None, str]: """For a given endpoint (`endp`), get the entry type and supported fields, testing that all mandatory fields are supported, then test queries on every property according @@ -503,7 +503,7 @@ def _test_must_properties( @test_case def _get_archetypal_entry( self, endp: str, properties: list[str] - ) -> tuple[Optional[dict[str, Any]], str]: + ) -> tuple[dict[str, Any] | None, str]: """Get a random entry from the first page of results for this endpoint. @@ -544,7 +544,7 @@ def _get_archetypal_entry( @test_case def _check_response_fields( self, endp: str, fields: list[str] - ) -> tuple[Optional[bool], str]: + ) -> tuple[bool | None, str]: """Check that the response field query parameter is obeyed. Parameters: @@ -593,7 +593,7 @@ def _construct_queries_for_property( sortable: bool, endp: str, chosen_entry: dict[str, Any], - ) -> tuple[Optional[bool], str]: + ) -> tuple[bool | None, str]: """For the given property, property type and chose entry, this method runs a series of queries for each field in the entry, testing that the initial document is returned where expected. @@ -705,7 +705,7 @@ def _construct_single_property_filters( endp: str, chosen_entry: dict[str, Any], query_optional: bool, - ) -> tuple[Optional[bool], str]: + ) -> tuple[bool | None, str]: """This method constructs appropriate queries using all operators for a certain field and applies some tests: @@ -952,9 +952,7 @@ def _construct_single_property_filters( return True, f"{prop} passed filter tests" - def _test_info_or_links_endpoint( - self, request_str: str - ) -> Union[Literal[False], dict]: + def _test_info_or_links_endpoint(self, request_str: str) -> Literal[False] | dict: """Requests an info or links endpoint and attempts to deserialize the response. @@ -1062,7 +1060,7 @@ def _test_multi_entry_endpoint(self, endp: str) -> None: @test_case def _test_data_available_matches_data_returned( self, deserialized: Any - ) -> tuple[Optional[bool], str]: + ) -> tuple[bool | None, str]: """In the case where no query is requested, `data_available` must equal `data_returned` in the meta response, which is tested here. @@ -1189,7 +1187,7 @@ def _test_versions_endpoint_content( def _test_versions_headers( self, content_type: dict[str, Any], - expected_parameter: Union[str, list[str]], + expected_parameter: str | list[str], ) -> tuple[dict[str, Any], str]: """Tests that the `Content-Type` field of the `/versions` header contains the passed parameter. @@ -1295,8 +1293,8 @@ def _test_page_limit( self, response: requests.models.Response, check_next_link: int = 5, - previous_links: Optional[set[str]] = None, - ) -> tuple[Optional[bool], str]: + previous_links: set[str] | None = None, + ) -> tuple[bool | None, str]: """Test that a multi-entry endpoint obeys the page limit by following pagination links up to a depth of `check_next_link`. @@ -1411,7 +1409,7 @@ def _deserialize_response( self, response: requests.models.Response, response_cls: Any, - request: Optional[str] = None, + request: str | None = None, ) -> tuple[Any, str]: """Try to create the appropriate pydantic model from the response. @@ -1446,8 +1444,8 @@ def _deserialize_response( @test_case def _get_available_endpoints( - self, base_info: Union[Any, dict[str, Any]] - ) -> tuple[Optional[list[str]], str]: + self, base_info: Any | dict[str, Any] + ) -> tuple[list[str] | None, str]: """Tries to get `entry_types_by_format` from base info response even if it could not be deserialized. @@ -1503,8 +1501,8 @@ def _get_available_endpoints( @test_case def _get_endpoint( - self, request_str: str, expected_status_code: Union[list[int], int] = 200 - ) -> tuple[Optional[requests.Response], str]: + self, request_str: str, expected_status_code: list[int] | int = 200 + ) -> tuple[requests.Response | None, str]: """Gets the response from the endpoint specified by `request_str`. function is wrapped by the `test_case` decorator diff --git a/optimade/warnings.py b/optimade/warnings.py index c715936d5..303d255bf 100644 --- a/optimade/warnings.py +++ b/optimade/warnings.py @@ -3,8 +3,6 @@ """ -from typing import Optional - __all__ = ( "OptimadeWarning", "FieldValueNotRecognized", @@ -21,7 +19,7 @@ class OptimadeWarning(Warning): """Base Warning for the `optimade` package""" def __init__( - self, detail: Optional[str] = None, title: Optional[str] = None, *args + self, detail: str | None = None, title: str | None = None, *args ) -> None: detail = detail if detail else self.__doc__ super().__init__(detail, *args) diff --git a/tasks.py b/tasks.py index 1455d8bea..ae094bdbe 100644 --- a/tasks.py +++ b/tasks.py @@ -3,7 +3,7 @@ import re import sys from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from invoke import task from jsondiff import diff @@ -16,7 +16,7 @@ def update_file( - filename: Union[Path, str], sub_line: tuple[str, str], strip: Optional[str] = None + filename: Path | str, sub_line: tuple[str, str], strip: str | None = None ): """Utility function for tasks to read, update, and write files""" with open(filename) as handle: diff --git a/tests/filtertransformers/test_base.py b/tests/filtertransformers/test_base.py index 57ef9129e..77a8ad7a8 100644 --- a/tests/filtertransformers/test_base.py +++ b/tests/filtertransformers/test_base.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Callable + from collections.abc import Callable from optimade.server.mappers import BaseResourceMapper diff --git a/tests/models/test_utils.py b/tests/models/test_utils.py index df8f09dcc..ff13cf901 100644 --- a/tests/models/test_utils.py +++ b/tests/models/test_utils.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable import pytest from pydantic import BaseModel, Field, ValidationError diff --git a/tests/server/conftest.py b/tests/server/conftest.py index f3d2b0138..09268833e 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -1,11 +1,11 @@ -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union import pytest from optimade.warnings import OptimadeWarning if TYPE_CHECKING: - from typing import Callable + from collections.abc import Callable from requests import Response @@ -73,10 +73,10 @@ def get_good_response( def inner( request: str, - server: Union[str, OptimadeTestClient] = "regular", + server: str | OptimadeTestClient = "regular", return_json: bool = True, **kwargs, - ) -> Union[dict, Response]: + ) -> dict | Response: if isinstance(server, str): if server == "regular": used_client = client @@ -142,12 +142,12 @@ def check_response(get_good_response): def inner( request: str, - expected_ids: Union[str, list[str]], + expected_ids: str | list[str], page_limit: int = CONFIG.page_limit, - expected_return: Optional[int] = None, + expected_return: int | None = None, expected_as_is: bool = False, - expected_warnings: Optional[list[dict[str, str]]] = None, - server: Union[str, OptimadeTestClient] = "regular", + expected_warnings: list[dict[str, str]] | None = None, + server: str | OptimadeTestClient = "regular", ): if expected_warnings: with pytest.warns(OptimadeWarning): @@ -193,10 +193,10 @@ def check_error_response(client, index_client): def inner( request: str, - expected_status: Optional[int] = None, - expected_title: Optional[str] = None, - expected_detail: Optional[str] = None, - server: Union[str, OptimadeTestClient] = "regular", + expected_status: int | None = None, + expected_title: str | None = None, + expected_detail: str | None = None, + server: str | OptimadeTestClient = "regular", ): response = None if isinstance(server, str): diff --git a/tests/server/query_params/conftest.py b/tests/server/query_params/conftest.py index b7f0e59f0..0d8a2c2a0 100644 --- a/tests/server/query_params/conftest.py +++ b/tests/server/query_params/conftest.py @@ -12,13 +12,12 @@ def structures(): @pytest.fixture def check_include_response(get_good_response): """Fixture to check "good" `include` response""" - from typing import Optional, Union def inner( request: str, - expected_included_types: Union[list, set], - expected_included_resources: Union[list, set], - expected_relationship_types: Optional[Union[list, set]] = None, + expected_included_types: list | set, + expected_included_resources: list | set, + expected_relationship_types: list | set | None = None, server: str = "regular", ): response = get_good_response(request, server) diff --git a/tests/server/routers/test_utils.py b/tests/server/routers/test_utils.py index b2216e837..8d4f76cfd 100644 --- a/tests/server/routers/test_utils.py +++ b/tests/server/routers/test_utils.py @@ -1,7 +1,6 @@ """Tests specifically for optimade.servers.routers.utils.""" from collections.abc import Mapping -from typing import Optional, Union from unittest import mock import pytest @@ -9,8 +8,8 @@ def mocked_providers_list_response( - url: Union[str, bytes] = "", - param: Optional[Union[Mapping[str, str], tuple[str, str]]] = None, + url: str | bytes = "", + param: Mapping[str, str] | tuple[str, str] | None = None, **kwargs, ): """This function will be used to mock requests.get @@ -29,11 +28,11 @@ def mocked_providers_list_response( ) class MockResponse: - def __init__(self, data: Union[list, dict], status_code: int): + def __init__(self, data: list | dict, status_code: int): self.data = data self.status_code = status_code - def json(self) -> Union[list, dict]: + def json(self) -> list | dict: return self.data def content(self) -> str: diff --git a/tests/server/test_client.py b/tests/server/test_client.py index 2255fbfda..687026373 100644 --- a/tests/server/test_client.py +++ b/tests/server/test_client.py @@ -4,7 +4,6 @@ import warnings from functools import partial from pathlib import Path -from typing import Optional import httpx import pytest @@ -416,7 +415,7 @@ def global_database_callback(_: str, results: dict): @pytest.mark.parametrize("use_async", [True, False]) def test_client_page_skip_callback(async_http_client, http_client, use_async): - def page_skip_callback(_: str, results: dict) -> Optional[dict]: + def page_skip_callback(_: str, results: dict) -> dict | None: """A test callback that skips to the final page of results.""" if len(results["data"]) > 16: return {"next": f"{TEST_URL}/structures?page_offset=16"} @@ -440,7 +439,7 @@ def test_client_mutable_data_callback(async_http_client, http_client, use_async) container: dict[str, str] = {} def mutable_database_callback( - _: str, results: dict, db: Optional[dict[str, str]] = None + _: str, results: dict, db: dict[str, str] | None = None ) -> None: """A test callback that creates a flat dictionary of results via mutable args.""" From 84342b3b8b37e1f3aabb0ab6ef07ab2c1333a77e Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Fri, 11 Oct 2024 17:05:20 +0100 Subject: [PATCH 3/4] Limit pytest durations output --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a99dcaafb..e24e0f739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,25 +177,25 @@ jobs: pip install -e . - name: Run non-server tests - run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml tests/ --ignore tests/server + run: pytest -rs -vvv --durations=10 --cov=./optimade/ --cov-report=xml tests/ --ignore tests/server - name: Install latest server dependencies run: pip install -r requirements-server.txt - name: Run server tests (using `mongomock`) - run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers + run: pytest -rs -vvv --durations=10 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers env: OPTIMADE_DATABASE_BACKEND: 'mongomock' - name: Run server tests with no API validation (using `mongomock`) run: - pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers + pytest -rs -vvv --durations=10 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers env: OPTIMADE_DATABASE_BACKEND: 'mongomock' OPTIMADE_VALIDATE_API_RESPONSE: false - name: Run server tests (using a real MongoDB) - run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers + run: pytest -rs -vvv --durations=10 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers env: OPTIMADE_DATABASE_BACKEND: 'mongodb' From e814cd26305f8cd7e1f604e5b2462e1dc1328d55 Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Mon, 14 Oct 2024 11:53:48 +0100 Subject: [PATCH 4/4] Extend annotation -> type map to include py310 native union types --- optimade/models/types.py | 7 ++++--- tests/models/test_types.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 tests/models/test_types.py diff --git a/optimade/models/types.py b/optimade/models/types.py index b230a0fde..8ed7f99de 100644 --- a/optimade/models/types.py +++ b/optimade/models/types.py @@ -1,3 +1,4 @@ +from types import UnionType from typing import Annotated, Optional, Union, get_args from pydantic import Field @@ -23,7 +24,7 @@ AnnotatedType = type(ChemicalSymbol) OptionalType = type(Optional[str]) -UnionType = type(Union[str, int]) +_UnionType = type(Union[str, int]) NoneType = type(None) @@ -39,7 +40,7 @@ def _get_origin_type(annotation: type) -> type: """ # If the annotation is a Union, get the first non-None type (this includes # Optional[T]) - if isinstance(annotation, (OptionalType, UnionType)): + if isinstance(annotation, (OptionalType, UnionType, _UnionType)): for arg in get_args(annotation): if arg not in (None, NoneType): annotation = arg @@ -50,7 +51,7 @@ def _get_origin_type(annotation: type) -> type: annotation = get_args(annotation)[0] # Recursively unpack annotation, if it is a Union, Optional, or Annotated type - while isinstance(annotation, (OptionalType, UnionType, AnnotatedType)): + while isinstance(annotation, (OptionalType, UnionType, _UnionType, AnnotatedType)): annotation = _get_origin_type(annotation) # Special case for Literal diff --git a/tests/models/test_types.py b/tests/models/test_types.py new file mode 100644 index 000000000..5dbccb996 --- /dev/null +++ b/tests/models/test_types.py @@ -0,0 +1,13 @@ +from typing import Annotated, Optional + + +def test_origin_type(): + from optimade.models.types import _get_origin_type + + assert _get_origin_type(int | None) is int + assert _get_origin_type(str | None) is str + assert _get_origin_type(Optional[int]) is int + assert _get_origin_type(Optional[str]) is str + assert _get_origin_type(Annotated[int, "test"]) is int + assert _get_origin_type(Annotated[str, "test"]) is str + assert _get_origin_type(int | str | None) is int