diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 93b246ab8..de32c8df5 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -36,7 +36,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Build the distribution id: build run: nox -vs build @@ -79,7 +79,7 @@ jobs: run: | sudo apt-get -y update sudo apt-get -y install patchelf scons - python -m pip install --upgrade nox pdm==2.26.2 + python -m pip install --upgrade nox pdm==2.26.4 git config --global --add safe.directory '*' - name: Bundle the distribution id: bundle @@ -114,7 +114,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Bundle the distribution id: bundle shell: bash @@ -159,7 +159,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Build Dockerfile run: nox -vs generate_dockerfile - name: Set up QEMU diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aefc4b9b..d02ff5bc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Run linters run: nox -vs lint - name: Validate new changelog entries @@ -47,7 +47,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Build the distribution run: nox -vs build cleanup_buckets: @@ -70,7 +70,7 @@ jobs: cache: "pip" - name: Install dependencies if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Find and remove old buckets if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead run: nox -vs cleanup_buckets @@ -111,7 +111,7 @@ jobs: run: | brew install fish - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Run unit tests run: nox -vs unit -p ${{ matrix.python-version }} - name: Run integration tests (without secrets) @@ -137,7 +137,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Generate Dockerfile run: nox -vs generate_dockerfile - name: Set up QEMU @@ -178,7 +178,7 @@ jobs: run: | sudo apt-get -y update sudo apt-get -y install patchelf scons - python -m pip install --upgrade nox pdm==2.26.2 + python -m pip install --upgrade nox pdm==2.26.4 git config --global --add safe.directory '*' - name: Bundle the distribution id: bundle @@ -217,7 +217,7 @@ jobs: with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies - run: python -m pip install --upgrade nox pdm==2.26.2 + run: python -m pip install --upgrade nox pdm==2.26.4 - name: Bundle the distribution id: bundle shell: bash @@ -256,6 +256,6 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y graphviz plantuml - python -m pip install --upgrade nox pdm==2.26.2 + python -m pip install --upgrade nox pdm==2.26.4 - name: Build the docs run: nox --non-interactive -vs doc diff --git a/b2/_internal/_cli/autocomplete_install.py b/b2/_internal/_cli/autocomplete_install.py index dcd631ea4..57f6a28a8 100644 --- a/b2/_internal/_cli/autocomplete_install.py +++ b/b2/_internal/_cli/autocomplete_install.py @@ -25,7 +25,8 @@ from shlex import quote import argcomplete -from class_registry import ClassRegistry, RegistryKeyError + +from b2._internal.class_registry import ClassRegistry, RegistryKeyError logger = logging.getLogger(__name__) diff --git a/b2/_internal/class_registry.py b/b2/_internal/class_registry.py new file mode 100644 index 000000000..80754c981 --- /dev/null +++ b/b2/_internal/class_registry.py @@ -0,0 +1,108 @@ +###################################################################### +# +# File: b2/_internal/class_registry.py +# +# Copyright 2026 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Callable, Hashable, Iterable, Iterator +from typing import Any + + +class RegistryKeyError(KeyError): + """Raised when a registry lookup fails.""" + + +class ClassRegistry: + """Registry with decorator-based class registration and instantiation.""" + + def __init__(self, attr_name: str | None = None, unique: bool = False) -> None: + self.attr_name = attr_name + self.unique = unique + self._registry: OrderedDict[Hashable, type] = OrderedDict() + + def __contains__(self, key: Hashable) -> bool: + try: + self.get_class(key) + except RegistryKeyError: + return False + return True + + def __getitem__(self, key: Hashable) -> object: + return self.get(key) + + def __iter__(self) -> Iterator[Hashable]: + return self.keys() + + def __len__(self) -> int: + return len(self._registry) + + def __repr__(self) -> str: + return f'{type(self).__name__}(attr_name={self.attr_name!r}, unique={self.unique!r})' + + def __setitem__(self, key: Hashable, class_: type) -> None: + self._register(key, class_) + + def __delitem__(self, key: Hashable) -> None: + self._unregister(key) + + def __missing__(self, key: Hashable) -> object: + raise RegistryKeyError(key) + + @staticmethod + def create_instance(class_: type, *args: Any, **kwargs: Any) -> object: + return class_(*args, **kwargs) + + def get_class(self, key: Hashable) -> type: + try: + return self._registry[key] + except KeyError: + return self.__missing__(key) + + def get(self, key: Hashable, *args: Any, **kwargs: Any) -> object: + return self.create_instance(self.get_class(key), *args, **kwargs) + + def items(self) -> Iterable[tuple[Hashable, type]]: + return self._registry.items() + + def keys(self) -> Iterable[Hashable]: + return self._registry.keys() + + def values(self) -> Iterable[type]: + return self._registry.values() + + def register(self, key: Hashable | type) -> Callable[[type], type] | type: + if isinstance(key, type): + if not self.attr_name: + raise ValueError( + f'Attempting to register {key.__name__} via decorator, but attr_name is not set.' + ) + attr_key = getattr(key, self.attr_name) + self._register(attr_key, key) + return key + + def _decorator(cls: type) -> type: + self._register(key, cls) + return cls + + return _decorator + + def unregister(self, key: Hashable) -> type: + return self._unregister(key) + + def _register(self, key: Hashable, class_: type) -> None: + if key in ['', None]: + raise ValueError( + f'Attempting to register class {class_.__name__} with empty registry key {key!r}.' + ) + if self.unique and key in self._registry: + raise RegistryKeyError(f'{class_.__name__} with key {key!r} is already registered.') + self._registry[key] = class_ + + def _unregister(self, key: Hashable) -> type: + return self._registry.pop(key) if key in self._registry else self.__missing__(key) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 1f25c0544..d14f9834f 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -118,7 +118,6 @@ UnableToCreateDirectory, ) from b2sdk.version import VERSION as b2sdk_version -from class_registry import ClassRegistry from tabulate import tabulate from b2._internal._cli.arg_parser_types import ( @@ -163,6 +162,7 @@ from b2._internal._cli.shell import detect_shell, resolve_short_call_name from b2._internal._utils.uri import B2URI, B2FileIdURI, B2URIAdapter, B2URIBase from b2._internal.arg_parser import B2ArgumentParser, add_normalized_argument +from b2._internal.class_registry import ClassRegistry from b2._internal.json_encoder import B2CliJsonEncoder from b2._internal.version import VERSION diff --git a/changelog.d/1081.changed.md b/changelog.d/1081.changed.md new file mode 100644 index 000000000..58e05935f --- /dev/null +++ b/changelog.d/1081.changed.md @@ -0,0 +1 @@ +Replace the `phx-class-registry` dependency with a simple in-house implementation. diff --git a/pdm.lock b/pdm.lock index e17962ac0..6e4773beb 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "bundle", "doc", "format", "full", "license", "lint", "release", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:197b3857d419ecc19bf7d330c26826205b86cb153f5b838fb25d7a8144a93cac" +content_hash = "sha256:8fd4faeaecd3d00b7066e7c0779e4fb4011256cd0da5dad1caa581e0af1f612b" [[metadata.targets]] requires_python = ">=3.9" @@ -48,7 +48,7 @@ files = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" requires_python = ">=3.9" summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["doc"] @@ -58,8 +58,8 @@ dependencies = [ "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"}, - {file = "anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0"}, + {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, + {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, ] [[package]] @@ -123,13 +123,13 @@ files = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" requires_python = ">=3.7" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default", "doc"] files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] @@ -518,18 +518,17 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.7.1" requires_python = ">=3.9" summary = "Read metadata from Python packages" groups = ["bundle", "doc"] marker = "python_version < \"3.10\"" dependencies = [ - "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=3.20", ] files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, ] [[package]] @@ -781,17 +780,6 @@ files = [ {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] -[[package]] -name = "phx-class-registry" -version = "4.0.6" -requires_python = ">=3" -summary = "Factory+Registry pattern for Python classes" -groups = ["default"] -files = [ - {file = "phx-class-registry-4.0.6.tar.gz", hash = "sha256:66e9818de0a9d62e8cfe311587fcd3853ba941b71c11a7a73e5808d6550db125"}, - {file = "phx_class_registry-4.0.6-py3-none-any.whl", hash = "sha256:90f8c44b9840ac1cb350876157669bf0d1f9d3be614a4f21d739a219a0640601"}, -] - [[package]] name = "pip" version = "25.3" @@ -1087,7 +1075,7 @@ files = [ [[package]] name = "pyinstaller-hooks-contrib" -version = "2025.10" +version = "2025.11" requires_python = ">=3.8" summary = "Community maintained hooks for PyInstaller" groups = ["bundle"] @@ -1097,8 +1085,8 @@ dependencies = [ "setuptools>=42.0.0", ] files = [ - {file = "pyinstaller_hooks_contrib-2025.10-py3-none-any.whl", hash = "sha256:aa7a378518772846221f63a84d6306d9827299323243db890851474dfd1231a9"}, - {file = "pyinstaller_hooks_contrib-2025.10.tar.gz", hash = "sha256:a1a737e5c0dccf1cf6f19a25e2efd109b9fec9ddd625f97f553dac16ee884881"}, + {file = "pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34"}, + {file = "pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d"}, ] [[package]] @@ -1290,7 +1278,7 @@ name = "setuptools" version = "79.0.1" requires_python = ">=3.9" summary = "Easily download, build, install, upgrade, and uninstall Python packages" -groups = ["default", "bundle", "lint"] +groups = ["bundle", "lint"] files = [ {file = "setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51"}, {file = "setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88"}, @@ -1614,54 +1602,59 @@ files = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" requires_python = ">=3.8" summary = "A lil' TOML parser" groups = ["doc", "license", "lint", "release", "test"] marker = "python_version < \"3.11\"" files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] @@ -1735,18 +1728,18 @@ files = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" requires_python = ">=3.9" summary = "HTTP library with thread-safe connection pooling, file post, and more." groups = ["default", "doc"] files = [ - {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, - {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.39.0" requires_python = ">=3.9" summary = "The lightning-fast ASGI server." groups = ["doc"] @@ -1756,8 +1749,8 @@ dependencies = [ "typing-extensions>=4.0; python_version < \"3.11\"", ] files = [ - {file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"}, - {file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"}, + {file = "uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a"}, + {file = "uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 7eec988d9..14e72d23a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,12 +28,10 @@ dependencies = [ "b2sdk>=2.10.2,<3", "docutils>=0.18.1,<0.22", "idna~=3.4; platform_system == 'Java'", - "phx-class-registry>=4.0,<5", "rst2ansi==0.1.5", "tabulate==0.9.0", "tqdm>=4.65.0,<5", "platformdirs>=3.11.0,<5", - "setuptools>=60,<80; python_version < '3.10'", # required by phx-class-registry<4.1 ] [project.optional-dependencies] diff --git a/test/unit/test_class_registry.py b/test/unit/test_class_registry.py new file mode 100644 index 000000000..d8b2e9453 --- /dev/null +++ b/test/unit/test_class_registry.py @@ -0,0 +1,103 @@ +###################################################################### +# +# File: test/unit/test_class_registry.py +# +# Copyright 2026 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +import pytest + +from b2._internal.class_registry import ClassRegistry, RegistryKeyError + + +def test_register_with_key_and_get_instance(): + registry = ClassRegistry() + + @registry.register('shell') + class ShellInstaller: + def __init__(self, prog: str) -> None: + self.prog = prog + + instance = registry.get('shell', 'b2') + + assert isinstance(instance, ShellInstaller) + assert instance.prog == 'b2' + + instance_kwargs = registry.get('shell', prog='b2-kw') + assert isinstance(instance_kwargs, ShellInstaller) + assert instance_kwargs.prog == 'b2-kw' + + +def test_register_with_attr_name(): + registry = ClassRegistry(attr_name='COMMAND_NAME') + + @registry.register + class ListBuckets: + COMMAND_NAME = 'list-buckets' + + assert 'list-buckets' in registry + assert isinstance(registry.get('list-buckets'), ListBuckets) + + with pytest.raises(AttributeError): + + @registry.register + class MissingKey: + pass + + +def test_register_requires_key_without_attr_name(): + registry = ClassRegistry() + + with pytest.raises(ValueError): + + @registry.register + class MissingKey: + pass + + +def test_duplicate_key_overrides(): + registry = ClassRegistry() + + @registry.register('dup') + class First: + pass + + @registry.register('dup') + class Second: + pass + + assert registry.get('dup').__class__ is Second + + +def test_duplicate_key_raises_when_unique(): + registry = ClassRegistry(unique=True) + + @registry.register('dup') + class First: + pass + + with pytest.raises(RegistryKeyError): + + @registry.register('dup') + class Second: + pass + + +def test_missing_key_raises_registry_key_error(): + registry = ClassRegistry() + + with pytest.raises(RegistryKeyError): + registry.get('missing') + + +def test_empty_key_is_rejected(): + registry = ClassRegistry() + + with pytest.raises(ValueError): + + @registry.register('') + class EmptyKey: + pass