From d859f13f745959122fd11282a339ed7024b900d5 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:02:37 -0500 Subject: [PATCH 01/12] test: add labels to tox environments --- tox.ini | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tox.ini b/tox.ini index 955db9e..faf9b8e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,8 @@ envlist = py310, py311, py312, py313, py314, py314t min_version = 4.11 [testenv] +labels = + test pass_env = CONAN_USER_HOME INCLUDE @@ -20,28 +22,44 @@ commands= sphinx-build -b doctest -d {envtmpdir}/doctrees docs/source {temp_dir}/doctest {posargs} [testenv:mypy] +labels = + lint + correctness dependency_groups = type_checking setenv = MYPY_CACHE_DIR = {temp_dir}/.mypy_cache commands = mypy {posargs: -p uiucprescon.imagevalidate} [testenv:flake8] description = check the code style +labels = + lint + style dependency_groups = lint skip_install=True commands = flake8 {posargs: src} [testenv:pylint] description = check the code style +labels = + lint + style + correctness dependency_groups = lint skip_install=True commands = pylint {posargs: src} --disable import-error [testenv:pydocstyle] +labels = + lint + style skip_install = true dependency_groups = lint commands = pydocstyle {posargs: {toxinidir}/src} [testenv:bandit] +labels = + lint + security skip_install = true dependency_groups = lint commands = bandit {posargs: --recursive {toxinidir}/src} From 7ef1c6ae1dad56200675c620a8177ca3e86d3c9d Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:02:49 -0500 Subject: [PATCH 02/12] style: addressed whitespace --- src/uiucprescon/imagevalidate/__init__.py | 4 +++- src/uiucprescon/imagevalidate/profile.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/uiucprescon/imagevalidate/__init__.py b/src/uiucprescon/imagevalidate/__init__.py index d3fbf47..970a9f2 100644 --- a/src/uiucprescon/imagevalidate/__init__.py +++ b/src/uiucprescon/imagevalidate/__init__.py @@ -2,7 +2,9 @@ from .issues import IssueCategory from .report import Report -from .profile import Profile, available_profiles, get_profile, get_profile_classes +from .profile import ( + Profile, available_profiles, get_profile, get_profile_classes +) from . import profiles __all__ = [ diff --git a/src/uiucprescon/imagevalidate/profile.py b/src/uiucprescon/imagevalidate/profile.py index 79d3016..7e86337 100644 --- a/src/uiucprescon/imagevalidate/profile.py +++ b/src/uiucprescon/imagevalidate/profile.py @@ -66,4 +66,5 @@ def get_profile_classes(): known_package_profiles[profile[1].profile_name()] = profile[1] return known_package_profiles + known_profiles = get_profile_classes() From 8203da756976d95ffb3232c1c11ec1f388df9c39 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:06:08 -0500 Subject: [PATCH 03/12] chore: upgrade lockfile dependency (bandit) Updated bandit v1.8.6 -> v1.9.4 --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index c709056..55dbc72 100644 --- a/uv.lock +++ b/uv.lock @@ -57,7 +57,7 @@ wheels = [ [[package]] name = "bandit" -version = "1.8.6" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -65,9 +65,9 @@ dependencies = [ { name = "rich" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/b5/7eb834e213d6f73aace21938e5e90425c92e5f42abafaf8a6d5d21beed51/bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b", size = 4240271, upload-time = "2025-07-06T03:10:50.9Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ca/ba5f909b40ea12ec542d5d7bdd13ee31c4d65f3beed20211ef81c18fa1f3/bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", size = 133808, upload-time = "2025-07-06T03:10:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] [[package]] From 526ad238e248d46ebf53f854aa8aa2c5cadd255d Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:09:57 -0500 Subject: [PATCH 04/12] docs: complete missing docstrings for public code --- src/uiucprescon/imagevalidate/issues.py | 4 ++++ src/uiucprescon/imagevalidate/profile.py | 1 + src/uiucprescon/imagevalidate/report.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/src/uiucprescon/imagevalidate/issues.py b/src/uiucprescon/imagevalidate/issues.py index 57892a4..d963cf1 100644 --- a/src/uiucprescon/imagevalidate/issues.py +++ b/src/uiucprescon/imagevalidate/issues.py @@ -1,7 +1,11 @@ +"""Module for defining issue categories related to image validation.""" + from enum import Enum class IssueCategory(Enum): + """Enum class defining issue categories.""" + VALID = 0 EMPTY_DATA = 1 MISSING_FIELD = 2 diff --git a/src/uiucprescon/imagevalidate/profile.py b/src/uiucprescon/imagevalidate/profile.py index 7e86337..a0680af 100644 --- a/src/uiucprescon/imagevalidate/profile.py +++ b/src/uiucprescon/imagevalidate/profile.py @@ -55,6 +55,7 @@ def get_profile(name: str) -> profile_pkg.AbsProfile: def get_profile_classes(): + """Get all available profiles.""" known_package_profiles: Dict[str, Type[profile_pkg.AbsProfile]] = {} profiles = \ inspect.getmembers( diff --git a/src/uiucprescon/imagevalidate/report.py b/src/uiucprescon/imagevalidate/report.py index b791bcc..5eb3a39 100644 --- a/src/uiucprescon/imagevalidate/report.py +++ b/src/uiucprescon/imagevalidate/report.py @@ -6,11 +6,15 @@ class ResultCategory(Enum): + """Enum class defining result categories.""" + ANY = 0 NONE = 1 class Result(NamedTuple): + """Result class defining result values.""" + expected: Union[str, ResultCategory] actual: Optional[str] From 6235d2c35fe68acde69f301538835282c28eda17 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:13:28 -0500 Subject: [PATCH 05/12] ci: add test for pydocstyle --- Jenkinsfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index df62277..e00d065 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -965,6 +965,21 @@ pipeline { } } } + stage('pyDocStyle'){ + steps{ + catchError(buildResult: 'SUCCESS', message: 'Did not pass all pyDocStyle tests', stageResult: 'UNSTABLE') { + sh( + label: 'Run pydocstyle', + script: 'uv run pydocstyle src/uiucprescon/imagevalidate > reports/pydocstyle-report.txt' + ) + } + } + post { + always{ + recordIssues(tools: [pyDocStyle(pattern: 'reports/pydocstyle-report.txt')]) + } + } + } stage('Run Doctest Tests'){ steps { catchError(buildResult: 'SUCCESS', message: 'Doctest found issues', stageResult: 'UNSTABLE') { From d793e6b69335b6e560507e8f57cb246d692c7723 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:38:08 -0500 Subject: [PATCH 06/12] fix: cyclic-import --- src/uiucprescon/imagevalidate/profile.py | 7 +++++-- src/uiucprescon/imagevalidate/profiles/absProfile.py | 10 ++++++++-- src/uiucprescon/imagevalidate/profiles/hathi_tiff.py | 10 +++++++--- src/uiucprescon/imagevalidate/report.py | 6 ++++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/uiucprescon/imagevalidate/profile.py b/src/uiucprescon/imagevalidate/profile.py index a0680af..3ea8d9c 100644 --- a/src/uiucprescon/imagevalidate/profile.py +++ b/src/uiucprescon/imagevalidate/profile.py @@ -1,11 +1,14 @@ """Profile for validating images.""" +from __future__ import annotations import os import inspect -from typing import Type, Set, Dict -from uiucprescon import imagevalidate +from typing import Type, Set, Dict, TYPE_CHECKING from . import profiles as profile_pkg +if TYPE_CHECKING: + from uiucprescon import imagevalidate + known_profiles: Dict[str, Type[profile_pkg.AbsProfile]] = {} diff --git a/src/uiucprescon/imagevalidate/profiles/absProfile.py b/src/uiucprescon/imagevalidate/profiles/absProfile.py index 79d3cc5..d05e7c1 100644 --- a/src/uiucprescon/imagevalidate/profiles/absProfile.py +++ b/src/uiucprescon/imagevalidate/profiles/absProfile.py @@ -1,12 +1,18 @@ """Abstract class for creating a profile.""" +from __future__ import annotations import abc -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, TYPE_CHECKING + import py3exiv2bind -from uiucprescon.imagevalidate import Report, IssueCategory, messages +from uiucprescon.imagevalidate import messages +from uiucprescon.imagevalidate.issues import IssueCategory from uiucprescon.imagevalidate.report import Result, ResultCategory +if TYPE_CHECKING: + from uiucprescon.imagevalidate.report import Report + class AbsProfile(metaclass=abc.ABCMeta): """Base class for metadata validation. diff --git a/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py b/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py index 39070b6..9327388 100644 --- a/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py +++ b/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py @@ -1,15 +1,19 @@ """Profile for HathiTrust tiff files.""" +from __future__ import annotations + import collections import sys import typing import py3exiv2bind -from uiucprescon.imagevalidate import IssueCategory -from uiucprescon.imagevalidate import Report, common -from uiucprescon.imagevalidate.report import Result +from uiucprescon.imagevalidate import common +from uiucprescon.imagevalidate.report import Result, Report from . import AbsProfile +if typing.TYPE_CHECKING: + from uiucprescon.imagevalidate import IssueCategory + class HathiTiff(AbsProfile): """Profile for validating Tiff files for HathiTrust.""" diff --git a/src/uiucprescon/imagevalidate/report.py b/src/uiucprescon/imagevalidate/report.py index 5eb3a39..af133f7 100644 --- a/src/uiucprescon/imagevalidate/report.py +++ b/src/uiucprescon/imagevalidate/report.py @@ -1,8 +1,10 @@ """Report generated from validation.""" +from __future__ import annotations -from typing import NamedTuple, Optional, Dict, List, Union +from typing import NamedTuple, Optional, Dict, List, Union, TYPE_CHECKING from enum import Enum -from uiucprescon import imagevalidate +if TYPE_CHECKING: + from uiucprescon import imagevalidate class ResultCategory(Enum): From 326bf50ca1c29bf7e43dd35460e842bb63c74bb5 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 09:49:09 -0500 Subject: [PATCH 07/12] refactor: common hathi profile attributes are in hathi_common.py --- .../imagevalidate/profiles/hathi_common.py | 77 +++++++++++++++++++ .../imagevalidate/profiles/hathi_jp2000.py | 67 ++-------------- .../imagevalidate/profiles/hathi_tiff.py | 74 +++--------------- 3 files changed, 97 insertions(+), 121 deletions(-) create mode 100644 src/uiucprescon/imagevalidate/profiles/hathi_common.py diff --git a/src/uiucprescon/imagevalidate/profiles/hathi_common.py b/src/uiucprescon/imagevalidate/profiles/hathi_common.py new file mode 100644 index 0000000..6114486 --- /dev/null +++ b/src/uiucprescon/imagevalidate/profiles/hathi_common.py @@ -0,0 +1,77 @@ +"""Shared values for HathiTrust profiles.""" +from __future__ import annotations + +import collections +import typing +from abc import ABC + +from uiucprescon.imagevalidate import Report +from . import AbsProfile + +if typing.TYPE_CHECKING: + from uiucprescon.imagevalidate import IssueCategory + +__all__ = [ + "SHARED_EXPECTED_METADATA_ANY_VALUE", + "SHARED_EXPECT_RESOLUTION_CONSTANTS" +] + +SHARED_EXPECTED_METADATA_ANY_VALUE = [ + 'Xmp.dc.creator', + + # Address + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr', + + # City + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity', + + # State + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion', + + # Zip code + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode', + + # Country + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry', + + # phone number + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork', + ] + +SHARED_EXPECT_RESOLUTION_CONSTANTS = { + "Exif.Image.XResolution": "400/1", + "Exif.Image.YResolution": "400/1", +} + + +class AbsValidateHathiTrustProfile(AbsProfile, ABC): + """Profile for validating files for HathiTrust.""" + + def validate(self, file: str) -> Report: + """Validate the image file as a HathiTrust image. + + Args: + file: + File path to an image file + + Returns: + Returns a report of the results. + + """ + report = Report() + report.filename = file + report_data = self.get_data_from_image(file) + report._properties = report_data + + analysis: typing.Dict[IssueCategory, list] = \ + collections.defaultdict(list) + + for key, result in report_data.items(): + issue_category = self.analyze_data_for_issues(result) + if issue_category: + message = self.generate_error_msg(issue_category, key, result) + analysis[issue_category].append(message) + + report._data.update(analysis) + + return report diff --git a/src/uiucprescon/imagevalidate/profiles/hathi_jp2000.py b/src/uiucprescon/imagevalidate/profiles/hathi_jp2000.py index a5d46a6..de5d148 100644 --- a/src/uiucprescon/imagevalidate/profiles/hathi_jp2000.py +++ b/src/uiucprescon/imagevalidate/profiles/hathi_jp2000.py @@ -1,46 +1,24 @@ """Profile for HathiTrust tiff files.""" -import collections import sys +import typing import py3exiv2bind -import typing -from uiucprescon.imagevalidate import IssueCategory, common -from uiucprescon.imagevalidate import Report +from uiucprescon.imagevalidate import common from uiucprescon.imagevalidate.report import Result from uiucprescon.imagevalidate import openjp2wrap # type: ignore -from . import AbsProfile +from . import hathi_common -class HathiJP2000(AbsProfile): +class HathiJP2000(hathi_common.AbsValidateHathiTrustProfile): """Profile for validating .jp2 files for HathiTrust.""" - expected_metadata_any_value = [ - 'Xmp.dc.creator', - - # Address - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr', - - # City - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity', - - # State - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion', + expected_metadata_any_value =\ + hathi_common.SHARED_EXPECTED_METADATA_ANY_VALUE - # Zip code - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode', + expected_metadata_constants =\ + hathi_common.SHARED_EXPECT_RESOLUTION_CONSTANTS - # Country - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry', - - # phone number - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork', - ] - - expected_metadata_constants = { - "Exif.Image.XResolution": "400/1", - "Exif.Image.YResolution": "400/1", - } valid_extensions = {".jp2"} @staticmethod @@ -48,35 +26,6 @@ def profile_name() -> str: """Get the profile name.""" return "HathiTrust JPEG 2000" - def validate(self, file: str) -> Report: - """Validate the image file as a HathiTrust jpeg2000 image. - - Args: - file: - File path to an image file - - Returns: - Returns a report of the results. - - """ - report = Report() - report.filename = file - report_data = self.get_data_from_image(file) - report._properties = report_data - - analysis: typing.Dict[IssueCategory, list] = \ - collections.defaultdict(list) - - for key, result in report_data.items(): - issue_category = self.analyze_data_for_issues(result) - if issue_category: - message = self.generate_error_msg(issue_category, key, result) - analysis[issue_category].append(message) - - report._data.update(analysis) - - return report - @classmethod def get_data_from_image(cls, filename: str) \ -> typing.Dict[str, Result]: diff --git a/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py b/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py index 9327388..f50249e 100644 --- a/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py +++ b/src/uiucprescon/imagevalidate/profiles/hathi_tiff.py @@ -2,49 +2,28 @@ from __future__ import annotations -import collections import sys import typing import py3exiv2bind from uiucprescon.imagevalidate import common -from uiucprescon.imagevalidate.report import Result, Report -from . import AbsProfile +from uiucprescon.imagevalidate.report import Result +from . import hathi_common -if typing.TYPE_CHECKING: - from uiucprescon.imagevalidate import IssueCategory - -class HathiTiff(AbsProfile): +class HathiTiff(hathi_common.AbsValidateHathiTrustProfile): """Profile for validating Tiff files for HathiTrust.""" - expected_metadata_any_value = [ - 'Xmp.dc.creator', - - # Address - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr', - - # City - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity', - - # State - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion', - - # Zip code - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode', + expected_metadata_any_value =\ + hathi_common.SHARED_EXPECTED_METADATA_ANY_VALUE - # Country - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry', - - # phone number - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork', - ] - - expected_metadata_constants = { - "Exif.Image.XResolution": "400/1", - "Exif.Image.YResolution": "400/1", - 'Exif.Image.BitsPerSample': "8 8 8", - } + expected_metadata_constants = \ + { + **hathi_common.SHARED_EXPECT_RESOLUTION_CONSTANTS, + **{ + 'Exif.Image.BitsPerSample': "8 8 8", + } + } valid_extensions = {".tif"} @staticmethod @@ -52,35 +31,6 @@ def profile_name() -> str: """Get the profile name.""" return "HathiTrust Tiff" - def validate(self, file: str) -> Report: - """Validate the image file as a HathiTrust tiff. - - Args: - file: - File path to an image file - - Returns: - Returns a report of the results. - - """ - report = Report() - report.filename = file - report_data = self.get_data_from_image(file) - report._properties = report_data - - analysis: typing.Dict[IssueCategory, list] = \ - collections.defaultdict(list) - - for key, result in report_data.items(): - issue_category = self.analyze_data_for_issues(result) - if issue_category: - message = self.generate_error_msg(issue_category, key, result) - analysis[issue_category].append(message) - - report._data.update(analysis) - - return report - @classmethod def get_data_from_image(cls, filename: str) -> typing.Dict[str, Result]: """Get data from image.""" From b7b0c08a178ecd9c472ea39762c7872381b5108a Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 10:17:58 -0500 Subject: [PATCH 08/12] style: updated code style use {} instead of dict() for empty dictionaries use [] instead of list() for empty lists use fstrings instead of format() --- src/uiucprescon/imagevalidate/common.py | 3 +-- src/uiucprescon/imagevalidate/profiles/absProfile.py | 12 ++++++------ src/uiucprescon/imagevalidate/report.py | 8 ++++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/uiucprescon/imagevalidate/common.py b/src/uiucprescon/imagevalidate/common.py index df8320b..358d9b4 100644 --- a/src/uiucprescon/imagevalidate/common.py +++ b/src/uiucprescon/imagevalidate/common.py @@ -102,8 +102,7 @@ def check(self, image: str) -> str: try: icc = exiv2_image.icc() except py3exiv2bind.core.NoICCError as error: - raise InvalidStrategy("Unable to get ICC profile." - "Reason: {}".format(error)) + raise InvalidStrategy(f"Unable to get ICC profile.Reason: {error}") pref_ccm = icc.get("pref_ccm") if not pref_ccm or pref_ccm.value.decode("ascii").rstrip(' \0') == '': diff --git a/src/uiucprescon/imagevalidate/profiles/absProfile.py b/src/uiucprescon/imagevalidate/profiles/absProfile.py index d05e7c1..bf420c3 100644 --- a/src/uiucprescon/imagevalidate/profiles/absProfile.py +++ b/src/uiucprescon/imagevalidate/profiles/absProfile.py @@ -20,8 +20,8 @@ class AbsProfile(metaclass=abc.ABCMeta): Implement the validate method when creating new profile """ - expected_metadata_constants: Dict[str, str] = dict() - expected_metadata_any_value: List[str] = list() + expected_metadata_constants: Dict[str, str] = {} + expected_metadata_any_value: List[str] = [] valid_extensions: Set[str] = set() @staticmethod @@ -44,7 +44,7 @@ def validate(self, file: str) -> Report: def _get_metadata_static_values(cls, image: py3exiv2bind.Image) \ -> Dict[str, Result]: - data = dict() + data = {} for key, value in cls.expected_metadata_constants.items(): data[key] = Result( expected=value, @@ -56,7 +56,7 @@ def _get_metadata_static_values(cls, image: py3exiv2bind.Image) \ def _get_metadata_has_values(cls, image: py3exiv2bind.Image) -> \ Dict[str, Result]: - data = dict() + data = {} for key in cls.expected_metadata_any_value: data[key] = Result( expected=ResultCategory.ANY, @@ -81,7 +81,7 @@ def generate_error_msg(category: IssueCategory, field: str, return message_generator.generate_message(field, report_data) - return "Unknown error with {}".format(field) + return f"Unknown error with {field}" @staticmethod def analyze_data_for_issues(result: Result) -> Optional[IssueCategory]: @@ -104,7 +104,7 @@ def get_data_from_image(cls, filename: str) \ -> Dict[str, Result]: """Access data from image.""" image = py3exiv2bind.Image(filename) - data: Dict[str, Result] = dict() + data: Dict[str, Result] = {} data.update(cls._get_metadata_has_values(image)) data.update(cls._get_metadata_static_values(image)) return data diff --git a/src/uiucprescon/imagevalidate/report.py b/src/uiucprescon/imagevalidate/report.py index af133f7..edf2e23 100644 --- a/src/uiucprescon/imagevalidate/report.py +++ b/src/uiucprescon/imagevalidate/report.py @@ -26,10 +26,10 @@ class Report: def __init__(self) -> None: """Access the results.""" - self._properties: Dict[str, Result] = dict() + self._properties: Dict[str, Result] = {} self.filename: Optional[str] = None - self._data: Dict[imagevalidate.IssueCategory, List[str]] = dict() + self._data: Dict[imagevalidate.IssueCategory, List[str]] = {} @property def valid(self) -> bool: @@ -46,7 +46,7 @@ def issues(self, -> List[str]: """Issues or problems discovered.""" if issue_type is not None: - return self._data.get(issue_type, list()) + return self._data.get(issue_type, []) # In issue category is selected, return all return [issue for issues in @@ -59,4 +59,4 @@ def __str__(self) -> str: else: issue_str = "No issues discovered" - return "File: {}\n{}".format(self.filename, issue_str) + return f"File: {self.filename}\n{issue_str}" From 0f850956fe7919bac41d96efa3ea5cf0fd2ff17f Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 10:33:06 -0500 Subject: [PATCH 09/12] feat: py3exiv2bind.core.NoICCError exceptions rethrow original exception in ColorSpaceIccDeviceModelCheck.check() --- src/uiucprescon/imagevalidate/common.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/uiucprescon/imagevalidate/common.py b/src/uiucprescon/imagevalidate/common.py index 358d9b4..9b6f519 100644 --- a/src/uiucprescon/imagevalidate/common.py +++ b/src/uiucprescon/imagevalidate/common.py @@ -74,8 +74,8 @@ def check(self, image: str) -> str: exiv_image = py3exiv2bind.Image(image) try: icc = exiv_image.icc() - except py3exiv2bind.core.NoICCError: - raise InvalidStrategy("Unable to get ICC profile.") + except py3exiv2bind.core.NoICCError as error: + raise InvalidStrategy("Unable to get ICC profile.") from error device_model = icc.get('device_model') if not device_model or \ @@ -102,7 +102,9 @@ def check(self, image: str) -> str: try: icc = exiv2_image.icc() except py3exiv2bind.core.NoICCError as error: - raise InvalidStrategy(f"Unable to get ICC profile.Reason: {error}") + raise InvalidStrategy( + f"Unable to get ICC profile.Reason: {error}" + ) from error pref_ccm = icc.get("pref_ccm") if not pref_ccm or pref_ccm.value.decode("ascii").rstrip(' \0') == '': From e5e4b30aa63d85474e95876a97f1ed31f9d144cf Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 10:46:56 -0500 Subject: [PATCH 10/12] ci: add pylint stage --- Jenkinsfile | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e00d065..bf1a630 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1034,6 +1034,24 @@ pipeline { } } } + stage('Run Pylint Static Analysis') { + steps{ + catchError(buildResult: 'SUCCESS', message: 'Pylint found issues', stageResult: 'UNSTABLE') { + sh( + script: '''mkdir -p logs + mkdir -p reports + uv run pylint src/uiucprescon/imagevalidate -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > reports/pylint.txt + ''', + label: 'Running pylint' + ) + } + } + post{ + always { + recordIssues(tools: [pyLint(pattern: 'reports/pylint.txt')]) + } + } + } } post{ always{ @@ -1086,12 +1104,12 @@ pipeline { if (env.CHANGE_ID){ sh( label: 'Running Sonar Scanner', - script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.pullrequest.key=${env.CHANGE_ID} -Dsonar.pullrequest.base=${env.CHANGE_TARGET} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory" + script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.pullrequest.key=${env.CHANGE_ID} -Dsonar.pullrequest.base=${env.CHANGE_TARGET} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory -Dsonar.python.pylint.reportPaths=reports/pylint.txt" ) } else { sh( label: 'Running Sonar Scanner', - script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.branch.name=${env.BRANCH_NAME} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory" + script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.branch.name=${env.BRANCH_NAME} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory -Dsonar.python.pylint.reportPaths=reports/pylint.txt" ) } } From 6df6adbe71eff06db8105d84c2133a613a0c3602 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 11:03:36 -0500 Subject: [PATCH 11/12] docs: fixed issue with doc config regex --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0198682..8a197d7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,7 +38,7 @@ def get_project_metadata(): author = metadata['authors'][0]['name'] # The short X.Y version -version_extractor = re.compile("\d+[.]\d+[.]\d+") +version_extractor = re.compile(r"\d+[.]\d+[.]\d+") version = version_extractor.search(metadata["version"]).group(0) # The full version, including alpha/beta/rc tags. release = metadata["version"] From 82f68d333bb18813d1d414e18a5c48c298965a21 Mon Sep 17 00:00:00 2001 From: hborcher Date: Mon, 8 Jun 2026 10:49:57 -0500 Subject: [PATCH 12/12] test: configure pylint set extension-pkg-allow-list use src directory disable too-few-public-methods where known --- pyproject.toml | 5 +++++ src/uiucprescon/imagevalidate/common.py | 5 +++++ src/uiucprescon/imagevalidate/messages.py | 5 +++++ src/uiucprescon/imagevalidate/profile.py | 2 ++ 4 files changed, 17 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 07be646..ccd93b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,11 @@ norecursedirs = "build" markers = "integration" junit_family="xunit2" +[tool.pylint.MAIN] +init-hook = "import sys; sys.path.append('src')" # Replace 'src' with your module directory +extension-pkg-allow-list = ["py3exiv2bind.core", "uiucprescon.imagevalidate.openjp2wrap"] + + [tool.cibuildwheel] test-groups = ["test"] test-command = "pytest {project}/tests" diff --git a/src/uiucprescon/imagevalidate/common.py b/src/uiucprescon/imagevalidate/common.py index 9b6f519..4632e2c 100644 --- a/src/uiucprescon/imagevalidate/common.py +++ b/src/uiucprescon/imagevalidate/common.py @@ -12,6 +12,7 @@ class InvalidStrategy(Exception): class AbsColorSpaceExtractor(metaclass=abc.ABCMeta): + # pylint: disable=too-few-public-methods """Base class for extracting the color space from an image file.""" @abc.abstractmethod @@ -29,6 +30,7 @@ def check(self, image: str) -> str: class ExtractColorSpace: + # pylint: disable=too-few-public-methods """Strategy context for extract color space from a file.""" def __init__(self, strategy: AbsColorSpaceExtractor) -> None: @@ -55,6 +57,7 @@ def check(self, image: str) -> str: class ColorSpaceIccDeviceModelCheck(AbsColorSpaceExtractor): + # pylint: disable=too-few-public-methods """Extract color space by reading the device_model tag in the ICC profile. Useful for identifying sRGB. @@ -85,6 +88,7 @@ def check(self, image: str) -> str: class ColorSpaceIccPrefCcmCheck(AbsColorSpaceExtractor): + # pylint: disable=too-few-public-methods """Extract color space from reading pref_ccm in the ICC profile header.""" def check(self, image: str) -> str: @@ -113,6 +117,7 @@ def check(self, image: str) -> str: class ColorSpaceOJPCheck(AbsColorSpaceExtractor): + # pylint: disable=too-few-public-methods """Color space extractor using openjpeg library.""" def check(self, image: str) -> str: diff --git a/src/uiucprescon/imagevalidate/messages.py b/src/uiucprescon/imagevalidate/messages.py index c439299..2b710a7 100644 --- a/src/uiucprescon/imagevalidate/messages.py +++ b/src/uiucprescon/imagevalidate/messages.py @@ -6,6 +6,7 @@ class AbsMessage(metaclass=abc.ABCMeta): + # pylint: disable=too-few-public-methods """Base class for messages.""" @abc.abstractmethod @@ -14,6 +15,7 @@ def generate_message(self, field: str, data: report.Result) -> str: class InvalidData(AbsMessage): + # pylint: disable=too-few-public-methods """Invalid data.""" def generate_message(self, field: str, data: report.Result) -> str: @@ -24,6 +26,7 @@ def generate_message(self, field: str, data: report.Result) -> str: class EmptyData(AbsMessage): + # pylint: disable=too-few-public-methods """Empty data.""" def generate_message(self, field: str, data: report.Result) -> str: @@ -32,6 +35,7 @@ def generate_message(self, field: str, data: report.Result) -> str: class MissingField(AbsMessage): + # pylint: disable=too-few-public-methods """Missing fields.""" def generate_message(self, field: str, data: report.Result) -> str: @@ -40,6 +44,7 @@ def generate_message(self, field: str, data: report.Result) -> str: class MessageGenerator: + # pylint: disable=too-few-public-methods """Message Generator.""" def __init__(self, strategy: AbsMessage) -> None: diff --git a/src/uiucprescon/imagevalidate/profile.py b/src/uiucprescon/imagevalidate/profile.py index 3ea8d9c..6f450b6 100644 --- a/src/uiucprescon/imagevalidate/profile.py +++ b/src/uiucprescon/imagevalidate/profile.py @@ -13,6 +13,7 @@ class Profile: + # pylint: disable=too-few-public-methods """Profile loader for validating embedded metadata in image files.""" def __init__(self, validation_profile: profile_pkg.AbsProfile) -> None: @@ -36,6 +37,7 @@ def validate(self, file: str) -> imagevalidate.Report: if not os.path.exists(file): raise FileNotFoundError(f"Unable to locate {file}") return self._profile.validate(file) +# pylint: enable=too-few-public-methods def available_profiles() -> Set[str]: