diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 28052a5..0000000 --- a/.flake8 +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -exclude = .tox, .git, __pycache__, .cache, build, dist, *.pyc, *.egg-info, .eggs -# Error codes: -# - https://flake8.pycqa.org/en/latest/user/error-codes.html -# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes -# - https://github.com/PyCQA/flake8-bugbear#list-of-warnings -# -# E203: whitespace before `,`, `;` or `:` -# E402: module level import not at top of file -# E501: line too long -# W503: line break before binary operator -ignore = - E203, - E402, - E501, - W503 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad50ca7..9f8d1c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,10 @@ repos: -- repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.9 hooks: - - id: pyupgrade - args: [--py38-plus] -- repo: https://github.com/asottile/reorder_python_imports - rev: v3.13.0 - hooks: - - id: reorder-python-imports - args: [--py38-plus] -- repo: https://github.com/psf/black - rev: "23.12.1" - hooks: - - id: black - args: [--safe, --quiet] -- repo: https://github.com/pycqa/flake8 - rev: "7.1.0" - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==24.4.26 - - flake8-comprehensions==3.14.0 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.41.0 hooks: diff --git a/metsrw/__init__.py b/metsrw/__init__.py index 9bb624d..6ca38e1 100644 --- a/metsrw/__init__.py +++ b/metsrw/__init__.py @@ -1,10 +1,11 @@ """METS reader and writer.""" + import logging from . import plugins from .di import Dependency -from .di import feature_broker from .di import FeatureBroker +from .di import feature_broker from .di import has_class_methods from .di import has_methods from .di import is_class @@ -20,18 +21,18 @@ from .metadata import SubSection from .mets import METSDocument from .utils import FILE_ID_PREFIX -from .utils import generate_mdtype_key from .utils import GROUP_ID_PREFIX -from .utils import lxmlns from .utils import NAMESPACES from .utils import SCHEMA_LOCATIONS +from .utils import generate_mdtype_key +from .utils import lxmlns from .utils import urldecode from .utils import urlencode from .validate import AM_PNTR_SCT_PATH from .validate import AM_SCT_PATH +from .validate import METS_XSD_PATH from .validate import get_schematron from .validate import get_xmlschema -from .validate import METS_XSD_PATH from .validate import report_string from .validate import schematron_validate from .validate import sct_report_string diff --git a/metsrw/di.py b/metsrw/di.py index 281ffb8..d3f2fdf 100644 --- a/metsrw/di.py +++ b/metsrw/di.py @@ -18,6 +18,7 @@ See http://code.activestate.com/recipes/413268/ """ + from .plugins import premisrw @@ -39,9 +40,9 @@ def provide(self, feature_name, provider, *args, **kwargs): provider if it is callable. """ if not self.allow_replace: - assert feature_name not in self.providers, "Duplicate feature: {!r}".format( - feature_name - ) + assert ( + feature_name not in self.providers + ), f"Duplicate feature: {feature_name!r}" if callable(provider) and not isinstance(provider, type): self.providers[feature_name] = lambda: provider(*args, **kwargs) else: @@ -96,8 +97,8 @@ def __get__(self, instance, owner): obj = feature_broker[self.dependency_name] for assertion in self.assertions: assert assertion(obj), ( - "The value {!r} of {!r} does not match the specified" - " criteria".format(obj, self.dependency_name) + f"The value {obj!r} of {self.dependency_name!r} does not match the specified" + " criteria" ) return obj diff --git a/metsrw/fsentry.py b/metsrw/fsentry.py index b7f04bf..594e254 100644 --- a/metsrw/fsentry.py +++ b/metsrw/fsentry.py @@ -184,12 +184,10 @@ def from_fptr(cls, label, type_, fptr): ) def __str__(self): - return "{s.type}: {s.path}".format(s=self) + return f"{self.type}: {self.path}" def __repr__(self): - return "FSEntry(type={s.type!r}, path={s.path!r}, use={s.use!r}, label={s.label!r}, file_uuid={s.file_uuid!r}, checksum={s.checksum!r}, checksumtype={s.checksumtype!r}, fileid={s._fileid!r})".format( - s=self - ) + return f"FSEntry(type={self.type!r}, path={self.path!r}, use={self.use!r}, label={self.label!r}, file_uuid={self.file_uuid!r}, checksum={self.checksum!r}, checksumtype={self.checksumtype!r}, fileid={self._fileid!r})" # PROPERTIES @@ -439,8 +437,8 @@ def serialize_filesec(self): flocat.set(utils.lxmlns("xlink") + "href", utils.urlencode(self.path)) except ValueError: raise exceptions.SerializeError( - 'Value "{}" (for attribute xlink:href) is not a valid' - " URL.".format(self.path) + f'Value "{self.path}" (for attribute xlink:href) is not a valid' + " URL." ) flocat.set("LOCTYPE", "OTHER") flocat.set("OTHERLOCTYPE", "SYSTEM") diff --git a/metsrw/metadata.py b/metsrw/metadata.py index 51755b6..8f58dca 100644 --- a/metsrw/metadata.py +++ b/metsrw/metadata.py @@ -1,6 +1,7 @@ """ Classes for metadata sections of the METS. Include amdSec, dmdSec, techMD, rightsMD, sourceMD, digiprovMD, mdRef and mdWrap. """ + import copy import logging @@ -9,7 +10,6 @@ from . import exceptions from . import utils - LOGGER = logging.getLogger(__name__) @@ -142,9 +142,7 @@ def parse(cls, element): """ if element.tag != cls.ALT_RECORD_ID_TAG: raise exceptions.ParseError( - "AltRecordID got unexpected tag {}; expected {}".format( - element.tag, cls.ALT_RECORD_ID_TAG - ) + f"AltRecordID got unexpected tag {element.tag}; expected {cls.ALT_RECORD_ID_TAG}" ) return cls(element.text, id=element.get("ID"), type=element.get("TYPE")) @@ -211,9 +209,7 @@ def parse(cls, element): """ if element.tag != cls.AGENT_TAG: raise exceptions.ParseError( - "Agent got unexpected tag {}; expected {}".format( - element.tag, cls.AGENT_TAG - ) + f"Agent got unexpected tag {element.tag}; expected {cls.AGENT_TAG}" ) role = element.get("ROLE") @@ -477,8 +473,7 @@ def parse(cls, root): target = utils.urldecode(target) except ValueError: raise exceptions.ParseError( - 'Value "{}" (of attribute xlink:href) is not a valid' - " URL.".format(target) + f'Value "{target}" (of attribute xlink:href) is not a valid' " URL." ) loctype = root.get("LOCTYPE") if not loctype: @@ -516,8 +511,8 @@ def serialize(self): el.attrib[utils.lxmlns("xlink") + "href"] = utils.urlencode(self.target) except ValueError: raise exceptions.SerializeError( - 'Value "{}" (for attribute xlink:href) is not a valid' - " URL.".format(self.target) + f'Value "{self.target}" (for attribute xlink:href) is not a valid' + " URL." ) el.attrib["MDTYPE"] = self.mdtype el.attrib["LOCTYPE"] = self.loctype diff --git a/metsrw/mets.py b/metsrw/mets.py index 890234b..9d16446 100755 --- a/metsrw/mets.py +++ b/metsrw/mets.py @@ -1,8 +1,8 @@ import logging import os import sys -from collections import namedtuple from collections import OrderedDict +from collections import namedtuple from datetime import datetime from lxml import etree @@ -12,7 +12,6 @@ from . import metadata from . import utils - LOGGER = logging.getLogger(__name__) AIP_ENTRY_TYPE = "archival information package" @@ -485,8 +484,7 @@ def _analyze_fptr(fptr_elem, tree, entry_type): path = utils.urldecode(path) except ValueError: raise exceptions.ParseError( - 'Value "{}" (of attribute xlink:href) is not a valid' - " URL.".format(path) + f'Value "{path}" (of attribute xlink:href) is not a valid' " URL." ) amdids = file_elem.get("ADMID") dmdids = file_elem.get("DMDID") diff --git a/metsrw/plugins/premisrw/__init__.py b/metsrw/plugins/premisrw/__init__.py index b5a63a8..3b92686 100644 --- a/metsrw/plugins/premisrw/__init__.py +++ b/metsrw/plugins/premisrw/__init__.py @@ -1,19 +1,18 @@ """PREMIS reader and writer.""" + import logging +from .premis import PREMISAgent +from .premis import PREMISElement +from .premis import PREMISEvent +from .premis import PREMISObject +from .premis import PREMISRights from .premis import data_find from .premis import data_find_all from .premis import data_find_text from .premis import data_find_text_or_all from .premis import data_to_premis from .premis import premis_to_data -from .premis import PREMISAgent -from .premis import PREMISElement -from .premis import PREMISEvent -from .premis import PREMISObject -from .premis import PREMISRights -from .utils import camel_to_snake -from .utils import lxmlns from .utils import NAMESPACES from .utils import PREMIS_2_1_META from .utils import PREMIS_2_1_NAMESPACE @@ -37,10 +36,11 @@ from .utils import PREMIS_SCHEMA_LOCATION from .utils import PREMIS_VERSION from .utils import PREMIS_VERSIONS_MAP +from .utils import XSI_NAMESPACE +from .utils import camel_to_snake +from .utils import lxmlns from .utils import snake_to_camel from .utils import snake_to_camel_cap -from .utils import XSI_NAMESPACE - LOGGER = logging.getLogger(__name__) LOGGER.addHandler(logging.NullHandler()) diff --git a/metsrw/plugins/premisrw/premis.py b/metsrw/plugins/premisrw/premis.py index bd1ea5d..abd845e 100644 --- a/metsrw/plugins/premisrw/premis.py +++ b/metsrw/plugins/premisrw/premis.py @@ -9,6 +9,7 @@ - PREMISRights """ + import abc import json import pprint @@ -183,8 +184,8 @@ def __getattr__(self, attr_name): ) ) raise AttributeError( - "Instance of {} has no attribute {}. Valid attributes" - " are\n{}".format(self.__class__, attr_name, valid_attributes) + f"Instance of {self.__class__} has no attribute {attr_name}. Valid attributes" + f" are\n{valid_attributes}" ) def find(self, path): @@ -391,8 +392,7 @@ def compression_details(self): event_type = self.findtext("event_type") if event_type != "compression": raise AttributeError( - 'PREMIS events of type "{}" have no compression' - " details".format(event_type) + f'PREMIS events of type "{event_type}" have no compression' " details" ) parsed_compression_event_detail = self.parsed_event_detail compression_program = _get_event_detail_attr( @@ -435,8 +435,7 @@ def encryption_details(self): event_type = self.findtext("event_type") if event_type != "encryption": raise AttributeError( - 'PREMIS events of type "{}" have no encryption' - " details".format(event_type) + f'PREMIS events of type "{event_type}" have no encryption' " details" ) parsed_encryption_event_detail = self.parsed_event_detail encryption_program = _get_event_detail_attr( @@ -846,11 +845,7 @@ def _get_event_detail_attr(attr, parsed_event_detail): try: return parsed_event_detail[attr] except KeyError: - print( - "Unable to find attribute {} in event detail {}".format( - attr, parsed_event_detail - ) - ) + print(f"Unable to find attribute {attr} in event detail {parsed_event_detail}") return "No value found" diff --git a/metsrw/utils.py b/metsrw/utils.py index 9af873f..6bc9c51 100644 --- a/metsrw/utils.py +++ b/metsrw/utils.py @@ -3,7 +3,6 @@ from urllib.parse import urlparse from urllib.parse import urlunparse - #################################### # LXML HELPER VALUES AND FUNCTIONS # #################################### diff --git a/pyproject.toml b/pyproject.toml index ea843eb..943ad69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ dev = [ "pip-tools", "pytest-cov", "pytest", + "ruff", "sphinx-rtd-theme", "sphinx==7.1.2", "sphinxcontrib-applehelp==1.0.4", @@ -72,6 +73,26 @@ dev = [ version = {attr = "metsrw.__version__"} readme = {file = ["README.md"], content-type = "text/markdown"} +[tool.ruff.lint] +# Rule reference: https://docs.astral.sh/ruff/rules/ +select = [ + "B", + "C4", + "E", + "F", + "I", + "UP", + "W", +] +ignore = [ + "B904", + "E501", + "UP031", +] + +[tool.ruff.lint.isort] +force-single-line = true + [tool.pytest.ini_options] python_files = [ "test_*.py", diff --git a/requirements-dev.txt b/requirements-dev.txt index e4ce22f..c3b2d99 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -67,6 +67,8 @@ pytest-cov==5.0.0 # via metsrw (pyproject.toml) requests==2.32.3 # via sphinx +ruff==0.4.9 + # via metsrw (pyproject.toml) snowballstemmer==2.2.0 # via sphinx sphinx==7.1.2 diff --git a/tests/constants.py b/tests/constants.py index 8245732..fea2854 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -2,7 +2,6 @@ import metsrw.plugins.premisrw as premisrw - EX_AGT_1_IDENTIFIER_TYPE = "preservation system" EX_AGT_1_IDENTIFIER_VALUE = "Archivematica-1.6.1" EX_AGT_1_NAME = "Archivematica" diff --git a/tests/plugins/premisrw/test_premis.py b/tests/plugins/premisrw/test_premis.py index 4176e72..9123ee4 100644 --- a/tests/plugins/premisrw/test_premis.py +++ b/tests/plugins/premisrw/test_premis.py @@ -173,9 +173,7 @@ def test_full_pointer_file(self): mw.serialize(), schematron=metsrw.AM_PNTR_SCT_PATH ) if not is_valid: - print( - "Pointer file is NOT" " valid.\n{}".format(metsrw.report_string(report)) - ) + print("Pointer file is NOT" f" valid.\n{metsrw.report_string(report)}") assert is_valid def test_pointer_file_read(self): @@ -289,14 +287,15 @@ def test_dynamic_attrs(self): # A partial path to a leaf element is not a valid accessor: with pytest.raises(AttributeError): - premis_object.fixity__message_digest + assert premis_object.fixity__message_digest # XML attribute accessors assert premis_object.xsi_type == c.EX_PTR_XSI_TYPE # namespaced assert premis_object.xsi__type == c.EX_PTR_XSI_TYPE # namespaced assert premis_object.type == c.EX_PTR_XSI_TYPE # not namespaced - assert premis_object.xsi_schema_location == ( - premisrw.PREMIS_META["xsi:schema_location"] + assert ( + premis_object.xsi_schema_location + == (premisrw.PREMIS_META["xsi:schema_location"]) ) assert compression_event.event_type == c.EX_COMPR_EVT_TYPE @@ -318,7 +317,7 @@ def test_dynamic_attrs(self): == c.EX_AGT_1_IDENTIFIER_TYPE ) with pytest.raises(AttributeError): - premis_agent_1.agent_identifier__agent_name + assert premis_agent_1.agent_identifier__agent_name def test_encryption_event(self): encryption_event = premisrw.PREMISEvent(data=c.EX_ENCR_EVT) diff --git a/tests/test_dependency_injection.py b/tests/test_dependency_injection.py index c2c2f78..a6d8c1a 100644 --- a/tests/test_dependency_injection.py +++ b/tests/test_dependency_injection.py @@ -2,6 +2,7 @@ import metsrw import metsrw.plugins.premisrw as premisrw + from .constants import EX_AGT_1 from .constants import EX_AGT_2 from .constants import EX_COMPR_EVT diff --git a/tests/test_metadata.py b/tests/test_metadata.py index fc7a9f0..c36f284 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -110,7 +110,6 @@ def test_identifier(self): amdsec_ids = [metsrw.AMDSec().id_string for _ in range(10)] # Generate a SubSection in between to make sure our count # doesn't jump - metsrw.SubSection("techMD", []).id_string amdsec_ids.append(metsrw.AMDSec().id_string) for index, amdsec_id in enumerate(amdsec_ids, 1): diff --git a/tests/test_mets.py b/tests/test_mets.py index 17ef471..82f3339 100644 --- a/tests/test_mets.py +++ b/tests/test_mets.py @@ -4,8 +4,8 @@ import os import tempfile import uuid -from unittest import mock from unittest import TestCase +from unittest import mock import pytest from lxml import etree @@ -856,9 +856,9 @@ def test_pointer_file(self): version="2.2", ) aip_premis_object.attrib["{" + nsmap["xsi"] + "}type"] = "premis:file" - aip_premis_object.attrib[ - "{" + nsmap["xsi"] + "}schemaLocation" - ] = premis_schema_location + aip_premis_object.attrib["{" + nsmap["xsi"] + "}schemaLocation"] = ( + premis_schema_location + ) aip_fs_entry.add_premis_object(aip_premis_object) # Create the AIP's PREMIS:EVENT for the compression using raw lxml @@ -885,9 +885,9 @@ def test_pointer_file(self): ], version="2.2", ) - aip_premis_compression_event.attrib[ - "{" + nsmap["xsi"] + "}schemaLocation" - ] = premis_schema_location + aip_premis_compression_event.attrib["{" + nsmap["xsi"] + "}schemaLocation"] = ( + premis_schema_location + ) aip_fs_entry.add_premis_event(aip_premis_compression_event) # Create the AIP's PREMIS:AGENTs using raw lxml @@ -900,9 +900,9 @@ def test_pointer_file(self): E_P.agentName(agent["name"]), E_P.agentType(agent["type"]), ) - agent_el.attrib[ - "{" + nsmap["xsi"] + "}schemaLocation" - ] = premis_schema_location + agent_el.attrib["{" + nsmap["xsi"] + "}schemaLocation"] = ( + premis_schema_location + ) aip_fs_entry.add_premis_agent(agent_el) mw.append_file(aip_fs_entry) diff --git a/tests/test_normative_structmap.py b/tests/test_normative_structmap.py index 5dee3d1..d821e41 100644 --- a/tests/test_normative_structmap.py +++ b/tests/test_normative_structmap.py @@ -5,15 +5,16 @@ are absent in the standard physical structural map. """ + import uuid from unittest import TestCase from lxml import etree import metsrw -from metsrw.plugins.premisrw import lxmlns from metsrw.plugins.premisrw import PREMIS_3_0_NAMESPACES from metsrw.plugins.premisrw import PREMISObject +from metsrw.plugins.premisrw import lxmlns class TestNormativeStructMap(TestCase): diff --git a/tests/test_utils.py b/tests/test_utils.py index ad11fc7..dbab164 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,6 @@ import metsrw - GOOD_PATHS_SLASH_URLS = ( "30_CFLQ_271_13-3-13_1524[1].pdf", "30/CFLQ_271_13-3-13_1524[1].pdf",