diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b37ba86..ed2dfcc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,17 +20,14 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip setuptools wheel
- pip install coverage
- pip install --editable .
- - name: Run test
- run: python -m unittest discover
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ - name: Run tests
+ run: uv run pytest
diff --git a/.gitignore b/.gitignore
index 1f4b930..333e320 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ dist
MANIFEST
bagit.egg-info
.idea
+uv.lock
+*.log
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9f1dc32..31dc8e5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,19 +2,19 @@ exclude: ".*test-data.*"
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.6.9
+ rev: v0.15.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: check-added-large-files
args: ["--maxkb=128"]
- id: check-ast
- - id: check-byte-order-marker
+ - id: fix-byte-order-marker
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index db7a199..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,7 +0,0 @@
-prune test-data
-exclude .*
-exclude Dockerfile
-exclude MANIFEST.in
-exclude test.py
-exclude bench.py
-recursive-include locale *.po *.mo
diff --git a/Makefile b/Makefile
index 9907ac7..17b0319 100644
--- a/Makefile
+++ b/Makefile
@@ -3,13 +3,13 @@ COMPILED_MESSAGES=$(patsubst %.po,%.mo, $(wildcard locale/*/LC_MESSAGES/bagit-py
all: messages compile
clean:
- rm -f locale/*/LC_MESSAGES/*.mo
+ rm -f src/bagit/locale/*/LC_MESSAGES/*.mo
messages:
- xgettext --language=python -d bagit-python --no-location -o locale/bagit-python.pot bagit.py
+ xgettext --language=python -d bagit-python --no-location -o src/bagit/locale/bagit-python.pot src/bagit/__init__.py
# Until http://savannah.gnu.org/bugs/?20923 is fixed:
- sed -i '' -e 's/CHARSET/UTF-8/g' locale/bagit-python.pot
- msgmerge --no-fuzzy-matching --lang=en --output-file=locale/en/LC_MESSAGES/bagit-python.po locale/en/LC_MESSAGES/bagit-python.po locale/bagit-python.pot
+ sed -i '' -e 's/CHARSET/UTF-8/g' src/bagit/locale/bagit-python.pot
+ msgmerge --no-fuzzy-matching --lang=en --output-file=src/bagit/locale/en/LC_MESSAGES/bagit-python.po src/bagit/locale/en/LC_MESSAGES/bagit-python.po src/bagit/locale/bagit-python.pot
%.mo: %.po
msgfmt -o $@ $<
diff --git a/README.rst b/README.rst
index ed33262..1ad7710 100644
--- a/README.rst
+++ b/README.rst
@@ -219,23 +219,23 @@ Contributing to bagit-python development
% git clone git://github.com/LibraryOfCongress/bagit-python.git
% cd bagit-python
# MAKE CHANGES
- % python test.py
+ % uv run pytest
Running the tests
~~~~~~~~~~~~~~~~~
-You can quickly run the tests using the built-in unittest framework:
+You can quickly run the tests using `uv `_.
::
- python -m unittest discover
+ uv run pytest
If you have Docker installed, you can run the tests under Linux inside a
container:
::
- % docker build -t bagit:latest . && docker run -it bagit:latest
+ docker build -t bagit:latest . && docker run -it bagit:latest
Benchmarks
----------
@@ -246,7 +246,7 @@ bench utility:
::
- % ./bench.py
+ ./utils/bench.py
License
-------
diff --git a/pyproject.toml b/pyproject.toml
index 7cd2d39..937984a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[build-system]
-requires = ["setuptools>=64", "setuptools-scm>=8"]
-build-backend = "setuptools.build_meta"
+requires = ["hatchling", "hatch-vcs"]
+build-backend = "hatchling.build"
[project]
name = "bagit"
@@ -8,7 +8,8 @@ dynamic = ["version"]
description = "Create and validate BagIt packages"
readme = {file = "README.rst", content-type = "text/x-rst"}
authors = [
- { name = "Ed Summers", email = "ehs@pobox.com" },
+ { name = "Chris Adams", email = "chris@improbable.org" },
+ { name = "Ed Summers", email = "ehs@pobox.com" }
]
classifiers = [
"Intended Audience :: Developers",
@@ -18,6 +19,10 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Filesystems",
]
+requires-python = ">= 3.9"
+
+[project.scripts]
+"bagit" = "bagit:main"
[project.urls]
Homepage = "https://libraryofcongress.github.io/bagit-python/"
@@ -27,14 +32,34 @@ Homepage = "https://libraryofcongress.github.io/bagit-python/"
[tool.ruff]
target-version = "py38"
-
-[tool.setuptools_scm]
-
[tool.isort]
line_length = 110
default_section = "THIRDPARTY"
known_first_party = "bagit"
+[tool.pytest.ini_options]
+testpaths = ["test.py"]
+addopts = "-v"
+
[tool.coverage.run]
branch = true
-include = "bagit.py"
+include = "src"
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.build.targets.sdist]
+packages = ["src/bagit"]
+include = [
+ "src/bagit/*.py",
+ "src/bagit/locale/*.po",
+ "src/bagit/locale/*.mo",
+]
+
+
+[dependency-groups]
+dev = [
+ "coverage>=7.8.2",
+ "pytest>=8.4.0",
+ "ruff>=0.11.12",
+]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 15c91b4..0000000
--- a/setup.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-
-from __future__ import absolute_import, print_function
-
-import glob
-import os
-import subprocess
-import sys
-
-from setuptools import setup
-
-description = "Create and validate BagIt packages"
-
-
-def get_message_catalogs():
- message_catalogs = []
-
- for po_file in glob.glob("locale/*/LC_MESSAGES/bagit-python.po"):
- mo_file = po_file.replace(".po", ".mo")
-
- if not os.path.exists(mo_file) or os.path.getmtime(mo_file) < os.path.getmtime(
- po_file
- ):
- try:
- subprocess.check_call(["msgfmt", "-o", mo_file, po_file])
- except (OSError, subprocess.CalledProcessError) as exc:
- print(
- "Translation catalog %s could not be compiled (is gettext installed?) "
- " — translations will not be available for this language: %s"
- % (po_file, exc),
- file=sys.stderr,
- )
- continue
-
- message_catalogs.append((os.path.dirname(mo_file), (mo_file,)))
-
- return message_catalogs
-
-
-setup(
- name="bagit",
- use_scm_version=True,
- url="https://libraryofcongress.github.io/bagit-python/",
- author="Ed Summers",
- author_email="ehs@pobox.com",
- py_modules=["bagit"],
- scripts=["bagit.py"],
- data_files=get_message_catalogs(),
- description=description,
- platforms=["POSIX"],
- setup_requires=["setuptools_scm"],
- classifiers=[
- "License :: Public Domain",
- "Intended Audience :: Developers",
- "Topic :: Communications :: File Sharing",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: System :: Filesystems",
- "Programming Language :: Python :: 3",
- ],
-)
diff --git a/bagit.py b/src/bagit/__init__.py
similarity index 98%
rename from bagit.py
rename to src/bagit/__init__.py
index eab85d3..c17b1f2 100755
--- a/bagit.py
+++ b/src/bagit/__init__.py
@@ -236,7 +236,11 @@ def make_bag(
break
except PermissionError as e:
if hasattr(e, "winerror") and e.winerror == 5:
- LOGGER.warning(_("PermissionError [WinError 5] when renaming temp folder. Retrying in 10 seconds..."))
+ LOGGER.warning(
+ _(
+ "PermissionError [WinError 5] when renaming temp folder. Retrying in 10 seconds..."
+ )
+ )
time.sleep(10)
else:
raise
@@ -250,7 +254,7 @@ def make_bag(
)
LOGGER.info(_("Creating bagit.txt"))
- txt = """BagIt-Version: 0.97\nTag-File-Character-Encoding: UTF-8\n"""
+ txt = """BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n"""
with open_text_file("bagit.txt", "w") as bagit_file:
bagit_file.write(txt)
@@ -946,7 +950,7 @@ def _path_is_dangerous(self, path):
real_path = os.path.normpath(real_path)
bag_path = os.path.realpath(self.path)
bag_path = os.path.normpath(bag_path)
- common = os.path.commonprefix((bag_path, real_path))
+ common = os.path.commonpath((bag_path, real_path))
return not (common == bag_path)
@@ -1124,12 +1128,12 @@ def _calc_hashes(args):
full_path = os.path.join(base_path, rel_path)
# Create a clone of the default empty hash objects:
- f_hashers = dict((alg, hashlib.new(alg)) for alg in hashes if alg in algorithms)
+ f_hashers = {alg: hashlib.new(alg) for alg in hashes if alg in algorithms}
try:
f_hashes = _calculate_file_hashes(full_path, f_hashers)
except BagValidationError as e:
- f_hashes = dict((alg, str(e)) for alg in f_hashers.keys())
+ f_hashes = {alg: str(e) for alg in f_hashers}
return rel_path, f_hashes, hashes
@@ -1489,10 +1493,7 @@ def _make_parser():
checksum_args = parser.add_argument_group(
_("Checksum Algorithms"),
- _(
- "Select the manifest algorithms to be used when creating bags"
- " (default=%s)"
- )
+ _("Select the manifest algorithms to be used when creating bags (default=%s)")
% ", ".join(DEFAULT_CHECKSUMS),
)
diff --git a/locale/bagit-python.pot b/src/bagit/locale/bagit-python.pot
similarity index 96%
rename from locale/bagit-python.pot
rename to src/bagit/locale/bagit-python.pot
index 3543c31..85bf167 100644
--- a/locale/bagit-python.pot
+++ b/src/bagit/locale/bagit-python.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-06-26 10:28-0400\n"
+"POT-Creation-Date: 2025-06-05 12:14-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -226,20 +226,23 @@ msgid ""
"%(found_byte_count)d bytes"
msgstr ""
-msgid "Bag validation failed"
+msgid "Bag is incomplete"
msgstr ""
#, python-format
msgid "Unable to calculate file hashes for %s"
msgstr ""
+msgid "Bag validation failed"
+msgstr ""
+
msgid "bagit.txt must not contain a byte-order mark"
msgstr ""
#, python-format
msgid ""
-"%(path)s %(algorithm)s validation failed: expected=\"%(expected)s\" found="
-"\"%(found)s\""
+"%(path)s %(algorithm)s validation failed: expected=\"%(expected)s\" "
+"found=\"%(found)s\""
msgstr ""
#, python-format
@@ -345,16 +348,23 @@ msgstr ""
msgid "bagit-python version %s"
msgstr ""
-msgid "The number of processes must be 0 or greater"
+msgid "The number of processes must be greater than 0"
msgstr ""
msgid "--fast is only allowed as an option for --validate!"
msgstr ""
+msgid "--completeness-only is only allowed as an option for --validate!"
+msgstr ""
+
#, python-format
msgid "%s valid according to Payload-Oxum"
msgstr ""
+#, python-format
+msgid "%s is complete and valid according to Payload-Oxum"
+msgstr ""
+
#, python-format
msgid "%s is valid"
msgstr ""
diff --git a/locale/bagit.pot b/src/bagit/locale/bagit.pot
similarity index 100%
rename from locale/bagit.pot
rename to src/bagit/locale/bagit.pot
diff --git a/locale/en/LC_MESSAGES/bagit-python.po b/src/bagit/locale/en/LC_MESSAGES/bagit-python.po
similarity index 96%
rename from locale/en/LC_MESSAGES/bagit-python.po
rename to src/bagit/locale/en/LC_MESSAGES/bagit-python.po
index ddf0f3f..8767e56 100644
--- a/locale/en/LC_MESSAGES/bagit-python.po
+++ b/src/bagit/locale/en/LC_MESSAGES/bagit-python.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-06-26 10:28-0400\n"
+"POT-Creation-Date: 2025-06-05 12:14-0400\n"
"PO-Revision-Date: 2017-04-27 15:02-0400\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -233,20 +233,23 @@ msgid ""
"%(found_byte_count)d bytes"
msgstr ""
-msgid "Bag validation failed"
+msgid "Bag is incomplete"
msgstr ""
#, python-format
msgid "Unable to calculate file hashes for %s"
msgstr "Unable to calculate file hashes for %s"
+msgid "Bag validation failed"
+msgstr ""
+
msgid "bagit.txt must not contain a byte-order mark"
msgstr ""
#, python-format
msgid ""
-"%(path)s %(algorithm)s validation failed: expected=\"%(expected)s\" found="
-"\"%(found)s\""
+"%(path)s %(algorithm)s validation failed: expected=\"%(expected)s\" "
+"found=\"%(found)s\""
msgstr ""
#, python-format
@@ -352,16 +355,23 @@ msgstr ""
msgid "bagit-python version %s"
msgstr ""
-msgid "The number of processes must be 0 or greater"
+msgid "The number of processes must be greater than 0"
msgstr ""
msgid "--fast is only allowed as an option for --validate!"
msgstr ""
+msgid "--completeness-only is only allowed as an option for --validate!"
+msgstr ""
+
#, python-format
msgid "%s valid according to Payload-Oxum"
msgstr "%s valid according to Payload-Oxum"
+#, python-format
+msgid "%s is complete and valid according to Payload-Oxum"
+msgstr ""
+
#, python-format
msgid "%s is valid"
msgstr "%s is valid"
diff --git a/test.py b/test.py
index 735bd73..f79bca0 100644
--- a/test.py
+++ b/test.py
@@ -523,7 +523,7 @@ def test_make_bag(self):
tagmanifest_txt = slurp_text_file(
j(self.tmpdir, "tagmanifest-md5.txt")
).splitlines()
- self.assertIn("9e5ad981e0d29adc278f6a294b8c2aca bagit.txt", tagmanifest_txt)
+ self.assertIn("eaa2c609ff6371712f623f5531945b44 bagit.txt", tagmanifest_txt)
self.assertIn(
"a0ce6631a2a6d1a88e6d38453ccc72a5 manifest-md5.txt", tagmanifest_txt
)
@@ -1157,10 +1157,9 @@ def test_invalid_fast_validate(self):
os.remove(j(self.tmpdir, "data", "loc", "2478433644_2839c5e8b8_o_d.jpg"))
testargs = ["bagit.py", "--validate", "--completeness-only", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
@@ -1172,10 +1171,9 @@ def test_valid_fast_validate(self):
bagit.make_bag(self.tmpdir)
testargs = ["bagit.py", "--validate", "--fast", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 0)
self.assertEqual(
@@ -1206,10 +1204,9 @@ def test_invalid_completeness_validate(self):
testargs = ["bagit.py", "--validate", "--completeness-only", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
@@ -1221,10 +1218,9 @@ def test_valid_completeness_validate(self):
bagit.make_bag(self.tmpdir)
testargs = ["bagit.py", "--validate", "--completeness-only", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 0)
self.assertEqual(
@@ -1242,10 +1238,9 @@ def test_invalid_full_validate(self):
testargs = ["bagit.py", "--validate", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("Bag validation failed", captured.records[-1].getMessage())
@@ -1254,10 +1249,9 @@ def test_valid_full_validate(self):
bagit.make_bag(self.tmpdir)
testargs = ["bagit.py", "--validate", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 0)
self.assertEqual("%s is valid" % self.tmpdir, captured.records[-1].getMessage())
@@ -1267,10 +1261,9 @@ def test_failed_create_bag(self):
testargs = ["bagit.py", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
@@ -1281,10 +1274,9 @@ def test_failed_create_bag(self):
def test_create_bag(self):
testargs = ["bagit.py", self.tmpdir]
- with self.assertLogs() as captured:
- with self.assertRaises(SystemExit) as cm:
- with mock.patch.object(sys, "argv", testargs):
- bagit.main()
+ with self.assertLogs() as captured, self.assertRaises(SystemExit) as cm:
+ with mock.patch.object(sys, "argv", testargs):
+ bagit.main()
for rec in captured.records:
print(rec.getMessage())
diff --git a/bench.py b/utils/bench.py
similarity index 100%
rename from bench.py
rename to utils/bench.py
diff --git a/utils/locales.py b/utils/locales.py
new file mode 100755
index 0000000..aaed193
--- /dev/null
+++ b/utils/locales.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+import sys
+import subprocess
+
+from pathlib import Path
+
+src_dir = Path(__file__).parent.parent / "src"
+
+for po_file in src_dir.glob("bagit/locale/*/LC_MESSAGES/bagit-python.po"):
+ mo_file = po_file.with_suffix(".mo")
+
+ if not mo_file.is_file() or mo_file.stat().st_mtime < po_file.stat().st_mtime:
+ try:
+ print(f"compiling {po_file} to {mo_file}")
+ subprocess.check_call(["msgfmt", "-o", mo_file, po_file])
+ except (OSError, subprocess.CalledProcessError) as exc:
+ print(
+ "Translation catalog %s could not be compiled (is gettext installed?) "
+ " — translations will not be available for this language: %s"
+ % (po_file, exc),
+ file=sys.stderr,
+ )
+ continue