You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/08/06 11:27:17 UTC
(tooling-trusted-releases) 01/02: Mark required fields required
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 729784621fa07dd9677fb9ee406c999910dbdaac
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Thu Aug 6 12:25:19 2026 +0100
Mark required fields required
---
atr/form.py | 10 +++++-----
atr/shared/announce.py | 7 ++++---
atr/shared/compose.py | 4 ++--
atr/shared/distribution.py | 8 ++++----
atr/shared/keys.py | 4 ++++
atr/shared/manual.py | 8 +++++---
atr/shared/projects.py | 13 ++++++++-----
atr/shared/resolve.py | 8 ++++++--
atr/shared/start.py | 1 +
atr/shared/tokens.py | 4 ++--
atr/shared/upload.py | 1 +
atr/shared/user.py | 2 +-
atr/shared/vote.py | 2 +-
atr/shared/voting.py | 6 +++---
14 files changed, 47 insertions(+), 31 deletions(-)
diff --git a/atr/form.py b/atr/form.py
index 0b496f4e..7e844133 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -186,8 +186,8 @@ def label(
if documentation is not None:
extra["documentation"] = documentation
extra["readonly"] = readonly
- # A field with no default is already marked required from is_required. This flag is for the fields
- # that carry an empty default - a list, an optional - but a validator still makes them mandatory
+ # Marks the field required in the UI. It has to be stated, since is_required over-fires on any field
+ # that simply lacks a default, whether or not the user must actually fill it in
if required:
extra["required"] = True
if len(kwargs) > 0:
@@ -938,9 +938,9 @@ def _render_row( # noqa: C901
label_classes = f"{label_col_class} col-form-label text-sm-end"
label_classes_with_error = f"{label_classes} text-danger" if has_flash_error else label_classes
label_content: list[Any] = [label_text]
- # A field with no default is required, but a collection type carries an empty default even when a
- # validator makes it mandatory, so `required=True` on the label covers what is_required can't see
- if is_required or _label_marks_required(field_info):
+ # Opt-in via required=True, not is_required: a bare field with no default reads as required to
+ # pydantic but still accepts empty input here, so is_required marks far more than is really mandatory
+ if _label_marks_required(field_info):
# The asterisk is hidden from screen readers, which pick the requirement up from the
# visually-hidden note beside it instead
label_content.append(htm.span(".text-danger.ms-1", aria_hidden="true")["*"])
diff --git a/atr/shared/announce.py b/atr/shared/announce.py
index d7c655df..fb14c6fb 100644
--- a/atr/shared/announce.py
+++ b/atr/shared/announce.py
@@ -28,12 +28,12 @@ class AnnounceForm(form.Form):
"""Form for announcing a release preview."""
revision_number: safe.RevisionNumber = form.label("Revision number", widget=form.Widget.HIDDEN)
- email_to: str = form.label("To", widget=form.Widget.CUSTOM)
+ email_to: str = form.label("To", widget=form.Widget.CUSTOM, required=True)
email_cc: form.StrList = form.label("CC")
email_bcc: form.StrList = form.label("BCC")
- subject: str = form.label("Subject", widget=form.Widget.CUSTOM)
+ subject: str = form.label("Subject", widget=form.Widget.CUSTOM, required=True)
subject_template_hash: str = form.label("Subject template hash", widget=form.Widget.HIDDEN)
- body: str = form.label("Body", widget=form.Widget.CUSTOM, max_length=100_000)
+ body: str = form.label("Body", widget=form.Widget.CUSTOM, max_length=100_000, required=True)
download_path_suffix: safe.OptionalRelPath = form.label("Download path suffix", widget=form.Widget.CUSTOM)
download_page: form.OptionalURL = form.label(
"Download page URL",
@@ -56,6 +56,7 @@ class AnnounceForm(form.Form):
confirm_announce: Literal["CONFIRM"] = form.label(
"Confirm",
"Type CONFIRM (in capitals) to enable the submit button.",
+ required=True,
)
@pydantic.field_validator("download_page")
diff --git a/atr/shared/compose.py b/atr/shared/compose.py
index eb0daf72..0b1d652c 100644
--- a/atr/shared/compose.py
+++ b/atr/shared/compose.py
@@ -31,8 +31,8 @@ type MOVE_FILE = Literal["MOVE_FILE"]
class MoveFileForm(form.Form):
variant: MOVE_FILE = form.value(MOVE_FILE)
- source_files: form.RelPathList = form.label("Files to move")
- target_directory: safe.RelPath = form.label("Target directory")
+ source_files: form.RelPathList = form.label("Files to move", required=True)
+ target_directory: safe.RelPath = form.label("Target directory", required=True)
@pydantic.model_validator(mode="after")
def validate_move(self) -> "MoveFileForm":
diff --git a/atr/shared/distribution.py b/atr/shared/distribution.py
index a87606cc..7d574bb6 100644
--- a/atr/shared/distribution.py
+++ b/atr/shared/distribution.py
@@ -157,8 +157,8 @@ class DistributionAutomateForm(form.Form):
"Owner or Namespace",
MAVEN_OWNER_NAMESPACE_HELP,
)
- package: safe.Alphanumeric = form.label("Package")
- version: safe.VersionKey = form.label("Version")
+ package: safe.Alphanumeric = form.label("Package", required=True)
+ version: safe.VersionKey = form.label("Version", required=True)
details: form.Bool = form.label(
"Include details",
"Include the details of the distribution in the response",
@@ -183,8 +183,8 @@ class DistributionRecordForm(form.Form):
"Owner or Namespace",
MAVEN_OWNER_NAMESPACE_HELP,
)
- package: safe.Alphanumeric = form.label("Package")
- version: safe.VersionKey = form.label("Version")
+ package: safe.Alphanumeric = form.label("Package", required=True)
+ version: safe.VersionKey = form.label("Version", required=True)
details: form.Bool = form.label(
"Include details",
"Include the details of the distribution in the response",
diff --git a/atr/shared/keys.py b/atr/shared/keys.py
index e4a7b5ae..aff71430 100644
--- a/atr/shared/keys.py
+++ b/atr/shared/keys.py
@@ -82,6 +82,7 @@ class AddSSHKeyForm(form.Form):
"Your SSH public key should be in the standard format, starting with a key type"
' (like "ssh-rsa" or "ssh-ed25519") followed by the key data.',
widget=form.Widget.TEXTAREA,
+ required=True,
)
@@ -126,12 +127,14 @@ class UploadFileForm(form.Form):
"Upload a KEYS file containing multiple PGP public keys."
" The file should contain keys in ASCII-armored format, starting with"
' "-----BEGIN PGP PUBLIC KEY BLOCK-----".',
+ required=True,
)
selected_committee: str = form.label(
"Associate keys with committee",
"Choose the committee whose KEYS file these keys belong to. Every key in the uploaded file is"
" added to that committee's public KEYS file, which is used to verify the signatures on its releases.",
widget=form.Widget.RADIO,
+ required=True,
)
@pydantic.model_validator(mode="after")
@@ -147,6 +150,7 @@ class UploadRemoteForm(form.Form):
"Committee",
"Choose the committee whose existing KEYS file to fetch from the ASF downloads server and import.",
widget=form.Widget.RADIO,
+ required=True,
)
diff --git a/atr/shared/manual.py b/atr/shared/manual.py
index fc1867d3..716864c6 100644
--- a/atr/shared/manual.py
+++ b/atr/shared/manual.py
@@ -24,9 +24,11 @@ import atr.models.safe as safe
class ResolveVoteForm(form.Form):
- vote_result: Literal["Passed", "Failed", "Cancelled"] = form.label("Vote result", widget=form.Widget.RADIO)
- vote_thread_url: str = form.label("Vote thread URL")
- vote_result_url: str = form.label("Vote result URL")
+ vote_result: Literal["Passed", "Failed", "Cancelled"] = form.label(
+ "Vote result", widget=form.Widget.RADIO, required=True
+ )
+ vote_thread_url: str = form.label("Vote thread URL", required=True)
+ vote_result_url: str = form.label("Vote result URL", required=True)
@pydantic.field_validator("vote_thread_url", "vote_result_url", mode="after")
@classmethod
diff --git a/atr/shared/projects.py b/atr/shared/projects.py
index d3fdbad7..99afffb7 100644
--- a/atr/shared/projects.py
+++ b/atr/shared/projects.py
@@ -122,11 +122,13 @@ class AddProjectForm(form.Form):
"Project name",
'For example, "Example" or "Example Components". Use title case; the "Apache" prefix is added for you.',
prefix="Apache",
+ required=True,
)
key: Annotated[safe.ProjectKey, pydantic.BeforeValidator(_strip_whitespace)] = form.label(
"Project key",
'For example, "example" or "example-components". '
"You must start with your committee key (above), and you must use lower case.",
+ required=True,
)
@pydantic.field_validator("display_name", mode="before")
@@ -295,8 +297,9 @@ class EditMetadataForm(form.Form):
"Project name",
'For example, "Example" or "Example Components". Use title case; the "Apache" prefix is added for you.',
prefix="Apache",
+ required=True,
)
- description: str = form.label(description="Project description", widget=form.Widget.TEXTAREA)
+ description: str = form.label(description="Project description", widget=form.Widget.TEXTAREA, required=True)
short_description: str = form.label(description="Short description", widget=form.Widget.TEXT, default="")
homepage: form.OptionalURL = form.label(
"Homepage",
@@ -557,7 +560,7 @@ class TrustedPublishingPolicyForm(form.Form):
class AddCategoryForm(form.Form):
variant: ADD_CATEGORY = form.value(ADD_CATEGORY)
project_key: safe.ProjectKey = form.label("Project name", widget=form.Widget.HIDDEN)
- category_to_add: str = form.label("New category name")
+ category_to_add: str = form.label("New category name", required=True)
class RemoveCategoryForm(form.Form):
@@ -569,7 +572,7 @@ class RemoveCategoryForm(form.Form):
class AddLanguageForm(form.Form):
variant: ADD_LANGUAGE = form.value(ADD_LANGUAGE)
project_key: safe.ProjectKey = form.label("Project name", widget=form.Widget.HIDDEN)
- language_to_add: str = form.label("New language name")
+ language_to_add: str = form.label("New language name", required=True)
class RemoveLanguageForm(form.Form):
@@ -584,7 +587,7 @@ class ArchiveSelectedProject(form.Form):
class ArchiveSelectedRelease(form.Form):
variant: ARCHIVE_SELECTED_RELEASE = form.value(ARCHIVE_SELECTED_RELEASE)
- confirm_archive: Literal["ARCHIVE"] = form.label("Confirmation", "Type ARCHIVE to confirm.")
+ confirm_archive: Literal["ARCHIVE"] = form.label("Confirmation", "Type ARCHIVE to confirm.", required=True)
class CompleteApprovalRequest(form.Form):
@@ -593,7 +596,7 @@ class CompleteApprovalRequest(form.Form):
class ConfirmReleaseArchival(form.Form):
variant: CONFIRM_RELEASE_ARCHIVAL = form.value(CONFIRM_RELEASE_ARCHIVAL)
- confirm_archive: Literal["ARCHIVE"] = form.label("Confirmation", "Type ARCHIVE to confirm.")
+ confirm_archive: Literal["ARCHIVE"] = form.label("Confirmation", "Type ARCHIVE to confirm.", required=True)
class DeleteSelectedProject(form.Form):
diff --git a/atr/shared/resolve.py b/atr/shared/resolve.py
index 169ccc46..2929e919 100644
--- a/atr/shared/resolve.py
+++ b/atr/shared/resolve.py
@@ -49,10 +49,11 @@ class ResolveFormBase(form.Form):
class CancelSubmitForm(ResolveFormBase):
variant: CANCEL_SUBMIT = form.value(CANCEL_SUBMIT)
- email_body: str = form.label("Email body", widget=form.Widget.TEXTAREA, max_length=100_000)
+ email_body: str = form.label("Email body", widget=form.Widget.TEXTAREA, max_length=100_000, required=True)
confirm_cancel: Literal["CONFIRM"] = form.label(
"Confirm",
"Type CONFIRM (in capitals) to cancel this vote.",
+ required=True,
)
vote_result: Literal["Cancelled"] = form.label("Vote result", default="Cancelled", widget=form.Widget.HIDDEN)
@@ -65,8 +66,11 @@ class SubmitForm(ResolveFormBase):
"{{OUTCOME}} is a placeholder - it will be replaced with the vote result you select when the email is sent.",
widget=form.Widget.TEXTAREA,
max_length=100_000,
+ required=True,
+ )
+ vote_result: Literal["Passed", "Failed", "Cancelled"] = form.label(
+ "Vote result", widget=form.Widget.RADIO, required=True
)
- vote_result: Literal["Passed", "Failed", "Cancelled"] = form.label("Vote result", widget=form.Widget.RADIO)
automatic_resolve_when_finished: form.Bool = form.label(
"Automatically resolve the second round vote",
widget=form.Widget.CUSTOM,
diff --git a/atr/shared/start.py b/atr/shared/start.py
index ac865e54..0e2cc76c 100644
--- a/atr/shared/start.py
+++ b/atr/shared/start.py
@@ -50,6 +50,7 @@ class StartReleaseForm(form.Form):
" and you can also set your own tag before a vote starts."
" Release candidate drafts inactive for 90 days are cleaned up automatically,"
" with a warning email sent at 80 days.",
+ required=True,
)
auto_archive_prior: form.Bool = form.label(
"Auto archive prior release",
diff --git a/atr/shared/tokens.py b/atr/shared/tokens.py
index 6876d787..27251aed 100644
--- a/atr/shared/tokens.py
+++ b/atr/shared/tokens.py
@@ -32,7 +32,7 @@ type DELETE_TOKEN = Literal["delete_token"]
class AddTokenForm(form.Form):
variant: ADD_TOKEN = form.value(ADD_TOKEN)
- label: str = form.label("Label")
+ label: str = form.label("Label", required=True)
@pydantic.field_validator("label", mode="after")
@classmethod
@@ -51,7 +51,7 @@ class DeleteTokenForm(form.Form):
class IssueForm(form.Form):
- pat: str = form.label("PAT", widget=form.Widget.TEXT)
+ pat: str = form.label("PAT", widget=form.Widget.TEXT, required=True)
type TokenForm = Annotated[
diff --git a/atr/shared/upload.py b/atr/shared/upload.py
index 99a88a28..27bc94e0 100644
--- a/atr/shared/upload.py
+++ b/atr/shared/upload.py
@@ -45,6 +45,7 @@ class SvnImportForm(form.Form):
svn_path: safe.RelPath = form.label(
"SVN path",
"Path within the committee's svn:dist directory, e.g. 'java-library/4_0_4' or '3.1.5rc1'.",
+ required=True,
)
revision: str = form.label(
"Revision",
diff --git a/atr/shared/user.py b/atr/shared/user.py
index 0cb7f1fc..868c76a5 100644
--- a/atr/shared/user.py
+++ b/atr/shared/user.py
@@ -33,7 +33,7 @@ class DeleteCacheForm(form.Empty):
class TallyForm(form.Form):
- thread: str = form.label("Thread URL or ID")
+ thread: str = form.label("Thread URL or ID", required=True)
type UserCacheForm = Annotated[
diff --git a/atr/shared/vote.py b/atr/shared/vote.py
index 27c9df68..81ecab76 100644
--- a/atr/shared/vote.py
+++ b/atr/shared/vote.py
@@ -33,7 +33,7 @@ def message_id_source_archive_url(message_id: str, vote_recipient: str) -> str:
class CastVoteForm(form.Form):
- decision: Literal["+1", "0", "-1"] = form.label("Your vote", widget=form.Widget.CUSTOM)
+ decision: Literal["+1", "0", "-1"] = form.label("Your vote", widget=form.Widget.CUSTOM, required=True)
comment: str = form.label("Comment (optional)", widget=form.Widget.TEXTAREA, max_length=50_000)
vote_seq: form.OptionalInt = form.label("Vote serial", default=None, widget=form.Widget.HIDDEN)
vote_mode: sql.VoteMode = form.label("Vote mode", widget=form.Widget.HIDDEN)
diff --git a/atr/shared/voting.py b/atr/shared/voting.py
index a92a2e98..5b1bd66a 100644
--- a/atr/shared/voting.py
+++ b/atr/shared/voting.py
@@ -29,7 +29,7 @@ import atr.util as util
class StartVotingForm(form.Form):
- email_to: str = form.label("To (mailing list)", widget=form.Widget.CUSTOM)
+ email_to: str = form.label("To (mailing list)", widget=form.Widget.CUSTOM, required=True)
email_cc: form.StrList = form.label("CC")
email_bcc: form.StrList = form.label("BCC")
second_round_email_to: str | None = form.label(
@@ -42,9 +42,9 @@ class StartVotingForm(form.Form):
"Minimum number of hours the vote will be open for.",
default=72,
)
- subject: str = form.label("Subject", widget=form.Widget.CUSTOM)
+ subject: str = form.label("Subject", widget=form.Widget.CUSTOM, required=True)
subject_template_hash: str = form.label("Subject template hash", widget=form.Widget.HIDDEN)
- body: str = form.label("Body", widget=form.Widget.CUSTOM, max_length=100_000)
+ body: str = form.label("Body", widget=form.Widget.CUSTOM, max_length=100_000, required=True)
notify_when_finished: form.Bool = form.label(
"Email reminder",
widget=form.Widget.CUSTOM,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org