This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 5115047  Make pluralisation more consistent throughout
5115047 is described below

commit 511504757e49161f19466f9c546c413d047c7c1d
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Dec 29 15:08:10 2025 +0000

    Make pluralisation more consistent throughout
---
 atr/admin/__init__.py          | 24 +++++++++++-------------
 atr/form.py                    |  4 ++--
 atr/get/checks.py              |  6 +++---
 atr/get/keys.py                |  2 +-
 atr/get/projects.py            |  8 ++++----
 atr/get/vote.py                |  6 +++---
 atr/manager.py                 |  3 ++-
 atr/post/draft.py              |  5 +----
 atr/post/finish.py             |  7 ++++---
 atr/post/keys.py               |  6 +++---
 atr/post/upload.py             |  8 +++-----
 atr/shared/__init__.py         |  8 ++++----
 atr/storage/writers/keys.py    |  4 ++--
 atr/storage/writers/release.py |  6 +++---
 atr/tabulate.py                | 12 ++++--------
 atr/tasks/checks/license.py    |  2 +-
 atr/tasks/checks/rat.py        |  8 +++-----
 atr/tasks/checks/signature.py  |  2 +-
 atr/tasks/checks/zipformat.py  |  5 ++++-
 atr/util.py                    | 34 ++++++++++++++++++----------------
 20 files changed, 77 insertions(+), 83 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 63b5a62..3b54161 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -303,7 +303,8 @@ async def delete_committee_keys_post(
 
         await data.commit()
         await quart.flash(
-            f"Removed {num_removed} key links for '{committee_name}'. Deleted 
{unused_deleted} unused keys.",
+            f"Removed {util.plural(num_removed, 'key link')} for 
'{committee_name}'. "
+            f"Deleted {util.plural(unused_deleted, 'unused key')}.",
             "success",
         )
 
@@ -389,9 +390,9 @@ async def delete_test_openpgp_keys_post(session: 
web.Committer) -> web.Response:
             delete_outcome = await wafc.keys.test_user_delete_all(test_uid)
             deleted_count = delete_outcome.result_or_raise()
 
-        suffix = "s" if (deleted_count != 1) else ""
         await quart.flash(
-            f"Successfully deleted {deleted_count} OpenPGP key{suffix} and 
their associated links for test user.",
+            f"Successfully deleted {util.plural(deleted_count, 'OpenPGP key')} 
"
+            "and their associated links for test user.",
             "success",
         )
     except Exception as e:
@@ -717,10 +718,8 @@ async def tasks_recent(session: web.Committer, minutes: 
int) -> str:
         recent_tasks = (await data.execute(statement)).scalars().all()
 
     page = htm.Block()
-    minute_word = "minute" if (minutes == 1) else "minutes"
-    page.h1[f"Tasks from the last {minutes} {minute_word}"]
-    task_word = "task" if (len(recent_tasks) == 1) else "tasks"
-    page.p[f"Found {len(recent_tasks)} {task_word}"]
+    page.h1[f"Tasks from the last {util.plural(minutes, 'minute')}"]
+    page.p[f"Found {util.plural(len(recent_tasks), 'task')}"]
 
     if recent_tasks:
         table = htm.Block(htpy.table, classes=".table.table-sm")
@@ -785,7 +784,7 @@ async def test(session: web.Committer) -> web.QuartResponse:
         start = time.perf_counter_ns()
         outcomes: outcome.List[types.Key] = await 
wacm.keys.ensure_stored(keys_file_text)
         end = time.perf_counter_ns()
-        log.info(f"Upload of {outcomes.result_count} keys took {end - start} 
ns")
+        log.info(f"Upload of {util.plural(outcomes.result_count, 'key')} took 
{end - start} ns")
     for ocr in outcomes.results():
         log.info(f"Uploaded key: {type(ocr)} {ocr.key_model.fingerprint}")
     for oce in outcomes.errors():
@@ -864,9 +863,9 @@ async def _check_keys(fix: bool = False) -> str:
             if fix:
                 key.apache_uid = asf_uid
                 await data.commit()
-    message = f"Checked {len(keys)} keys"
+    message = f"Checked {util.plural(len(keys), 'key')}"
     if bad_keys:
-        message += f"\nFound {len(bad_keys)} bad keys:\n{'\n'.join(bad_keys)}"
+        message += f"\nFound {util.plural(len(bad_keys), 'bad 
key')}:\n{'\n'.join(bad_keys)}"
     return message
 
 
@@ -943,12 +942,11 @@ async def _delete_releases(session: web.Committer, 
releases_to_delete: list[str]
             fail_count += 1
             error_messages.append(f"{release_name}: Unexpected error ({e})")
 
-    releases = "release" if (success_count == 1) else "releases"
     if success_count > 0:
-        await quart.flash(f"Successfully deleted {success_count} {releases}.", 
"success")
+        await quart.flash(f"Successfully deleted {util.plural(success_count, 
'release')}.", "success")
     if fail_count > 0:
         errors_str = "\n".join(error_messages)
-        await quart.flash(f"Failed to delete {fail_count} 
{releases}:\n{errors_str}", "error")
+        await quart.flash(f"Failed to delete {util.plural(fail_count, 
'release')}:\n{errors_str}", "error")
 
 
 def _format_exception_location(exc: BaseException) -> str:
diff --git a/atr/form.py b/atr/form.py
index 3fdcf58..6f40775 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -35,6 +35,7 @@ import quart_wtf.utils as utils
 
 import atr.htm as htm
 import atr.models.schema as schema
+import atr.util as util
 
 if TYPE_CHECKING:
     from collections.abc import Iterator
@@ -121,8 +122,7 @@ def flash_error_data(
 
 def flash_error_summary(errors: list[pydantic_core.ErrorDetails], flash_data: 
dict[str, Any]) -> markupsafe.Markup:
     div = htm.Block(htm.div, classes=".atr-initial")
-    plural = len(errors) > 1
-    div.text(f"Please fix the following issue{'s' if plural else ''}:")
+    div.text(f"Please fix the following {util.plural(len(errors), 'issue', 
include_count=False)}:")
     with div.block(htm.ul, classes=".mt-2.mb-0") as ul:
         for i, flash_datum in enumerate(flash_data.values()):
             if i > 9:
diff --git a/atr/get/checks.py b/atr/get/checks.py
index f4f5dac..35b5585 100644
--- a/atr/get/checks.py
+++ b/atr/get/checks.py
@@ -479,9 +479,9 @@ def _render_summary(
         f" {files_skipped} {skipped_word}." if (files_skipped > 0) else "",
     ]
 
-    check_word = "check" if (totals.file_pass_after == 1) else "checks"
-    warn_word = "warning" if (totals.file_warn_after == 1) else "warnings"
-    err_word = "error" if (totals.file_err_after == 1) else "errors"
+    check_word = util.plural(totals.file_pass_after, "check", 
include_count=False)
+    warn_word = util.plural(totals.file_warn_after, "warning", 
include_count=False)
+    err_word = util.plural(totals.file_err_after, "error", include_count=False)
 
     summary_div = htm.Block(htm.div, classes=".d-flex.flex-wrap.gap-4.mb-3")
     summary_div.span(".text-success")[
diff --git a/atr/get/keys.py b/atr/get/keys.py
index 9951ee8..1b14e2a 100644
--- a/atr/get/keys.py
+++ b/atr/get/keys.py
@@ -115,7 +115,7 @@ async def details(session: web.Committer, fingerprint: str) 
-> str:
             expires_content = htm.span(".text-warning.fw-bold")[
                 expires_str,
                 " ",
-                htm.span(".badge.bg-warning.text-dark.ms-2")[f"Expires in 
{days_until_expiry} days"],
+                htm.span(".badge.bg-warning.text-dark.ms-2")[f"Expires in 
{util.plural(days_until_expiry, 'day')}"],
             ]
         else:
             expires_content = expires_str
diff --git a/atr/get/projects.py b/atr/get/projects.py
index c1eb00b..e0d7485 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -453,7 +453,7 @@ async def _render_releases_sections(
                     title=f"View draft {project.name} {drf.version}",
                 )[
                     f"{project.name} {drf.version} ",
-                    htm.span(".badge.bg-secondary.ms-2")[f"{file_count} 
{'file' if (file_count == 1) else 'files'}"],
+                    
htm.span(".badge.bg-secondary.ms-2")[util.plural(file_count, "file")],
                 ]
             )
         sections.div(".d-flex.flex-wrap.gap-2.mb-4")[*draft_buttons]
@@ -470,7 +470,7 @@ async def _render_releases_sections(
                     title=f"View candidate {project.name} {cnd.version}",
                 )[
                     f"{project.name} {cnd.version} ",
-                    htm.span(".badge.bg-info.ms-2")[f"{file_count} {'file' if 
(file_count == 1) else 'files'}"],
+                    htm.span(".badge.bg-info.ms-2")[util.plural(file_count, 
"file")],
                 ]
             )
         sections.div(".d-flex.flex-wrap.gap-2.mb-4")[*candidate_buttons]
@@ -487,7 +487,7 @@ async def _render_releases_sections(
                     title=f"View preview {project.name} {prv.version}",
                 )[
                     f"{project.name} {prv.version} ",
-                    htm.span(".badge.bg-warning.ms-2")[f"{file_count} {'file' 
if (file_count == 1) else 'files'}"],
+                    htm.span(".badge.bg-warning.ms-2")[util.plural(file_count, 
"file")],
                 ]
             )
         sections.div(".d-flex.flex-wrap.gap-2.mb-4")[*preview_buttons]
@@ -504,7 +504,7 @@ async def _render_releases_sections(
                     title=f"View release {project.name} {rel.version}",
                 )[
                     f"{project.name} {rel.version} ",
-                    htm.span(".badge.bg-success.ms-2")[f"{file_count} {'file' 
if (file_count == 1) else 'files'}"],
+                    htm.span(".badge.bg-success.ms-2")[util.plural(file_count, 
"file")],
                 ]
             )
         sections.div(".d-flex.flex-wrap.gap-2.mb-4")[*release_buttons]
diff --git a/atr/get/vote.py b/atr/get/vote.py
index b2bdcd2..f5e4d20 100644
--- a/atr/get/vote.py
+++ b/atr/get/vote.py
@@ -354,9 +354,9 @@ def _render_section_checks(page: htm.Block, release: 
sql.Release, file_totals: c
     warn_count = file_totals.file_warn_after
     err_count = file_totals.file_err_after
 
-    check_word = "check" if (pass_count == 1) else "checks"
-    warn_word = "warning" if (warn_count == 1) else "warnings"
-    err_word = "error" if (err_count == 1) else "errors"
+    check_word = util.plural(pass_count, "check", include_count=False)
+    warn_word = util.plural(warn_count, "warning", include_count=False)
+    err_word = util.plural(err_count, "error", include_count=False)
 
     checks_list = htm.Block(htm.div, classes=".d-flex.flex-wrap.gap-4.mb-3")
     checks_list.span(".text-success")[
diff --git a/atr/manager.py b/atr/manager.py
index 37a6281..9d5ccec 100644
--- a/atr/manager.py
+++ b/atr/manager.py
@@ -32,6 +32,7 @@ import sqlmodel
 import atr.db as db
 import atr.log as log
 import atr.models.sql as sql
+import atr.util as util
 
 # Global debug flag to control worker process output capturing
 global_worker_debug: bool = False
@@ -340,7 +341,7 @@ class WorkerManager:
                         log.error(f"Expected cursor result, got 
{type(result)}")
                         return
                     if result.rowcount > 0:
-                        log.info(f"Reset {result.rowcount} tasks to state 
'QUEUED' due to worker issues")
+                        log.info(f"Reset {util.plural(result.rowcount, 
'task')} to state 'QUEUED' due to worker issues")
 
         except Exception as e:
             log.error(f"Error resetting broken tasks: {e}")
diff --git a/atr/post/draft.py b/atr/post/draft.py
index 9060b73..dc81f94 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -94,10 +94,7 @@ async def delete_file(
 
     success_message = f"File '{rel_path_to_delete.name}' deleted successfully"
     if metadata_files_deleted:
-        success_message += (
-            f", and {metadata_files_deleted} associated metadata "
-            f"file{'' if (metadata_files_deleted == 1) else 's'} deleted"
-        )
+        success_message += f", and {util.plural(metadata_files_deleted, 
'associated metadata file')} deleted"
     return await session.redirect(
         get.compose.selected, success=success_message, 
project_name=project_name, version_name=version_name
     )
diff --git a/atr/post/finish.py b/atr/post/finish.py
index 997f9ca..7408891 100644
--- a/atr/post/finish.py
+++ b/atr/post/finish.py
@@ -21,6 +21,7 @@ import atr.blueprints.post as post
 import atr.log as log
 import atr.shared as shared
 import atr.storage as storage
+import atr.util as util
 import atr.web as web
 
 
@@ -126,16 +127,16 @@ async def _remove_rc_tags(
         if creation_error is not None:
             return await respond(409, creation_error)
 
-        items = "item" if (renamed_count == 1) else "items"
         if error_messages:
             status_ok = renamed_count > 0
             # TODO: Ideally HTTP would have a general mixed status, like 207 
but for anything
             http_status = 200 if status_ok else 500
-            msg = f"RC tags removed for {renamed_count} {items} with some 
errors: {'; '.join(error_messages)}"
+            msg = f"RC tags removed for {util.plural(renamed_count, 'item')}"
+            msg += f" with some errors: {'; '.join(error_messages)}"
             return await respond(http_status, msg)
 
         if renamed_count > 0:
-            return await respond(200, f"Successfully removed RC tags from 
{renamed_count} {items}.")
+            return await respond(200, f"Successfully removed RC tags from 
{util.plural(renamed_count, 'item')}.")
 
         return await respond(200, "No items required RC tag removal or no 
changes were made.")
 
diff --git a/atr/post/keys.py b/atr/post/keys.py
index 9dc03d7..9e5eb6f 100644
--- a/atr/post/keys.py
+++ b/atr/post/keys.py
@@ -134,9 +134,9 @@ async def import_selected_revision(
         wacm = await write.as_project_committee_member(project_name)
         outcomes: outcome.List[types.Key] = await 
wacm.keys.import_keys_file(project_name, version_name)
 
-    message = f"Uploaded {outcomes.result_count} keys"
+    message = f"Uploaded {util.plural(outcomes.result_count, 'key')}"
     if outcomes.error_count > 0:
-        message += f", failed to upload {outcomes.error_count} keys for 
{wacm.committee_name}"
+        message += f", failed to upload {util.plural(outcomes.error_count, 
'key')} for {wacm.committee_name}"
     return await session.redirect(
         get.compose.selected,
         success=message,
@@ -251,7 +251,7 @@ async def _process_keys(keys_text: str, selected_committee: 
str) -> str:
     error_count = outcomes.error_count
     total_count = success_count + error_count
 
-    message = f"Processed {total_count} keys: {success_count} successful"
+    message = f"Processed {util.plural(total_count, 'key')}: {success_count} 
successful"
     if error_count > 0:
         message += f", {error_count} failed"
 
diff --git a/atr/post/upload.py b/atr/post/upload.py
index a3565ca..7908bbb 100644
--- a/atr/post/upload.py
+++ b/atr/post/upload.py
@@ -66,8 +66,7 @@ async def finalise(
         async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             number_of_files = len(staged_files)
-            plural = "s" if (number_of_files != 1) else ""
-            description = f"Upload of {number_of_files} file{plural} through 
web interface"
+            description = f"Upload of {util.plural(number_of_files, 'file')} 
through web interface"
 
             async with wacp.release.create_and_manage_revision(project_name, 
version_name, description) as creating:
                 for filename in staged_files:
@@ -79,7 +78,7 @@ async def finalise(
 
         return await session.redirect(
             get.compose.selected,
-            success=f"{number_of_files} file{plural} added successfully",
+            success=f"{util.plural(number_of_files, 'file')} added 
successfully",
             project_name=project_name,
             version_name=version_name,
         )
@@ -157,10 +156,9 @@ async def _add_files(
             wacp = await write.as_project_committee_participant(project_name)
             number_of_files = await wacp.release.upload_files(project_name, 
version_name, file_name, file_data)
 
-        plural = number_of_files != 1
         return await session.redirect(
             get.compose.selected,
-            success=f"{number_of_files} file{'s' if plural else ''} added 
successfully",
+            success=f"{util.plural(number_of_files, 'file')} added 
successfully",
             project_name=project_name,
             version_name=version_name,
         )
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index f3427d9..4c9e400 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -241,11 +241,11 @@ def _render_checks_summary(info: types.PathInfo | None, 
project_name: str, versi
 
             file_content: list[htm.Element | str] = []
             if error_count > 0:
-                label = "error" if (error_count == 1) else "errors"
-                
file_content.append(htpy.span(".badge.bg-danger.me-2")[f"{error_count} 
{label}"])
+                
file_content.append(htpy.span(".badge.bg-danger.me-2")[util.plural(error_count, 
"error")])
             if warning_count > 0:
-                label = "warning" if (warning_count == 1) else "warnings"
-                
file_content.append(htpy.span(".badge.bg-warning.text-dark.me-2")[f"{warning_count}
 {label}"])
+                file_content.append(
+                    
htpy.span(".badge.bg-warning.text-dark.me-2")[util.plural(warning_count, 
"warning")]
+                )
             
file_content.append(htpy.a(href=report_url)[htpy.strong[htpy.code[file_path]]])
 
             files_div.div[*file_content]
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index 57a6f45..584db4f 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -558,7 +558,7 @@ class CommitteeParticipant(FoundationCommitter):
                 stmt.returning(via(sql.PublicSigningKey.fingerprint)),
             )
             key_inserts = {row.fingerprint for row in key_insert_result}
-            log.info(f"Inserted or updated {len(key_inserts)} keys")
+            log.info(f"Inserted or updated {util.plural(len(key_inserts), 
'key')}")
         else:
             # TODO: Warn the user about any keys that were already inserted
             key_inserts = set()
@@ -585,7 +585,7 @@ class CommitteeParticipant(FoundationCommitter):
                 .returning(via(sql.KeyLink.key_fingerprint))
             )
             link_inserts = {row.key_fingerprint for row in link_insert_result}
-            log.info(f"Inserted {len(link_inserts)} key links")
+            log.info(f"Inserted {util.plural(len(link_inserts), 'key link')}")
 
             def replace_with_linked(key: types.Key) -> types.Key:
                 # nonlocal link_inserts
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 30a8669..fdf4284 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -122,12 +122,12 @@ class CommitteeParticipant(FoundationCommitter):
         tasks_to_delete = await 
self.__data.task(project_name=release.project.name, 
version_name=release.version).all()
         for task in tasks_to_delete:
             await self.__data.delete(task)
-        log.debug(f"Deleted {len(tasks_to_delete)} tasks for {project_name} 
{version}")
+        log.debug(f"Deleted {util.plural(len(tasks_to_delete), 'task')} for 
{project_name} {version}")
 
         checks_to_delete = await 
self.__data.check_result(release_name=release.name).all()
         for check in checks_to_delete:
             await self.__data.delete(check)
-        log.debug(f"Deleted {len(checks_to_delete)} check results for 
{project_name} {version}")
+        log.debug(f"Deleted {util.plural(len(checks_to_delete), 'check 
result')} for {project_name} {version}")
 
         # TODO: Ensure that revisions are not deleted
         # But this makes testing difficult
@@ -436,7 +436,7 @@ class CommitteeParticipant(FoundationCommitter):
     ) -> int:
         """Process and save the uploaded files into a new draft revision."""
         number_of_files = len(files)
-        description = f"Upload of {number_of_files} file{'' if 
(number_of_files == 1) else 's'} through web interface"
+        description = f"Upload of {util.plural(number_of_files, 'file')} 
through web interface"
         async with self.create_and_manage_revision(project_name, version_name, 
description) as creating:
             # Save each uploaded file to the new revision directory
             for file in files:
diff --git a/atr/tabulate.py b/atr/tabulate.py
index b7ea3ce..e4b1259 100644
--- a/atr/tabulate.py
+++ b/atr/tabulate.py
@@ -212,14 +212,10 @@ def _format_duration(duration_hours: float | int) -> str:
         minutes = 0
 
     parts: list[str] = []
-    if hours == 1:
-        parts.append("1 hour")
-    elif hours > 1:
-        parts.append(f"{hours} hours")
-    if minutes == 1:
-        parts.append("1 minute")
-    elif minutes > 1:
-        parts.append(f"{minutes} minutes")
+    if hours > 0:
+        parts.append(util.plural(hours, "hour"))
+    if minutes > 0:
+        parts.append(util.plural(minutes, "minute"))
 
     if not parts:
         return "less than 1 minute"
diff --git a/atr/tasks/checks/license.py b/atr/tasks/checks/license.py
index d4f9ce4..e67c00d 100644
--- a/atr/tasks/checks/license.py
+++ b/atr/tasks/checks/license.py
@@ -453,7 +453,7 @@ def _headers_check_core_logic(artifact_path: str, 
ignore_lines: list[str]) -> It
 
     yield ArtifactResult(
         status=sql.CheckResultStatus.SUCCESS,
-        message=f"Checked {artifact_data.files_checked} files,"
+        message=f"Checked {util.plural(artifact_data.files_checked, 'file')},"
         f" found {artifact_data.files_with_valid_headers} with valid headers,"
         f" {artifact_data.files_with_invalid_headers} with invalid headers,"
         f" and {artifact_data.files_skipped} skipped",
diff --git a/atr/tasks/checks/rat.py b/atr/tasks/checks/rat.py
index fadae35..3bce4f5 100644
--- a/atr/tasks/checks/rat.py
+++ b/atr/tasks/checks/rat.py
@@ -196,7 +196,7 @@ def _check_core_logic(
                 raise ValueError("XML output path is None")
 
             results = _check_core_logic_parse_output(xml_output_path, 
extract_dir)
-            log.info(f"Successfully parsed RAT output with 
{results.get('total_files', 0)} files")
+            log.info(f"Successfully parsed RAT output with 
{util.plural(results.get('total_files', 0), 'file')}")
 
             # The unknown_license_files key may contain a list of dicts
             # {"name": "./README.md", "license": "Unknown license"}
@@ -597,11 +597,9 @@ def _summary_message(valid: bool, unapproved_licenses: 
int, unknown_licenses: in
     if not valid:
         message = "Found "
         if unapproved_licenses > 0:
-            files = "file" if (unapproved_licenses == 1) else "files"
-            message += f"{unapproved_licenses} {files} with unapproved 
licenses"
+            message += f"{util.plural(unapproved_licenses, 'file')} with 
unapproved licenses"
             if unknown_licenses > 0:
                 message += " and "
         if unknown_licenses > 0:
-            files = "file" if (unknown_licenses == 1) else "files"
-            message += f"{unknown_licenses} {files} with unknown licenses"
+            message += f"{util.plural(unknown_licenses, 'file')} with unknown 
licenses"
     return message
diff --git a/atr/tasks/checks/signature.py b/atr/tasks/checks/signature.py
index 60d65d0..bc92e27 100644
--- a/atr/tasks/checks/signature.py
+++ b/atr/tasks/checks/signature.py
@@ -137,7 +137,7 @@ def _check_core_logic_verify_signature(
         if not import_result.fingerprints:
             log.warning("No fingerprints found after importing keys")
         end = time.perf_counter_ns()
-        log.info(f"Import {len(ascii_armored_keys)} keys took {(end - start) / 
1000000} ms")
+        log.info(f"Import of {util.plural(len(ascii_armored_keys), 'key')} 
took {(end - start) / 1000000} ms")
         verified = gpg.verify_file(sig_file, str(artifact_path))
 
     key_fp = verified.pubkey_fingerprint.lower() if 
verified.pubkey_fingerprint else None
diff --git a/atr/tasks/checks/zipformat.py b/atr/tasks/checks/zipformat.py
index b1363db..cdce89e 100644
--- a/atr/tasks/checks/zipformat.py
+++ b/atr/tasks/checks/zipformat.py
@@ -23,6 +23,7 @@ from typing import Any
 import atr.log as log
 import atr.models.results as results
 import atr.tasks.checks as checks
+import atr.util as util
 
 
 async def integrity(args: checks.FunctionArguments) -> results.Results | None:
@@ -38,7 +39,9 @@ async def integrity(args: checks.FunctionArguments) -> 
results.Results | None:
         if result_data.get("error"):
             await recorder.failure(result_data["error"], result_data)
         else:
-            await recorder.success(f"Zip archive integrity OK 
({result_data['member_count']} members)", result_data)
+            await recorder.success(
+                f"Zip archive integrity OK 
({util.plural(result_data['member_count'], 'member')})", result_data
+            )
     except Exception as e:
         await recorder.failure("Error checking zip integrity", {"error": 
str(e)})
 
diff --git a/atr/util.py b/atr/util.py
index 0838124..c3281f6 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -284,14 +284,6 @@ def create_path_matcher(lines: Iterable[str], full_path: 
pathlib.Path, base_dir:
     return lambda file_path: gitignore_parser.handle_negation(file_path, rules)
 
 
-def is_dev_environment() -> bool:
-    conf = config.get()
-    for development_host in ("127.0.0.1", "localhost.apache.org"):
-        if (conf.APP_HOST == development_host) or 
conf.APP_HOST.startswith(f"{development_host}:"):
-            return True
-    return False
-
-
 def email_from_uid(uid: str) -> str | None:
     if m := re.search(r"<([^>]+)>", uid):
         return m.group(1).lower()
@@ -524,6 +516,14 @@ async def has_files(release: sql.Release) -> bool:
     return False
 
 
+def is_dev_environment() -> bool:
+    conf = config.get()
+    for development_host in ("127.0.0.1", "localhost.apache.org"):
+        if (conf.APP_HOST == development_host) or 
conf.APP_HOST.startswith(f"{development_host}:"):
+            return True
+    return False
+
+
 async def is_dir_resolve(path: pathlib.Path) -> pathlib.Path | None:
     try:
         resolved_path = await asyncio.to_thread(path.resolve)
@@ -688,6 +688,15 @@ def permitted_voting_recipients(asf_uid: str, 
committee_name: str) -> list[str]:
     ]
 
 
+def plural(count: int, singular: str, plural_form: str | None = None, *, 
include_count: bool = True) -> str:
+    if plural_form is None:
+        plural_form = singular + "s"
+    word = singular if (count == 1) else plural_form
+    if include_count:
+        return f"{count} {word}"
+    return word
+
+
 async def read_file_for_viewer(full_path: pathlib.Path, max_size: int) -> 
tuple[str | None, bool, bool, str | None]:
     """Read file content for viewer."""
     content: str | None = None
@@ -759,13 +768,6 @@ def release_directory_base(release: sql.Release) -> 
pathlib.Path:
     return base_dir / project_name / version_name
 
 
-# def release_directory_eventual(release: models.Release) -> pathlib.Path:
-#     """Return the path to the eventual destination of the release files."""
-#     path_project = release.project.name
-#     path_version = release.version
-#     return get_finished_dir() / path_project / path_version
-
-
 def release_directory_revision(release: sql.Release) -> pathlib.Path | None:
     """Return the path to the directory containing the active files for a 
given release phase."""
     path_project = release.project.name
@@ -1024,7 +1026,7 @@ def version_sort_key(version: str) -> bytes:
         if version[i].isdigit():
             # Find the end of this digit sequence
             j = i
-            while j < length and version[j].isdigit():
+            while (j < length) and version[j].isdigit():
                 j += 1
 
             digit_sequence = version[i:j]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to