diff --git a/borgmatic/borg/feature.py b/borgmatic/borg/feature.py index 5294121d0..b9311cd13 100644 --- a/borgmatic/borg/feature.py +++ b/borgmatic/borg/feature.py @@ -1,6 +1,6 @@ from enum import Enum -from pkg_resources import parse_version +from packaging.version import parse class Feature(Enum): @@ -18,17 +18,17 @@ class Feature(Enum): FEATURE_TO_MINIMUM_BORG_VERSION = { - Feature.COMPACT: parse_version('1.2.0a2'), # borg compact - Feature.ATIME: parse_version('1.2.0a7'), # borg create --atime - Feature.NOFLAGS: parse_version('1.2.0a8'), # borg create --noflags - Feature.NUMERIC_IDS: parse_version('1.2.0b3'), # borg create/extract/mount --numeric-ids - Feature.UPLOAD_RATELIMIT: parse_version('1.2.0b3'), # borg create --upload-ratelimit - Feature.SEPARATE_REPOSITORY_ARCHIVE: parse_version('2.0.0a2'), # --repo with separate archive - Feature.RCREATE: parse_version('2.0.0a2'), # borg rcreate - Feature.RLIST: parse_version('2.0.0a2'), # borg rlist - Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo - Feature.MATCH_ARCHIVES: parse_version('2.0.0b3'), # borg --match-archives - Feature.EXCLUDED_FILES_MINUS: parse_version('2.0.0b5'), # --list --filter uses "-" for excludes + Feature.COMPACT: parse('1.2.0a2'), # borg compact + Feature.ATIME: parse('1.2.0a7'), # borg create --atime + Feature.NOFLAGS: parse('1.2.0a8'), # borg create --noflags + Feature.NUMERIC_IDS: parse('1.2.0b3'), # borg create/extract/mount --numeric-ids + Feature.UPLOAD_RATELIMIT: parse('1.2.0b3'), # borg create --upload-ratelimit + Feature.SEPARATE_REPOSITORY_ARCHIVE: parse('2.0.0a2'), # --repo with separate archive + Feature.RCREATE: parse('2.0.0a2'), # borg rcreate + Feature.RLIST: parse('2.0.0a2'), # borg rlist + Feature.RINFO: parse('2.0.0a2'), # borg rinfo + Feature.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives + Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes } @@ -37,4 +37,4 @@ def available(feature, borg_version): Given a Borg Feature constant and a Borg version string, return whether that feature is available in that version of Borg. ''' - return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse_version(borg_version) + return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse(borg_version) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index fece475a8..999e9d8e1 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -8,7 +8,11 @@ from queue import Queue from subprocess import CalledProcessError import colorama -import pkg_resources + +try: + import importlib_metadata +except ModuleNotFoundError: # pragma: nocover + import importlib.metadata as importlib_metadata import borgmatic.actions.borg import borgmatic.actions.break_lock @@ -706,7 +710,7 @@ def main(): # pragma: no cover global_arguments = arguments['global'] if global_arguments.version: - print(pkg_resources.require('borgmatic')[0].version) + print(importlib_metadata.version('borgmatic')) sys.exit(0) if global_arguments.bash_completion: print(borgmatic.commands.completion.bash_completion()) diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index fcf29d380..537f4beed 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -1,9 +1,13 @@ import os import jsonschema -import pkg_resources import ruamel.yaml +try: + import importlib_metadata +except ModuleNotFoundError: # pragma: nocover + import importlib.metadata as importlib_metadata + from borgmatic.config import environment, load, normalize, override @@ -11,8 +15,17 @@ def schema_filename(): ''' Path to the installed YAML configuration schema file, used to validate and parse the configuration. + + Raise FileNotFoundError when the schema path does not exist. ''' - return pkg_resources.resource_filename('borgmatic', 'config/schema.yaml') + try: + return next( + str(path.locate()) + for path in importlib_metadata.files('borgmatic') + if path.match('config/schema.yaml') + ) + except StopIteration: + raise FileNotFoundError('Configuration file schema could not be found') def format_json_error_path_element(path_element): diff --git a/setup.cfg b/setup.cfg index 308413866..a5ba3a6a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,8 +4,6 @@ description_file=README.md [tool:pytest] testpaths = tests addopts = --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end -filterwarnings = - ignore:Deprecated call to `pkg_resources.declare_namespace\('ruamel'\)`.*:DeprecationWarning [flake8] max-line-length = 100 diff --git a/setup.py b/setup.py index f6fca3db9..ce6b78e5f 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ setup( install_requires=( 'colorama>=0.4.1,<0.5', 'jsonschema', + 'packaging', 'requests', 'ruamel.yaml>0.15.0,<0.18.0', 'setuptools', diff --git a/test_requirements.txt b/test_requirements.txt index 67e025630..6516a5008 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -12,8 +12,10 @@ flake8-use-fstring==1.4 flake8-variables-names==0.0.5 flexmock==0.11.3 idna==3.4 +importlib_metadata==6.3.0; python_version < '3.8' isort==5.12.0 mccabe==0.7.0 +packaging==23.1 pluggy==1.0.0 pathspec==0.11.1; python_version >= '3.8' py==1.11.0 @@ -27,3 +29,5 @@ requests==2.28.2 ruamel.yaml>0.15.0,<0.18.0 toml==0.10.2; python_version >= '3.8' typed-ast; python_version >= '3.8' +typing-extensions==4.5.0; python_version < '3.8' +zipp==3.15.0; python_version < '3.8' diff --git a/tests/unit/config/test_validate.py b/tests/unit/config/test_validate.py index 24105547a..e2b9f98f8 100644 --- a/tests/unit/config/test_validate.py +++ b/tests/unit/config/test_validate.py @@ -4,6 +4,28 @@ from flexmock import flexmock from borgmatic.config import validate as module +def test_schema_filename_finds_schema_path(): + schema_path = '/var/borgmatic/config/schema.yaml' + + flexmock(module.importlib_metadata).should_receive('files').and_return( + flexmock(match=lambda path: False, locate=lambda: None), + flexmock(match=lambda path: True, locate=lambda: schema_path), + flexmock(match=lambda path: False, locate=lambda: None), + ) + + assert module.schema_filename() == schema_path + + +def test_schema_filename_with_missing_schema_path_raises(): + flexmock(module.importlib_metadata).should_receive('files').and_return( + flexmock(match=lambda path: False, locate=lambda: None), + flexmock(match=lambda path: False, locate=lambda: None), + ) + + with pytest.raises(FileNotFoundError): + assert module.schema_filename() + + def test_format_json_error_path_element_formats_array_index(): module.format_json_error_path_element(3) == '[3]'