diff --git a/.appveyor.yml b/.appveyor.yml index 773607c7..4219b657 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -51,7 +51,7 @@ build: off environment: # not ready for --doctest-modules yet, some are invalid - TEST_SCRIPT: 'hatch test -i py=3.9 --cover --durations 10' + TEST_SCRIPT: 'hatch test -i py=3.10 --cover --durations 10' # unless indicated otherwise, we test datalad_next DTS: datalad_next # place coverage files to a known location regardless of where a test run diff --git a/datalad_next/annexremotes/archivist.py b/datalad_next/annexremotes/archivist.py index e374c10d..615abf9b 100644 --- a/datalad_next/annexremotes/archivist.py +++ b/datalad_next/annexremotes/archivist.py @@ -35,12 +35,7 @@ from pathlib import Path from shutil import copyfileobj -from typing import ( - Dict, - Generator, - List, - Tuple, -) +from collections.abc import Generator from datalad_next.archive_operations import ArchiveOperations @@ -379,13 +374,13 @@ class _ArchiveHandlers: # TODO make archive access caching behavior configurable from the outside def __init__(self, repo): # mapping of archive keys to an info dict - self._db: Dict[AnnexKey, _ArchiveInfo] = {} + self._db: dict[AnnexKey, _ArchiveInfo] = {} # for running git-annex queries against the repo self._repo = repo def from_locators( - self, locs: List[ArchivistLocator] - ) -> Generator[Tuple[ArchiveOperations, Iterable[ArchivistLocator]], + self, locs: list[ArchivistLocator], + ) -> Generator[tuple[ArchiveOperations, Iterable[ArchivistLocator]], None, None]: """Produce archive handlers for the given locators diff --git a/datalad_next/commands/__init__.py b/datalad_next/commands/__init__.py index 0262dfac..9054e252 100644 --- a/datalad_next/commands/__init__.py +++ b/datalad_next/commands/__init__.py @@ -18,8 +18,6 @@ """ from __future__ import annotations -from typing import Dict - from datalad.interface.base import ( Interface, build_doc, diff --git a/datalad_next/commands/credentials.py b/datalad_next/commands/credentials.py index 44f3f57f..f5c34384 100644 --- a/datalad_next/commands/credentials.py +++ b/datalad_next/commands/credentials.py @@ -12,7 +12,6 @@ import json import logging -from typing import Dict from datalad import ( cfg as dlcfg, diff --git a/datalad_next/config/utils.py b/datalad_next/config/utils.py index c605ded1..813fe0d8 100644 --- a/datalad_next/config/utils.py +++ b/datalad_next/config/utils.py @@ -1,14 +1,10 @@ from __future__ import annotations from os import environ -from typing import ( - Dict, - Mapping, - Tuple, -) +from collections.abc import Mapping -def get_gitconfig_items_from_env() -> Mapping[str, str | Tuple[str, ...]]: +def get_gitconfig_items_from_env() -> Mapping[str, str | tuple[str, ...]]: """Parse git-config ENV (``GIT_CONFIG_COUNT|KEY|VALUE``) and return as dict This implementation does not use ``git-config`` directly, but aims to @@ -29,7 +25,7 @@ def get_gitconfig_items_from_env() -> Mapping[str, str | Tuple[str, ...]]: times, the respective values are aggregated in reported as a tuple for that specific key. """ - items: Dict[str, str | Tuple[str, ...]] = {} + items: dict[str, str | tuple[str, ...]] = {} for k, v in ((_get_gitconfig_var_from_env(i, 'key'), _get_gitconfig_var_from_env(i, 'value')) for i in range(_get_gitconfig_itemcount())): @@ -64,7 +60,7 @@ def _get_gitconfig_var_from_env(nid: int, kind: str) -> str: return var -def set_gitconfig_items_in_env(items: Mapping[str, str | Tuple[str, ...]]): +def set_gitconfig_items_in_env(items: Mapping[str, str | tuple[str, ...]]): """Set git-config ENV (``GIT_CONFIG_COUNT|KEY|VALUE``) from a mapping Any existing declaration of configuration items in the environment is diff --git a/datalad_next/constraints/exceptions.py b/datalad_next/constraints/exceptions.py index b02411e4..5d011d8f 100644 --- a/datalad_next/constraints/exceptions.py +++ b/datalad_next/constraints/exceptions.py @@ -6,11 +6,7 @@ from dataclasses import dataclass from textwrap import indent from types import MappingProxyType -from typing import ( - Any, - Dict, - Tuple, -) +from typing import Any # needed for imports in other pieced of the ``constraints`` module from datalad_next.exceptions import NoDatasetFound @@ -38,7 +34,7 @@ def __init__(self, constraint, value: Any, msg: str, - ctx: Dict[str, Any] | None = None): + ctx: dict[str, Any] | None = None): """ Parameters ---------- @@ -100,7 +96,7 @@ def constraint(self): return self.args[1] @property - def caused_by(self) -> Tuple[Exception] | None: + def caused_by(self) -> tuple[Exception] | None: """Returns a tuple of any underlying exceptions that caused a violation """ cb = self.context.get('__caused_by__', None) @@ -149,7 +145,7 @@ class ConstraintErrors(ConstraintError): nature of the context identifiers (expect for being hashable). See ``CommandParametrizationError`` for a specialization. """ - def __init__(self, exceptions: Dict[Any, ConstraintError]): + def __init__(self, exceptions: dict[Any, ConstraintError]): super().__init__( # this is the main payload, the base class expects a Constraint # but only stores it @@ -184,7 +180,7 @@ class ParameterContextErrors(Mapping): # went wrong (in general, for a specific parameter, etc...) def __init__( self, - errors: Dict[ParameterConstraintContext, ConstraintError], + errors: dict[ParameterConstraintContext, ConstraintError], ): self._errors = errors @@ -249,7 +245,7 @@ class ParameterConstraintContext: EnsureRange(min=3)(params['p1'] + params['p2']) """ - parameters: Tuple[str] + parameters: tuple[str, ...] description: str | None = None def __str__(self): @@ -297,8 +293,8 @@ class ParametrizationErrors(ConstraintErrors): """ def __init__( self, - exceptions: Dict[str, ConstraintError] | - Dict[ParameterConstraintContext, ConstraintError]): + exceptions: dict[str, ConstraintError] | + dict[ParameterConstraintContext, ConstraintError]): super().__init__( {k if isinstance(k, ParameterConstraintContext) else ParameterConstraintContext((k,)): diff --git a/datalad_next/constraints/parameter.py b/datalad_next/constraints/parameter.py index 1ae892e5..9ed52f2a 100644 --- a/datalad_next/constraints/parameter.py +++ b/datalad_next/constraints/parameter.py @@ -2,12 +2,11 @@ from __future__ import annotations -from collections.abc import Container -from itertools import chain -from typing import ( +from collections.abc import ( Callable, - Dict, + Container, ) +from itertools import chain from .base import Constraint from .basic import ( @@ -92,12 +91,12 @@ class EnsureCommandParameterization(Constraint): """ def __init__( self, - param_constraints: Dict[str, Constraint], + param_constraints: dict[str, Constraint], *, validate_defaults: Container[str] | None = None, joint_constraints: - Dict[ParameterConstraintContext, Callable] | None = None, - tailor_for_dataset: Dict[str, str] | None = None, + dict[ParameterConstraintContext, Callable] | None = None, + tailor_for_dataset: dict[str, str] | None = None, ): """ Parameters @@ -134,7 +133,7 @@ def __init__( self._validate_defaults = validate_defaults or set() self._tailor_for_dataset = tailor_for_dataset or {} - def joint_validation(self, params: Dict, on_error: str) -> Dict: + def joint_validation(self, params: dict, on_error: str) -> dict: """Higher-order validation considering multiple parameters at a time This method is called with all, individually validated, command @@ -245,7 +244,7 @@ def __call__( at_default=None, required=None, on_error='raise-early', - ) -> Dict: + ) -> dict: """ Parameters ---------- diff --git a/datalad_next/iter_collections/annexworktree.py b/datalad_next/iter_collections/annexworktree.py index 4e950bb9..6c4153e1 100644 --- a/datalad_next/iter_collections/annexworktree.py +++ b/datalad_next/iter_collections/annexworktree.py @@ -13,9 +13,6 @@ PurePath, ) from typing import ( - Dict, - Type, - Union, Any, Generator, ) @@ -345,7 +342,7 @@ def _get_worktree_item( def _join_annex_info( - processed_data: Union[Type[StoreOnly], Dict[str, str]], + processed_data: type[StoreOnly] | dict[str, str], stored_data: GitWorktreeItem, ) -> dict: """Internal helper to join results from pipeline stages diff --git a/datalad_next/iter_collections/utils.py b/datalad_next/iter_collections/utils.py index bdabeefd..3b12f023 100644 --- a/datalad_next/iter_collections/utils.py +++ b/datalad_next/iter_collections/utils.py @@ -12,11 +12,8 @@ import stat from typing import ( TYPE_CHECKING, - Dict, - Union, Any, IO, - List, ) from datalad_next.consts import COPY_BUFSIZE @@ -102,12 +99,12 @@ def from_path( path: Path, *, link_target: bool = True, - ) -> Union[ - DirectoryItem, - AnnexWorktreeFileSystemItem, - FileSystemItem, - GitWorktreeFileSystemItem, - ]: + ) -> ( + DirectoryItem + | AnnexWorktreeFileSystemItem + | FileSystemItem + | GitWorktreeFileSystemItem + ): """Populate item properties from a single `stat` and `readlink` call The given ``path`` must exist. The ``link_target`` flag indicates @@ -142,10 +139,10 @@ def from_path( def compute_multihash_from_fp( - fp: Union[BufferedReader, ExFileObject, ZipExtFile], - hash: List[str], + fp: BufferedReader | ExFileObject | ZipExtFile, + hash: list[str], bufsize: int = COPY_BUFSIZE, -) -> Dict[str, str]: +) -> dict[str, str]: """Compute multiple hashes from a file-like """ mhash = MultiHash(hash) diff --git a/datalad_next/patches/customremotes_main.py b/datalad_next/patches/customremotes_main.py index 167d6674..d25c4a74 100644 --- a/datalad_next/patches/customremotes_main.py +++ b/datalad_next/patches/customremotes_main.py @@ -14,12 +14,10 @@ This patch also adds code that allows to patch a class that is already loaded """ +from __future__ import annotations + from contextlib import closing import logging -from typing import ( - Dict, - Type, -) from . import apply_patch from datalad_next.annexremotes import SpecialRemote @@ -63,7 +61,7 @@ class AnnexProgressLogHandler(logging.Handler): def __init__(self, annexremote: SpecialRemote): super().__init__() self.annexremote = annexremote - self._ptrackers: Dict[str, int] = {} + self._ptrackers: dict[str, int] = {} def emit(self, record: logging.LogRecord): """Process a log record @@ -101,7 +99,7 @@ def emit(self, record: logging.LogRecord): self.annexremote.send_progress(prg) -def patched_underscore_main(args: list, cls: Type[SpecialRemote]): +def patched_underscore_main(args: list, cls: type[SpecialRemote]): """Full replacement for datalad.customremotes.main._main() Its only purpose is to create a running instance of a SpecialRemote. diff --git a/datalad_next/patches/push_to_export_remote.py b/datalad_next/patches/push_to_export_remote.py index 3dec437b..119d50c9 100644 --- a/datalad_next/patches/push_to_export_remote.py +++ b/datalad_next/patches/push_to_export_remote.py @@ -9,11 +9,8 @@ import logging from typing import ( - Dict, Generator, Iterable, - Optional, - Union, ) import datalad.core.distributed.push as mod_push @@ -37,12 +34,12 @@ lgr = logging.getLogger('datalad.core.distributed.push') -def _is_export_remote(remote_info: Optional[Dict]) -> bool: +def _is_export_remote(remote_info: dict | None) -> bool: """Check if remote_info is valid and has exporttree set to "yes" Parameters ---------- - remote_info: Optional[Dict] + remote_info: dict | None Optional dictionary the contains git annex special. Returns @@ -57,8 +54,8 @@ def _is_export_remote(remote_info: Optional[Dict]) -> bool: def _get_credentials(ds: Dataset, - remote_info: Dict - ) -> Optional[Dict]: + remote_info: dict, + ) -> dict | None: # Check for credentials params = { @@ -125,8 +122,8 @@ def get_export_records(repo: AnnexRepo) -> Generator: def _get_export_log_entry(repo: AnnexRepo, - target_uuid: str - ) -> Optional[Dict]: + target_uuid: str, + ) -> dict | None: target_entries = [ entry for entry in repo.get_export_records() @@ -138,7 +135,7 @@ def _get_export_log_entry(repo: AnnexRepo, def _is_valid_treeish(repo: AnnexRepo, - export_entry: Dict, + export_entry: dict, ) -> bool: # Due to issue https://github.com/datalad/datalad-next/issues/39 @@ -156,10 +153,10 @@ def _transfer_data(repo: AnnexRepo, target: str, content: Iterable, data: str, - force: Optional[str], - jobs: Optional[Union[str, int]], - res_kwargs: Dict, - got_path_arg: bool + force: str | None, + jobs: str | int | None, + res_kwargs: dict, + got_path_arg: bool, ) -> Generator: target_uuid, remote_info = ([ diff --git a/datalad_next/url_operations/any.py b/datalad_next/url_operations/any.py index a3d9d3ac..11b14e7d 100644 --- a/datalad_next/url_operations/any.py +++ b/datalad_next/url_operations/any.py @@ -8,7 +8,6 @@ import logging from pathlib import Path import re -from typing import Dict from datalad_next.config import ConfigManager from datalad_next.exceptions import CapturedException @@ -169,7 +168,7 @@ def stat(self, url: str, *, credential: str | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Call `*UrlOperations.stat()` for the respective URL scheme""" return self._get_handler(url).stat( url, credential=credential, timeout=timeout) @@ -180,7 +179,7 @@ def download(self, *, credential: str | None = None, hash: list[str] | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Call `*UrlOperations.download()` for the respective URL scheme""" return self._get_handler(from_url).download( from_url, to_path, credential=credential, hash=hash, @@ -192,7 +191,7 @@ def upload(self, *, credential: str | None = None, hash: list[str] | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Call `*UrlOperations.upload()` for the respective URL scheme""" return self._get_handler(to_url).upload( from_path, to_url, credential=credential, hash=hash, @@ -202,7 +201,7 @@ def delete(self, url: str, *, credential: str | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Call `*UrlOperations.delete()` for the respective URL scheme""" return self._get_handler(url).delete( url, credential=credential, timeout=timeout) diff --git a/datalad_next/url_operations/http.py b/datalad_next/url_operations/http.py index 47c68132..5d98e502 100644 --- a/datalad_next/url_operations/http.py +++ b/datalad_next/url_operations/http.py @@ -6,7 +6,7 @@ import logging from pathlib import Path import sys -from typing import Dict + import requests from requests_toolbelt import user_agent @@ -37,7 +37,7 @@ class HttpUrlOperations(UrlOperations): authentication challenges. """ - def __init__(self, cfg=None, headers: Dict | None = None): + def __init__(self, cfg=None, headers: dict | None = None): """ Parameters ---------- @@ -56,7 +56,7 @@ def __init__(self, cfg=None, headers: Dict | None = None): if headers: self._headers.update(headers) - def get_headers(self, headers: Dict | None = None) -> Dict: + def get_headers(self, headers: dict | None = None) -> dict: # start with the default hdrs = dict(self._headers) if headers is not None: @@ -82,7 +82,7 @@ def stat(self, url: str, *, credential: str | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Gather information on a URL target, without downloading it See :meth:`datalad_next.url_operations.UrlOperations.stat` @@ -94,7 +94,7 @@ def stat(self, For access targets found absent. """ auth = DataladAuth(self.cfg, credential=credential) - props: Dict[str, str | int] + props: dict[str, str | int] with requests.head( url, headers=self.get_headers(), @@ -131,7 +131,7 @@ def delete(self, url: str, *, credential: str | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Delete the target of a http(s)://-URL """ @@ -159,7 +159,7 @@ def download(self, *, credential: str | None = None, hash: list[str] | None = None, - timeout: float | None = None) -> Dict: + timeout: float | None = None) -> dict: """Download via HTTP GET request See :meth:`datalad_next.url_operations.UrlOperations.download` @@ -259,7 +259,7 @@ def probe_url(self, url, timeout=10.0, headers=None): return req.url, props def _stream_download_from_request( - self, r, to_path, hash: list[str] | None = None) -> Dict: + self, r, to_path, hash: list[str] | None = None) -> dict: from_url = r.url hasher = self._get_hasher(hash) progress_id = self._get_progress_id(from_url, to_path) @@ -283,7 +283,7 @@ def _stream_download_from_request( ) fp = None - props: Dict[str, str] = {} + props: dict[str, str] = {} try: # we can only write to file-likes opened in bytes mode fp = sys.stdout.buffer if to_path is None else open(to_path, 'wb') diff --git a/datalad_next/utils/multihash.py b/datalad_next/utils/multihash.py index b691695a..ba9328b0 100644 --- a/datalad_next/utils/multihash.py +++ b/datalad_next/utils/multihash.py @@ -3,10 +3,6 @@ from __future__ import annotations import hashlib -from typing import ( - ByteString, - Dict, -) class NoOpHash: @@ -50,12 +46,12 @@ def __init__(self, algorithms: list[str]): _hasher.append(hr()) self._hasher = dict(zip(algorithms, _hasher)) - def update(self, data: ByteString) -> None: + def update(self, data: bytes | bytearray | memoryview) -> None: """Updates all configured digests""" for h in self._hasher.values(): h.update(data) - def get_hexdigest(self) -> Dict[str, str]: + def get_hexdigest(self) -> dict[str, str]: """Returns a mapping of algorithm name to hexdigest for all algorithms """ return {a: h.hexdigest() for a, h in self._hasher.items()} diff --git a/datalad_next/utils/requests_auth.py b/datalad_next/utils/requests_auth.py index 8d8473c6..a0a586fe 100644 --- a/datalad_next/utils/requests_auth.py +++ b/datalad_next/utils/requests_auth.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging -from typing import Dict from urllib.parse import urlparse import requests @@ -122,7 +121,7 @@ def __init__(self, cfg: ConfigManager, credential: str | None = None): self._entered_credential = None def save_entered_credential(self, suggested_name: str | None = None, - context: str | None = None) -> Dict | None: + context: str | None = None) -> dict | None: """Utility method to save a pending credential in the store Pending credentials have been entered manually, and were subsequently @@ -155,7 +154,7 @@ def __call__(self, r): return r def _get_credential(self, url, auth_schemes - ) -> tuple[str | None, str | None, Dict | None]: + ) -> tuple[str | None, str | None, dict | None]: """Get a credential for access to `url` given server-supported schemes If a particular credential to use was given to the `DataladAuth` diff --git a/pyproject.toml b/pyproject.toml index c7310b97..10dad2a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ name = "datalad-next" dynamic = ["version"] description = "What is next in DataLad" readme = "README.md" -requires-python = ">= 3.8" +requires-python = ">= 3.10" license = "MIT" keywords = [ "datalad", @@ -99,7 +99,7 @@ extra-dependencies = [ DATALAD_EXTENSIONS_LOAD = "next" [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10", "3.11", "3.12", "3.13"] +python = ["3.10", "3.11", "3.12", "3.13", "3.14"] [tool.hatch.envs.types] description = "type checking with MyPy" @@ -110,7 +110,7 @@ extra-dependencies = [ [tool.hatch.envs.types.scripts] check = [ - "mypy --install-types --non-interactive --python-version 3.8 --pretty --show-error-context {args:datalad_next}", + "mypy --install-types --non-interactive --python-version 3.10 --pretty --show-error-context {args:datalad_next}", ] [tool.hatch.envs.docs] @@ -233,7 +233,7 @@ exclude = [ ] line-length = 88 indent-width = 4 -target-version = "py38" +target-version = "py310" [tool.ruff.format] # Prefer single quotes over double quotes. quote-style = "single" diff --git a/readthedocs.yml b/readthedocs.yml index c90e6e0f..67fd7d38 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -7,9 +7,9 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" + python: "3.10" # Build documentation in the docs/ directory with Sphinx sphinx: