diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1a21714e2..118881f55 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,8 +13,7 @@ jobs: fail-fast: false matrix: python: - - '3.10' - #- 3.11 + - '3.11' steps: - name: Check out repository uses: actions/checkout@v7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 864232f22..178fe8b1f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 820a4a940..2299b505e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.11' - name: Install build & twine run: python -m pip install build twine diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a03fe759e..1d7a08699 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,8 +34,7 @@ jobs: - macos-latest # M1 architecture - macos-15-intel # non-M1 architecture python: - - '3.10' # Needs quotes so YAML doesn't think it's 3.1 - - '3.11' + - '3.11' # Needs quotes so YAML doesn't think it's 3.1 - '3.12' - '3.13' - '3.14' @@ -48,10 +47,10 @@ jobs: - default include: - os: ubuntu-latest - python: '3.10' + python: '3.11' mode: dandi-api - os: ubuntu-latest - python: '3.10' + python: '3.11' mode: dandi-api vendored_dandiapi: ember-dandi instance_name: EMBER-DANDI @@ -61,13 +60,13 @@ jobs: python: 3.14 mode: obolibrary-only - os: ubuntu-latest - python: '3.10' + python: '3.11' mode: dev-deps - os: ubuntu-latest python: 3.14 mode: dev-deps - os: ubuntu-latest - python: '3.10' + python: '3.11' mode: nfs steps: diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml index 3025111d9..ff4ec5448 100644 --- a/.github/workflows/typing.yml +++ b/.github/workflows/typing.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cc1f3519b..e43bb4339 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,7 +2,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.11" python: install: - requirements: docs/requirements.txt diff --git a/dandi/cli/base.py b/dandi/cli/base.py index 1b118d7ed..7aa3059c6 100644 --- a/dandi/cli/base.py +++ b/dandi/cli/base.py @@ -31,21 +31,17 @@ def get_metavar(self, param, ctx=None): class EnumChoice(click.Choice): - """A ``click.Choice`` over a ``str``-valued ``Enum``, matched on member values. + """A ``click.Choice`` over an ``Enum``, matched on member values. - The available choices presented on the command line are the enum member - values (e.g. ``error``, ``skip``), and ``convert`` returns the corresponding - enum member. A string default is converted to its member as well. + ``click >= 8.2`` matches ``Enum`` members on ``.name`` by default; this + subclass overrides :meth:`click.Choice.normalize_choice` to match on + ``.value`` instead, so command-line tokens stay the lowercase enum values + (``error``, ``skip``, ...). ``click.Choice.convert`` then returns the + matched enum member. """ - def __init__(self, enum_cls: type[Enum], case_sensitive: bool = True) -> None: - self.enum_cls = enum_cls - super().__init__([e.value for e in enum_cls], case_sensitive=case_sensitive) - - def convert(self, value, param, ctx): - if value is None or isinstance(value, self.enum_cls): - return value - return self.enum_cls(super().convert(value, param, ctx)) + def normalize_choice(self, choice, ctx=None): + return choice.value if isinstance(choice, Enum) else str(choice) class ChoiceList(click.ParamType): diff --git a/dandi/cli/tests/test_base.py b/dandi/cli/tests/test_base.py index 1576f422a..9732af10f 100644 --- a/dandi/cli/tests/test_base.py +++ b/dandi/cli/tests/test_base.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import StrEnum import click from click.testing import CliRunner @@ -7,14 +7,11 @@ from ..base import EnumChoice -class _Existing(str, Enum): +class _Existing(StrEnum): ERROR = "error" SKIP = "skip" OVERWRITE = "overwrite-different" - def __str__(self) -> str: - return self.value - def _make_command(**option_kwargs): @click.command() diff --git a/dandi/consts.py b/dandi/consts.py index 9f51dcf04..145b39ee2 100644 --- a/dandi/consts.py +++ b/dandi/consts.py @@ -11,7 +11,7 @@ from collections.abc import Iterator from dataclasses import dataclass -from enum import Enum +from enum import Enum, StrEnum import os #: A list of metadata fields which dandi extracts from .nwb files. @@ -101,13 +101,10 @@ class EmbargoStatus(Enum): EMBARGOED = "EMBARGOED" -class SyncMode(str, Enum): +class SyncMode(StrEnum): ASK = "ask" DO = "do" - def __str__(self) -> str: - return self.value - dandiset_metadata_file = "dandiset.yaml" dandiset_identifier_regex = f"^{DANDISET_ID_REGEX}$" diff --git a/dandi/download.py b/dandi/download.py index 438f2e4d8..ff4e172a6 100644 --- a/dandi/download.py +++ b/dandi/download.py @@ -14,7 +14,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence from dataclasses import InitVar, dataclass, field from datetime import datetime -from enum import Enum +from enum import Enum, StrEnum from functools import partial import hashlib import inspect @@ -69,32 +69,23 @@ lgr = get_logger() -class DownloadExisting(str, Enum): +class DownloadExisting(StrEnum): ERROR = "error" SKIP = "skip" OVERWRITE = "overwrite" OVERWRITE_DIFFERENT = "overwrite-different" REFRESH = "refresh" - def __str__(self) -> str: - return self.value - -class DownloadFormat(str, Enum): +class DownloadFormat(StrEnum): PYOUT = "pyout" DEBUG = "debug" - def __str__(self) -> str: - return self.value - -class PathType(str, Enum): +class PathType(StrEnum): EXACT = "exact" GLOB = "glob" - def __str__(self) -> str: - return self.value - def download( urls: str | Sequence[str], diff --git a/dandi/move.py b/dandi/move.py index 72806dcd4..887c7dc1f 100644 --- a/dandi/move.py +++ b/dandi/move.py @@ -15,7 +15,7 @@ from collections.abc import Iterator from contextlib import ExitStack from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from itertools import zip_longest import os.path from pathlib import Path, PurePosixPath @@ -35,24 +35,18 @@ lgr = get_logger() -class MoveExisting(str, Enum): +class MoveExisting(StrEnum): ERROR = "error" SKIP = "skip" OVERWRITE = "overwrite" - def __str__(self) -> str: - return self.value - -class MoveWorkOn(str, Enum): +class MoveWorkOn(StrEnum): AUTO = "auto" BOTH = "both" LOCAL = "local" REMOTE = "remote" - def __str__(self) -> str: - return self.value - #: A /-separated path to an asset, relative to the root of the Dandiset AssetPath = NewType("AssetPath", str) diff --git a/dandi/organize.py b/dandi/organize.py index 70771d6cb..daf4fb9c2 100644 --- a/dandi/organize.py +++ b/dandi/organize.py @@ -15,11 +15,11 @@ from collections import Counter from collections.abc import Sequence from copy import deepcopy -from enum import Enum +from enum import StrEnum import os import os.path as op -import posixpath from pathlib import Path, PurePosixPath +import posixpath import re import traceback import uuid @@ -54,7 +54,7 @@ lgr = get_logger() -class FileOperationMode(str, Enum): +class FileOperationMode(StrEnum): DRY = "dry" SIMULATE = "simulate" COPY = "copy" @@ -63,23 +63,17 @@ class FileOperationMode(str, Enum): SYMLINK = "symlink" AUTO = "auto" - def __str__(self) -> str: - return self.value - def as_copy_mode(self) -> CopyMode: # Raises ValueError if the mode can't be mapped to a CopyMode return CopyMode(self.value) -class CopyMode(str, Enum): +class CopyMode(StrEnum): SYMLINK = "symlink" COPY = "copy" MOVE = "move" HARDLINK = "hardlink" - def __str__(self) -> str: - return self.value - def copy(self, old_path: AnyPath, new_path: AnyPath) -> None: if self is CopyMode.SYMLINK: os.symlink(old_path, new_path) @@ -93,13 +87,10 @@ def copy(self, old_path: AnyPath, new_path: AnyPath) -> None: raise AssertionError(f"Unhandled CopyMode member: {self!r}") -class OrganizeInvalid(str, Enum): +class OrganizeInvalid(StrEnum): FAIL = "fail" WARN = "warn" - def __str__(self) -> str: - return self.value - dandi_path = op.join("sub-{subject_id}", "{dandi_filename}") diff --git a/dandi/upload.py b/dandi/upload.py index 94adb21b9..e9d5b2402 100644 --- a/dandi/upload.py +++ b/dandi/upload.py @@ -14,7 +14,7 @@ from collections import defaultdict from collections.abc import Iterator, Sequence from contextlib import ExitStack -from enum import Enum +from enum import StrEnum import io import os.path from pathlib import Path @@ -83,25 +83,19 @@ class Uploaded(TypedDict): errors: list[str] -class UploadExisting(str, Enum): +class UploadExisting(StrEnum): ERROR = "error" SKIP = "skip" FORCE = "force" OVERWRITE = "overwrite" REFRESH = "refresh" - def __str__(self) -> str: - return self.value - -class UploadValidation(str, Enum): +class UploadValidation(StrEnum): REQUIRE = "require" SKIP = "skip" IGNORE = "ignore" - def __str__(self) -> str: - return self.value - def upload( paths: Sequence[str | Path] | None = None, diff --git a/pyproject.toml b/pyproject.toml index a7623e51c..03d9f0d62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta' name = "dandi" description = "Command line client for interaction with DANDI instances" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" license = {file = "LICENSE"} authors = [ {name = "DANDI developers", email = "team@dandiarchive.org"}, @@ -36,7 +36,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -46,7 +45,7 @@ classifiers = [ dependencies = [ "bidsschematools ~= 1.0", "bids-validator-deno >= 2.0.5", - "click >= 7.1", + "click >= 8.2", "click-didyoumean", "dandischema ~= 0.12.0", "etelemetry >= 0.2.2", @@ -237,7 +236,7 @@ init_forbid_extra = true [tool.hatch.envs.default] installer = "uv" -python = "3.10" +python = "3.11" [tool.hatch.envs.test] features = ["test"] diff --git a/tox.ini b/tox.ini index 3f735d092..b71a3efcf 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,6 @@ skip_install = true deps = codespell~=2.4.1 flake8 - tomli; python_version < '3.11' commands = codespell dandi docs tools setup.py flake8 --config=setup.cfg {posargs} dandi setup.py