From 584cb5ed8f6b5703d2ab26a9bdb960728a41132a Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Sun, 1 Dec 2024 11:24:16 +0100 Subject: [PATCH] Improve typing (#133) - add `from __future__ import annotations` everywhere - install all dependencies explicitly --- pyproject.toml | 2 + src/curies/api.py | 157 ++++++++++---------- src/curies/cli.py | 34 +++-- src/curies/discovery.py | 26 ++-- src/curies/mapping_service/api.py | 40 ++--- src/curies/mapping_service/rdflib_custom.py | 4 +- src/curies/mapping_service/utils.py | 6 +- src/curies/reconciliation.py | 7 +- src/curies/resolver_service.py | 30 ++-- src/curies/sources.py | 6 +- tox.ini | 3 +- 11 files changed, 167 insertions(+), 148 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5270746..20db60b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,8 @@ requires-python = ">=3.9" dependencies = [ "pytrie", "pydantic>=2.0", + "eval_type_backport; python_version < '3.10'", + "typing-extensions", ] [project.optional-dependencies] diff --git a/src/curies/api.py b/src/curies/api.py index 0cb005a..b11e62e 100644 --- a/src/curies/api.py +++ b/src/curies/api.py @@ -1,5 +1,7 @@ """Data structures and algorithms for :mod:`curies`.""" +from __future__ import annotations + import csv import itertools as itt import json @@ -15,7 +17,6 @@ Callable, Literal, NamedTuple, - Optional, TypeVar, Union, cast, @@ -133,7 +134,7 @@ def curie(self) -> str: return f"{self.prefix}:{self.identifier}" @classmethod - def from_curie(cls, curie: str, *, sep: str = ":") -> "ReferenceTuple": + def from_curie(cls, curie: str, *, sep: str = ":") -> ReferenceTuple: """Parse a CURIE string and populate a reference tuple. :param curie: A string representation of a compact URI (CURIE) @@ -201,7 +202,7 @@ class notion of parsed reference (instead of merely stringified model_config = ConfigDict(frozen=True) - def __lt__(self, other: "Reference") -> bool: + def __lt__(self, other: Reference) -> bool: """Sort the reference lexically first by prefix, then by identifier.""" return self.pair < other.pair @@ -233,7 +234,7 @@ def pair(self) -> ReferenceTuple: return ReferenceTuple(self.prefix, self.identifier) @classmethod - def from_curie(cls, curie: str, *, sep: str = ":") -> "Reference": + def from_curie(cls, curie: str, *, sep: str = ":") -> Reference: """Parse a CURIE string and populate a reference. :param curie: A string representation of a compact URI (CURIE) @@ -257,7 +258,7 @@ class NamedReference(Reference): model_config = ConfigDict(frozen=True) @classmethod - def from_curie(cls, curie: str, name: str, *, sep: str = ":") -> "NamedReference": # type:ignore + def from_curie(cls, curie: str, name: str, *, sep: str = ":") -> NamedReference: # type:ignore """Parse a CURIE string and populate a reference. :param curie: A string representation of a compact URI (CURIE) @@ -293,7 +294,7 @@ class Record(BaseModel): ) prefix_synonyms: list[str] = Field(default_factory=list, title="CURIE prefix synonyms") uri_prefix_synonyms: list[str] = Field(default_factory=list, title="URI prefix synonyms") - pattern: Optional[str] = Field( + pattern: str | None = Field( default=None, description="The regular expression pattern for entries in this semantic space. " "Warning: this is an experimental feature.", @@ -647,8 +648,8 @@ def add_prefix( self, prefix: str, uri_prefix: str, - prefix_synonyms: Optional[Collection[str]] = None, - uri_prefix_synonyms: Optional[Collection[str]] = None, + prefix_synonyms: Collection[str] | None = None, + uri_prefix_synonyms: Collection[str] | None = None, *, case_sensitive: bool = True, merge: bool = False, @@ -700,8 +701,8 @@ def add_prefix( @classmethod def from_extended_prefix_map( - cls, records: LocationOr[Iterable[Union[Record, dict[str, Any]]]], **kwargs: Any - ) -> "Converter": + cls, records: LocationOr[Iterable[Record | dict[str, Any]]], **kwargs: Any + ) -> Converter: """Get a converter from a list of dictionaries by creating records out of them. :param records: @@ -778,7 +779,7 @@ def from_extended_prefix_map( @classmethod def from_priority_prefix_map( cls, data: LocationOr[Mapping[str, list[str]]], **kwargs: Any - ) -> "Converter": + ) -> Converter: """Get a converter from a priority prefix map. :param data: @@ -815,9 +816,7 @@ def from_priority_prefix_map( ) @classmethod - def from_prefix_map( - cls, prefix_map: LocationOr[Mapping[str, str]], **kwargs: Any - ) -> "Converter": + def from_prefix_map(cls, prefix_map: LocationOr[Mapping[str, str]], **kwargs: Any) -> Converter: """Get a converter from a simple prefix map. :param prefix_map: @@ -855,7 +854,7 @@ def from_prefix_map( @classmethod def from_reverse_prefix_map( cls, reverse_prefix_map: LocationOr[Mapping[str, str]], **kwargs: Any - ) -> "Converter": + ) -> Converter: """Get a converter from a reverse prefix map. :param reverse_prefix_map: @@ -900,7 +899,7 @@ def from_reverse_prefix_map( return cls(records, **kwargs) @classmethod - def from_jsonld(cls, data: LocationOr[dict[str, Any]], **kwargs: Any) -> "Converter": + def from_jsonld(cls, data: LocationOr[dict[str, Any]], **kwargs: Any) -> Converter: """Get a converter from a JSON-LD object, which contains a prefix map in its ``@context`` key. :param data: @@ -938,7 +937,7 @@ def from_jsonld(cls, data: LocationOr[dict[str, Any]], **kwargs: Any) -> "Conver @classmethod def from_jsonld_github( cls, owner: str, repo: str, *path: str, branch: str = "main", **kwargs: Any - ) -> "Converter": + ) -> Converter: """Construct a remote JSON-LD URL on GitHub then parse with :meth:`Converter.from_jsonld`. :param owner: A github repository owner or organization (e.g., ``biopragmatics``) @@ -971,9 +970,9 @@ def from_jsonld_github( @classmethod def from_rdflib( cls, - graph_or_manager: Union["rdflib.Graph", "rdflib.namespace.NamespaceManager"], + graph_or_manager: rdflib.Graph | rdflib.namespace.NamespaceManager, **kwargs: Any, - ) -> "Converter": + ) -> Converter: """Get a converter from an RDFLib graph or namespace manager. :param graph_or_manager: A RDFLib graph or manager object @@ -1004,10 +1003,10 @@ def from_rdflib( @classmethod def from_shacl( cls, - graph: Union[str, Path, "rdflib.Graph"], - format: Optional[str] = None, + graph: str | Path | rdflib.Graph, + format: str | None = None, **kwargs: Any, - ) -> "Converter": + ) -> Converter: """Get a converter from SHACL, either in a turtle f. :param graph: A RDFLib graph, a Path, a string representing a file path, or a string URL @@ -1019,7 +1018,7 @@ def from_shacl( import rdflib temporary_graph = rdflib.Graph() - temporary_graph.parse(location=graph, format=format) + temporary_graph.parse(location=Path(graph).resolve().as_posix(), format=format) graph = temporary_graph query = """\ @@ -1031,7 +1030,7 @@ def from_shacl( OPTIONAL { ?bnode2 sh:pattern ?pattern . } } """ - results = graph.query(query) + results = cast(Iterable[tuple[str, str, str]], graph.query(query)) records = [ Record(prefix=str(prefix), uri_prefix=str(uri_prefix), pattern=pattern and str(pattern)) for prefix, uri_prefix, pattern in results @@ -1127,11 +1126,11 @@ def compress_or_standardize( *, strict: Literal[False] = False, passthrough: Literal[False] = False, - ) -> Optional[str]: ... + ) -> str | None: ... def compress_or_standardize( self, uri_or_curie: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + ) -> str | None: """Compress a URI or standardize a CURIE. :param uri_or_curie: @@ -1200,11 +1199,9 @@ def compress( @overload def compress( self, uri: str, *, strict: Literal[False] = False, passthrough: Literal[False] = False - ) -> Optional[str]: ... + ) -> str | None: ... - def compress( - self, uri: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + def compress(self, uri: str, *, strict: bool = False, passthrough: bool = False) -> str | None: """Compress a URI to a CURIE, if possible. :param uri: @@ -1250,7 +1247,7 @@ def compress( return uri return None - def parse_uri(self, uri: str) -> Union[ReferenceTuple, tuple[None, None]]: + def parse_uri(self, uri: str) -> ReferenceTuple | tuple[None, None]: """Compress a URI to a CURIE pair. :param uri: @@ -1330,11 +1327,11 @@ def expand_or_standardize( *, strict: Literal[False] = False, passthrough: Literal[False] = False, - ) -> Optional[str]: ... + ) -> str | None: ... def expand_or_standardize( self, curie_or_uri: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + ) -> str | None: """Expand a CURIE or standardize a URI. :param curie_or_uri: @@ -1403,11 +1400,9 @@ def expand( @overload def expand( self, curie: str, *, strict: Literal[False] = False, passthrough: Literal[False] = False - ) -> Optional[str]: ... + ) -> str | None: ... - def expand( - self, curie: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + def expand(self, curie: str, *, strict: bool = False, passthrough: bool = False) -> str | None: """Expand a CURIE to a URI, if possible. :param curie: @@ -1443,7 +1438,7 @@ def expand( return curie return None - def expand_all(self, curie: str) -> Optional[Collection[str]]: + def expand_all(self, curie: str) -> Collection[str] | None: """Expand a CURIE pair to all possible URIs. :param curie: @@ -1473,7 +1468,7 @@ def parse_curie(self, curie: str) -> ReferenceTuple: """Parse a CURIE.""" return ReferenceTuple.from_curie(curie, sep=self.delimiter) - def expand_pair(self, prefix: str, identifier: str) -> Optional[str]: + def expand_pair(self, prefix: str, identifier: str) -> str | None: """Expand a CURIE pair to the standard URI. :param prefix: @@ -1500,7 +1495,7 @@ def expand_pair(self, prefix: str, identifier: str) -> Optional[str]: return None return uri_prefix + identifier - def expand_pair_all(self, prefix: str, identifier: str) -> Optional[Collection[str]]: + def expand_pair_all(self, prefix: str, identifier: str) -> Collection[str] | None: """Expand a CURIE pair to all possible URIs. :param prefix: @@ -1549,11 +1544,11 @@ def standardize_prefix( @overload def standardize_prefix( self, prefix: str, *, strict: Literal[False] = False, passthrough: Literal[False] = False - ) -> Optional[str]: ... + ) -> str | None: ... def standardize_prefix( self, prefix: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + ) -> str | None: """Standardize a prefix. :param prefix: @@ -1607,11 +1602,11 @@ def standardize_curie( @overload def standardize_curie( self, curie: str, *, strict: Literal[False] = False, passthrough: Literal[False] = False - ) -> Optional[str]: ... + ) -> str | None: ... def standardize_curie( self, curie: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + ) -> str | None: """Standardize a CURIE. :param curie: @@ -1672,11 +1667,11 @@ def standardize_uri( @overload def standardize_uri( self, uri: str, *, strict: Literal[False] = False, passthrough: Literal[False] = False - ) -> Optional[str]: ... + ) -> str | None: ... def standardize_uri( self, uri: str, *, strict: bool = False, passthrough: bool = False - ) -> Optional[str]: + ) -> str | None: """Standardize a URI. :param uri: @@ -1727,9 +1722,9 @@ def standardize_uri( def pd_compress( self, - df: "pandas.DataFrame", - column: Union[str, int], - target_column: Union[None, str, int] = None, + df: pandas.DataFrame, + column: str | int, + target_column: None | str | int = None, strict: bool = False, passthrough: bool = False, ambiguous: bool = False, @@ -1750,9 +1745,9 @@ def pd_compress( def pd_expand( self, - df: "pandas.DataFrame", - column: Union[str, int], - target_column: Union[None, str, int] = None, + df: pandas.DataFrame, + column: str | int, + target_column: None | str | int = None, strict: bool = False, passthrough: bool = False, ambiguous: bool = False, @@ -1773,10 +1768,10 @@ def pd_expand( def pd_standardize_prefix( self, - df: "pandas.DataFrame", + df: pandas.DataFrame, *, - column: Union[str, int], - target_column: Union[None, str, int] = None, + column: str | int, + target_column: None | str | int = None, strict: bool = False, passthrough: bool = False, ) -> None: @@ -1794,10 +1789,10 @@ def pd_standardize_prefix( def pd_standardize_curie( self, - df: "pandas.DataFrame", + df: pandas.DataFrame, *, - column: Union[str, int], - target_column: Union[None, str, int] = None, + column: str | int, + target_column: None | str | int = None, strict: bool = False, passthrough: bool = False, ) -> None: @@ -1829,10 +1824,10 @@ def pd_standardize_curie( def pd_standardize_uri( self, - df: "pandas.DataFrame", + df: pandas.DataFrame, *, - column: Union[str, int], - target_column: Union[None, str, int] = None, + column: str | int, + target_column: None | str | int = None, strict: bool = False, passthrough: bool = False, ) -> None: @@ -1850,10 +1845,10 @@ def pd_standardize_uri( def file_compress( self, - path: Union[str, Path], + path: str | Path, column: int, *, - sep: Optional[str] = None, + sep: str | None = None, header: bool = True, strict: bool = False, passthrough: bool = False, @@ -1876,10 +1871,10 @@ def file_compress( def file_expand( self, - path: Union[str, Path], + path: str | Path, column: int, *, - sep: Optional[str] = None, + sep: str | None = None, header: bool = True, strict: bool = False, passthrough: bool = False, @@ -1902,10 +1897,10 @@ def file_expand( @staticmethod def _file_helper( - func: Callable[[str], Optional[str]], - path: Union[str, Path], + func: Callable[[str], str | None], + path: str | Path, column: int, - sep: Optional[str] = None, + sep: str | None = None, header: bool = True, ) -> None: path = Path(path).expanduser().resolve() @@ -1923,7 +1918,7 @@ def _file_helper( writer.writerow(_header) writer.writerows(rows) - def get_record(self, prefix: str) -> Optional[Record]: + def get_record(self, prefix: str) -> Record | None: """Get the record for the prefix.""" # TODO better data structure for this for record in self.records: @@ -1931,7 +1926,7 @@ def get_record(self, prefix: str) -> Optional[Record]: return record return None - def get_subconverter(self, prefixes: Iterable[str]) -> "Converter": + def get_subconverter(self, prefixes: Iterable[str]) -> Converter: r"""Get a converter with a subset of prefixes. :param prefixes: A list of prefixes to keep from this converter. These can @@ -2093,7 +2088,7 @@ def load_prefix_map(prefix_map: LocationOr[Mapping[str, str]], **kwargs: Any) -> def load_extended_prefix_map( - records: LocationOr[Iterable[Union[Record, dict[str, Any]]]], **kwargs: Any + records: LocationOr[Iterable[Record | dict[str, Any]]], **kwargs: Any ) -> Converter: """Get a converter from a list of dictionaries by creating records out of them. @@ -2181,7 +2176,7 @@ def load_jsonld_context(data: LocationOr[dict[str, Any]], **kwargs: Any) -> Conv return Converter.from_jsonld(data, **kwargs) -def load_shacl(data: LocationOr["rdflib.Graph"], **kwargs: Any) -> Converter: +def load_shacl(data: LocationOr[rdflib.Graph], **kwargs: Any) -> Converter: """Get a converter from a JSON-LD object, which contains a prefix map in its ``@context`` key. :param data: @@ -2193,7 +2188,7 @@ def load_shacl(data: LocationOr["rdflib.Graph"], **kwargs: Any) -> Converter: return Converter.from_shacl(data, **kwargs) -def write_extended_prefix_map(converter: Converter, path: Union[str, Path]) -> None: +def write_extended_prefix_map(converter: Converter, path: str | Path) -> None: """Write an extended prefix map as JSON to a file.""" path = _ensure_path(path) path.write_text( @@ -2206,9 +2201,9 @@ def write_extended_prefix_map(converter: Converter, path: Union[str, Path]) -> N ) -def _record_to_dict(record: Record) -> Mapping[str, Union[str, list[str]]]: +def _record_to_dict(record: Record) -> Mapping[str, str | list[str]]: """Convert a record to a dict.""" - rv: dict[str, Union[str, list[str]]] = { + rv: dict[str, str | list[str]] = { "prefix": record.prefix, "uri_prefix": record.uri_prefix, } @@ -2221,7 +2216,7 @@ def _record_to_dict(record: Record) -> Mapping[str, Union[str, list[str]]]: return rv -def _ensure_path(path: Union[str, Path]) -> Path: +def _ensure_path(path: str | Path) -> Path: if isinstance(path, str): path = Path(path).resolve() return path @@ -2243,7 +2238,7 @@ def _get_jsonld_context( def write_jsonld_context( converter: Converter, - path: Union[str, Path], + path: str | Path, *, include_synonyms: bool = False, expand: bool = False, @@ -2312,7 +2307,7 @@ def write_jsonld_context( json.dump(obj, file, indent=4, sort_keys=True) -def _get_expanded_term(record: Record, *, expand: bool) -> Union[str, dict[str, Any]]: +def _get_expanded_term(record: Record, *, expand: bool) -> str | dict[str, Any]: if not expand: return record.uri_prefix @@ -2329,7 +2324,7 @@ def _get_expanded_term(record: Record, *, expand: bool) -> Union[str, dict[str, def write_shacl( converter: Converter, - path: Union[str, Path], + path: str | Path, *, include_synonyms: bool = False, ) -> None: @@ -2387,7 +2382,7 @@ def write_shacl( def write_tsv( - converter: Converter, path: Union[str, Path], *, header: tuple[str, str] = ("prefix", "base") + converter: Converter, path: str | Path, *, header: tuple[str, str] = ("prefix", "base") ) -> None: """Write a simple prefix map CSV file. @@ -2424,7 +2419,7 @@ def write_tsv( writer.writerow((record.prefix, record.uri_prefix)) -def _get_shacl_line(prefix: str, uri_prefix: str, pattern: Optional[str] = None) -> str: +def _get_shacl_line(prefix: str, uri_prefix: str, pattern: str | None = None) -> str: line = f' [ sh:prefix "{prefix}" ; sh:namespace "{uri_prefix}"^^xsd:anyURI ' if pattern: pattern = pattern.replace("\\", "\\\\") diff --git a/src/curies/cli.py b/src/curies/cli.py index 9302a06..5772750 100644 --- a/src/curies/cli.py +++ b/src/curies/cli.py @@ -1,5 +1,3 @@ -# type:ignore - """This package comes with a built-in CLI for running a resolver web application. .. code-block:: @@ -25,19 +23,30 @@ The same flags and arguments are applicable. """ +from __future__ import annotations + import sys from collections.abc import Mapping -from typing import Callable +from typing import TYPE_CHECKING, Callable import click +import fastapi +from typing_extensions import TypeAlias from curies import Converter, sources +if TYPE_CHECKING: + import fastapi + import flask + __all__ = [ "main", ] -LOADERS = { +AppHint: TypeAlias = "flask.Flask | fastapi.FastAPI" + + +LOADERS: dict[str, Callable[[str], Converter]] = { "jsonld": Converter.from_jsonld, "prefix_map": Converter.from_prefix_map, "extended_prefix_map": Converter.from_extended_prefix_map, @@ -54,7 +63,7 @@ } -def _get_converter(location, format) -> Converter: +def _get_converter(location: str, format: str | None) -> Converter: if location in CONVERTERS: return CONVERTERS[location]() if format is None: @@ -63,7 +72,7 @@ def _get_converter(location, format) -> Converter: return LOADERS[format](location) -def _get_resolver_app(converter: Converter, framework: str): +def _get_resolver_app(converter: Converter, framework: str) -> AppHint: from curies import resolver_service if framework == "flask": @@ -74,7 +83,7 @@ def _get_resolver_app(converter: Converter, framework: str): raise ValueError(f"Unhandled framework: {framework}") -def _get_mapper_app(converter: Converter, framework: str): +def _get_mapper_app(converter: Converter, framework: str) -> AppHint: from curies import mapping_service if framework == "flask": @@ -85,13 +94,14 @@ def _get_mapper_app(converter: Converter, framework: str): raise ValueError(f"Unhandled framework: {framework}") -def _run_app(app, server, host, port): +def _run_app(app: AppHint, server: str, host: str, port: int) -> None: if server == "uvicorn": import uvicorn uvicorn.run(app, host=host, port=port) elif server == "werkzeug": - app.run(host=host, port=port) + # we ignore the type because at this point, we know the app has to be a flask.Flask + app.run(host=host, port=port) # type:ignore[union-attr] elif server == "gunicorn": raise NotImplementedError else: @@ -130,7 +140,7 @@ def _run_app(app, server, host, port): @click.group() -def main(): +def main() -> None: """Run the `curies` CLI.""" @@ -145,7 +155,7 @@ def main(): @FORMAT_OPTION @HOST_OPTION @PORT_OPTION -def resolver(location, host: str, port: int, framework: str, format: str, server: str): +def resolver(location: str, host: str, port: int, framework: str, format: str, server: str) -> None: """Serve a resolver app.""" converter = _get_converter(location, format) app = _get_resolver_app(converter, framework=framework) @@ -163,7 +173,7 @@ def resolver(location, host: str, port: int, framework: str, format: str, server @FORMAT_OPTION @HOST_OPTION @PORT_OPTION -def mapper(location, host: str, port: int, framework: str, format: str, server: str): +def mapper(location: str, host: str, port: int, framework: str, format: str, server: str) -> None: """Serve a mapper app.""" converter = _get_converter(location, format) app = _get_mapper_app(converter, framework=framework) diff --git a/src/curies/discovery.py b/src/curies/discovery.py index 3ff15e8..0ec2a9d 100644 --- a/src/curies/discovery.py +++ b/src/curies/discovery.py @@ -39,10 +39,12 @@ 4. Construct a converter from this prefix map and return it """ +from __future__ import annotations + from collections import defaultdict from collections.abc import Iterable, Mapping, Sequence from pathlib import PurePath -from typing import IO, TYPE_CHECKING, Any, Optional, TextIO, Union +from typing import IO, TYPE_CHECKING, Any, TextIO, Union from typing_extensions import Literal @@ -62,9 +64,9 @@ def discover_from_rdf( - graph: Union[GraphInput, "rdflib.Graph"], + graph: GraphInput | rdflib.Graph, *, - format: Optional[GraphFormats] = None, + format: GraphFormats | None = None, **kwargs: Any, ) -> Converter: """Discover new URI prefixes from RDF content via :mod:`rdflib`. @@ -94,7 +96,7 @@ def discover_from_rdf( def get_uris_from_rdf( - graph: Union[GraphInput, "rdflib.Graph"], *, format: Optional[GraphFormats] = None + graph: GraphInput | rdflib.Graph, *, format: GraphFormats | None = None ) -> set[str]: """Get a set of URIs from a graph.""" graph = _ensure_graph(graph=graph, format=format) @@ -102,8 +104,8 @@ def get_uris_from_rdf( def _ensure_graph( - *, graph: Union[GraphInput, "rdflib.Graph"], format: Optional[GraphFormats] = None -) -> "rdflib.Graph": + *, graph: GraphInput | rdflib.Graph, format: GraphFormats | None = None +) -> rdflib.Graph: import rdflib if not isinstance(graph, rdflib.Graph): @@ -114,7 +116,7 @@ def _ensure_graph( return graph -def _yield_uris(*, graph: "rdflib.Graph") -> Iterable[str]: +def _yield_uris(*, graph: rdflib.Graph) -> Iterable[str]: import rdflib for parts in graph.triples((None, None, None)): @@ -126,10 +128,10 @@ def _yield_uris(*, graph: "rdflib.Graph") -> Iterable[str]: def discover( uris: Iterable[str], *, - delimiters: Optional[Sequence[str]] = None, - cutoff: Optional[int] = None, + delimiters: Sequence[str] | None = None, + cutoff: int | None = None, metaprefix: str = "ns", - converter: Optional[Converter] = None, + converter: Converter | None = None, ) -> Converter: """Discover new URI prefixes and construct a converter with a unique dummy CURIE prefix for each. @@ -218,9 +220,9 @@ def discover( def _get_uri_prefix_to_luids( *, - converter: Optional[Converter] = None, + converter: Converter | None = None, uris: Iterable[str], - delimiters: Optional[Sequence[str]] = None, + delimiters: Sequence[str] | None = None, ) -> Mapping[str, set[str]]: """Get a mapping from putative URI prefixes to corresponding putative local unique identifiers. diff --git a/src/curies/mapping_service/api.py b/src/curies/mapping_service/api.py index 031ddff..ea77c6b 100644 --- a/src/curies/mapping_service/api.py +++ b/src/curies/mapping_service/api.py @@ -1,8 +1,10 @@ """Implementation of mapping service.""" +from __future__ import annotations + import itertools as itt from collections.abc import Collection, Iterable -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast from rdflib import OWL, Graph, URIRef from rdflib.term import _is_valid_uri @@ -16,7 +18,7 @@ import flask -def _prepare_predicates(predicates: Union[None, str, Collection[str]] = None) -> set[URIRef]: +def _prepare_predicates(predicates: None | str | Collection[str] = None) -> set[URIRef]: if predicates is None: return {OWL.sameAs} if isinstance(predicates, str): @@ -24,17 +26,17 @@ def _prepare_predicates(predicates: Union[None, str, Collection[str]] = None) -> return {URIRef(predicate) for predicate in predicates} -class MappingServiceGraph(Graph): # type:ignore +class MappingServiceGraph(Graph): """A service that implements identifier mapping based on a converter.""" converter: Converter - predicates: set[URIRef] + query_predicates: set[URIRef] def __init__( self, *args: Any, converter: Converter, - predicates: Union[None, str, list[str]] = None, + predicates: None | str | list[str] = None, **kwargs: Any, ) -> None: """Instantiate the graph. @@ -89,7 +91,7 @@ def __init__( ====================================== ================================================= """ self.converter = converter - self.predicates = _prepare_predicates(predicates) + self.query_predicates = _prepare_predicates(predicates) super().__init__(*args, **kwargs) def _expand_pair_all(self, uri_in: str) -> list[URIRef]: @@ -101,25 +103,25 @@ def _expand_pair_all(self, uri_in: str) -> list[URIRef]: # produce invalid URIs e.g., containing spaces return [URIRef(uri) for uri in uris if _is_valid_uri(uri)] - def triples( + def triples( # type:ignore self, triple: tuple[URIRef, URIRef, URIRef] ) -> Iterable[tuple[URIRef, URIRef, URIRef]]: """Generate triples, overriden to dynamically generate mappings based on this graph's converter.""" subj_query, pred_query, obj_query = triple - if pred_query in self.predicates: + if pred_query in self.query_predicates: if subj_query is None and obj_query is not None: subjects = self._expand_pair_all(obj_query) - for subj, pred in itt.product(subjects, self.predicates): + for subj, pred in itt.product(subjects, self.query_predicates): yield subj, pred, obj_query elif subj_query is not None and obj_query is None: objects = self._expand_pair_all(subj_query) - for obj, pred in itt.product(objects, self.predicates): + for obj, pred in itt.product(objects, self.query_predicates): yield subj_query, pred, obj def get_flask_mapping_blueprint( converter: Converter, route: str = "/sparql", **kwargs: Any -) -> "flask.Blueprint": +) -> flask.Blueprint: """Get a blueprint for :class:`flask.Flask`. :param converter: A converter @@ -133,8 +135,8 @@ def get_flask_mapping_blueprint( graph = MappingServiceGraph(converter=converter) processor = MappingServiceSPARQLProcessor(graph=graph) - @blueprint.route(route, methods=["GET", "POST"]) # type:ignore - def serve_sparql() -> "Response": + @blueprint.route(route, methods=["GET", "POST"]) + def serve_sparql() -> Response: """Run a SPARQL query and serve the results.""" sparql = request.values.get("query") if not sparql: @@ -151,7 +153,7 @@ def serve_sparql() -> "Response": def get_fastapi_router( converter: Converter, route: str = "/sparql", **kwargs: Any -) -> "fastapi.APIRouter": +) -> fastapi.APIRouter: """Get a router for :class:`fastapi.FastAPI`. :param converter: A converter @@ -165,13 +167,13 @@ def get_fastapi_router( graph = MappingServiceGraph(converter=converter) processor = MappingServiceSPARQLProcessor(graph=graph) - def _resolve(accept: Header, sparql: str) -> Response: + def _resolve(accept: str, sparql: str) -> Response: content_type = handle_header(accept) results = graph.query(sparql, processor=processor) response = results.serialize(format=CONTENT_TYPE_TO_RDFLIB_FORMAT[content_type]) return Response(response, media_type=content_type) - @api_router.get(route) # type:ignore + @api_router.get(route) def resolve_get( query: str = Query(description="The SPARQL query to run"), accept: str = Header(), @@ -179,7 +181,7 @@ def resolve_get( """Run a SPARQL query and serve the results.""" return _resolve(accept, query) - @api_router.post(route) # type:ignore + @api_router.post(route) def resolve_post( query: str = Form(description="The SPARQL query to run"), accept: str = Header(), @@ -190,7 +192,7 @@ def resolve_post( return api_router -def get_flask_mapping_app(converter: Converter) -> "flask.Flask": +def get_flask_mapping_app(converter: Converter) -> flask.Flask: """Get a Flask app for the mapping service.""" from flask import Flask @@ -200,7 +202,7 @@ def get_flask_mapping_app(converter: Converter) -> "flask.Flask": return app -def get_fastapi_mapping_app(converter: Converter) -> "fastapi.FastAPI": +def get_fastapi_mapping_app(converter: Converter) -> fastapi.FastAPI: """Get a FastAPI app. :param converter: A converter diff --git a/src/curies/mapping_service/rdflib_custom.py b/src/curies/mapping_service/rdflib_custom.py index 0f9cae9..5dea71b 100644 --- a/src/curies/mapping_service/rdflib_custom.py +++ b/src/curies/mapping_service/rdflib_custom.py @@ -2,7 +2,7 @@ """A custom SPARQL processor that optimizes the query based on https://github.com/RDFLib/rdflib/pull/2257.""" -from typing import Union +from __future__ import annotations from rdflib.plugins.sparql.algebra import translateQuery from rdflib.plugins.sparql.evaluate import evalQuery @@ -52,7 +52,7 @@ class MappingServiceSPARQLProcessor(SPARQLProcessor): def query( self, - query: Union[str, Query], + query: str | Query, initBindings=None, # noqa:N803 initNs=None, # noqa:N803 base=None, diff --git a/src/curies/mapping_service/utils.py b/src/curies/mapping_service/utils.py index 3528c35..25d1819 100644 --- a/src/curies/mapping_service/utils.py +++ b/src/curies/mapping_service/utils.py @@ -1,10 +1,12 @@ """Utilities for the mapping service.""" +from __future__ import annotations + import json import json.decoder import unittest from collections.abc import Mapping -from typing import Callable, Optional +from typing import Callable from defusedxml import ElementTree @@ -127,7 +129,7 @@ def parse_header(header: str) -> list[str]: return sorted(parts, key=parts.__getitem__, reverse=True) -def handle_header(header: Optional[str], default: str = DEFAULT_CONTENT_TYPE) -> str: +def handle_header(header: str | None, default: str = DEFAULT_CONTENT_TYPE) -> str: """Canonicalize a header.""" if not header: return default diff --git a/src/curies/reconciliation.py b/src/curies/reconciliation.py index 7c02bf3..8cdb5b0 100644 --- a/src/curies/reconciliation.py +++ b/src/curies/reconciliation.py @@ -1,9 +1,10 @@ """Reconciliation.""" +from __future__ import annotations + import logging from collections import Counter, defaultdict from collections.abc import Collection, Mapping -from typing import Optional from .api import Converter, Record @@ -161,7 +162,7 @@ def rewire(converter: Converter, rewiring: Mapping[str, str]) -> Converter: return Converter(records) -def _get_curie_preferred_or_synonym(record: Record, upgrades: Mapping[str, str]) -> Optional[str]: +def _get_curie_preferred_or_synonym(record: Record, upgrades: Mapping[str, str]) -> str | None: if record.prefix in upgrades: return upgrades[record.prefix] for s in record.prefix_synonyms: @@ -170,7 +171,7 @@ def _get_curie_preferred_or_synonym(record: Record, upgrades: Mapping[str, str]) return None -def _get_uri_preferred_or_synonym(record: Record, upgrades: Mapping[str, str]) -> Optional[str]: +def _get_uri_preferred_or_synonym(record: Record, upgrades: Mapping[str, str]) -> str | None: if record.uri_prefix in upgrades: return upgrades[record.uri_prefix] for s in record.uri_prefix_synonyms: diff --git a/src/curies/resolver_service.py b/src/curies/resolver_service.py index d1e7057..43305c6 100644 --- a/src/curies/resolver_service.py +++ b/src/curies/resolver_service.py @@ -1,7 +1,9 @@ """A simple web service for resolving CURIEs.""" +from __future__ import annotations + from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from .api import Converter @@ -21,7 +23,7 @@ FAILURE_CODE = 422 -def get_flask_blueprint(converter: Converter, **kwargs: Any) -> "flask.Blueprint": +def get_flask_blueprint(converter: Converter, **kwargs: Any) -> flask.Blueprint: """Get a blueprint for :class:`flask.Flask`. :param converter: A converter @@ -69,8 +71,8 @@ def get_flask_blueprint(converter: Converter, **kwargs: Any) -> "flask.Blueprint blueprint = Blueprint("metaresolver", __name__, **kwargs) - @blueprint.route(f"/{converter.delimiter}") # type:ignore - def resolve(prefix: str, identifier: str) -> "Response": + @blueprint.route(f"/{converter.delimiter}") + def resolve(prefix: str, identifier: str) -> Response: """Resolve a CURIE.""" location = converter.expand_pair(prefix, identifier) if location is None: @@ -83,10 +85,10 @@ def resolve(prefix: str, identifier: str) -> "Response": def get_flask_app( converter: Converter, - blueprint_kwargs: Optional[Mapping[str, Any]] = None, - flask_kwargs: Optional[Mapping[str, Any]] = None, - register_kwargs: Optional[Mapping[str, Any]] = None, -) -> "flask.Flask": + blueprint_kwargs: Mapping[str, Any] | None = None, + flask_kwargs: Mapping[str, Any] | None = None, + register_kwargs: Mapping[str, Any] | None = None, +) -> flask.Flask: """Get a Flask app. :param converter: A converter @@ -152,7 +154,7 @@ def get_flask_app( return app -def get_fastapi_router(converter: Converter, **kwargs: Any) -> "fastapi.APIRouter": +def get_fastapi_router(converter: Converter, **kwargs: Any) -> fastapi.APIRouter: """Get a router for :class:`fastapi.FastAPI`. :param converter: A converter @@ -200,7 +202,7 @@ def get_fastapi_router(converter: Converter, **kwargs: Any) -> "fastapi.APIRoute api_router = APIRouter(**kwargs) - @api_router.get(f"/{{prefix}}{converter.delimiter}{{identifier}}") # type:ignore + @api_router.get(f"/{{prefix}}{converter.delimiter}{{identifier}}") def resolve( prefix: str = Path( title="Prefix", @@ -227,10 +229,10 @@ def resolve( def get_fastapi_app( converter: Converter, - router_kwargs: Optional[Mapping[str, Any]] = None, - fastapi_kwargs: Optional[Mapping[str, Any]] = None, - include_kwargs: Optional[Mapping[str, Any]] = None, -) -> "fastapi.FastAPI": + router_kwargs: Mapping[str, Any] | None = None, + fastapi_kwargs: Mapping[str, Any] | None = None, + include_kwargs: Mapping[str, Any] | None = None, +) -> fastapi.FastAPI: """Get a FastAPI app. :param converter: A converter diff --git a/src/curies/sources.py b/src/curies/sources.py index a84ce0f..c9006de 100644 --- a/src/curies/sources.py +++ b/src/curies/sources.py @@ -1,5 +1,7 @@ """External sources of contexts.""" +from __future__ import annotations + from typing import Any from .api import Converter @@ -49,10 +51,10 @@ def get_obo_converter() -> Converter: return Converter.from_jsonld(url) -def get_prefixcommons_converter(name: str) -> Converter: +def get_prefixcommons_converter(name: str = "monarch_context") -> Converter: """Get a Prefix Commons-maintained context. - :param name: The name of the JSON-LD file (e.g., ``monarch_context``). + :param name: The name of the JSON-LD file (e.g., defaults to ``monarch_context``). See the full list at https://github.com/prefixcommons/prefixcommons-py/tree/master/prefixcommons/registry. :returns: A converter diff --git a/tox.ini b/tox.ini index d4062bf..43997e0 100644 --- a/tox.ini +++ b/tox.ini @@ -121,8 +121,9 @@ description = Run the mypy tool to check static typing on the project. deps = mypy types-requests + types-PyYAML + types-ujson pydantic -skip_install = true commands = mypy --install-types --non-interactive --ignore-missing-imports --strict src/ [testenv:doc8]