Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 71 additions & 23 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2724,10 +2724,16 @@ def _kept_collision_notes(args, entries: list, tree: list) -> list[str]:
#: and no elector is one: the manifest writes it between double quotes, which
#: `render` spells with `double_quoted` (#284). The validator's rule (#261)
#: asks about every value, so an elector `git config user.name` leaves empty
#: is named by where it came from as well (#529).
#: is named by where it came from as well (#529), and so is an organisation
#: read from the source's `origin` (L1-39): a url can end in `~x`, which
#: `checked_value` takes and the validator refuses, and that refusal said
#: `--org is '~x'`. The words are `_planned_org`'s for the same value; a
#: source `plan` cloned has the `--source` it was cloned from as its
#: `origin`.
FLAG_DEFAULTS = {"id": "--project lowercased",
TRACKING_KEY: "the source's default branch",
ELECTED_BY_KEY: "`git config user.name`"}
ELECTED_BY_KEY: "`git config user.name`",
"org": "the organisation of the source's `origin`"}
#: How a sentence names the id `execute` derives when the plan sets none:
#: when it has no `id:`, or an `id:` with no value (`null`, `~` or nothing,
#: as `main`'s `plan --id null` wrote it), which `Plan.get` reads alike.
Expand Down Expand Up @@ -3362,22 +3368,57 @@ def _refuse_a_tracking_branch_the_remote_lacks(urls: dict, tracking: str,
REMOTE_LACKS_REMEDIATION + (LOCAL_REMOTE_LACKS if local else ""))


def _refuse_bad_id(project_id: str, project: str, passed: bool) -> None:
#: The name the naming policy takes for an assembly root, in the words
#: `naming-unclassified` says it in (`_check_names`). A derived id's remedy
#: says the same (#658): "change --project" alone sent the person to
#: `North-Wind`, which `naming-unclassified` then refused.
PROJECT_NAME_RULE = ("one CamelCase token with no hyphen, underscore, dot or "
"space")


def _refuse_bad_id(project_id: str, project: str, given: set,
source: Source) -> None:
"""`plan`'s refusal of an id the project's validators would not take
(`PROJECT_ID_RE`), in the words of where it came from. A `--id` the person
passed is named as the flag they typed, with the value as typed. One
`plan` derived from `--project` (lowercased, `FLAG_DEFAULTS`) is named as
derived, the way `_flag_named` names the tracking branch and the elector
it fills in (#529): `--id 'north_wind' must match` named a flag nobody
had passed (#601). The finding's code is the same for both."""
(`PROJECT_ID_RE`), in the words of where it came from. `given` is
`flags_given`. A `--id` the person passed, `''` among them (L2-53), is
named as the flag they typed, with the value as typed, and the remedy is
another `--id`. One `plan` derived from `--project` is named by
`_flag_named` from `FLAG_DEFAULTS`, as the tracking branch and the
elector it fills in are (#529): `--id 'north_wind' must match` named a
flag nobody had passed (#601). The finding's code is the same for both.

A DERIVED ID IS MENDED IN `--project`, NOT WITH `--id` (L2-54). It is
refused only for a `--project` holding a character no id takes, and the
naming policy reads `--project` itself, as all three repositories'
names: "pass an explicit lowercase --id" led to `naming-unclassified`.
So the remedy is to change `--project`, to what the naming policy takes
(`PROJECT_NAME_RULE`, the words of `naming-unclassified`); and where the
source names a repository and `--project` is its name, adopted in place,
that name is not the person's to retype, and the remedy is
`_rename_first`'s, after the same rule. A source `plan` cloned names its
repository as an `origin` does (`Source.repository`)."""
if PROJECT_ID_RE.match(project_id):
return
pattern = PROJECT_ID_RE.pattern
what = (f"--id {project_id!r} must match {pattern}" if passed
else f"the id derived from --project {project!r} (--id was not "
f"given) is {project_id!r}, which must match {pattern}")
raise Refusal("adopt-bad-id", what,
"Remediation: pass an explicit lowercase --id.")
flag, pattern = _flag_named("id", given), PROJECT_ID_RE.pattern
if "id" in given:
raise Refusal("adopt-bad-id",
f"{flag} {project_id!r} must match {pattern}",
"Remediation: pass an explicit lowercase --id.")
derived = FLAG_DEFAULTS["id"]
repository = source.repository
if repository is not None and _is_the_source(
repository, repository.partition("/")[0], project):
remedy = _rename_first(repository, f"the id is {derived}, and "
"--project is the source's own name, which "
f"must be {PROJECT_NAME_RULE}. ")
else:
remedy = (f"Remediation: change --project to {PROJECT_NAME_RULE}. "
f"The id is {derived}, and an explicit --id would not "
"change --project, which also names the assembly root and "
"both legs.")
raise Refusal("adopt-bad-id",
f"{flag} is {project_id!r}, which must match {pattern}",
remedy)


def _checked_plan_inputs(args, source: Source,
Expand All @@ -3394,7 +3435,15 @@ def _checked_plan_inputs(args, source: Source,
# Which of the flags `plan` fills in by itself were passed: a value it
# filled in is named by where it came from, should it be refused below.
# Not `given`, which #479 binds below to whether `--tracking-branch` was.
flags_given = {key for key in FLAG_DEFAULTS if getattr(args, key)}
# PASSED IS ON THE COMMAND LINE, `''` INCLUDED (L2-53), as #479 reads
# `--tracking-branch`: `--id ''` and `--elected-by ''` were read as not
# passed, so `--id "$ID"` with `ID` unset planned the derived id, and an
# empty elector became `git config user.name` though `--help` says "not
# blank". Each is a value typed, held to its flag's own rule below and
# refused there by name. (`_planned_org` still reads `--org ''` as no
# `--org`, as it did.)
flags_given = {key for key in FLAG_DEFAULTS
if getattr(args, key) is not None}
args.project = checked_value("--project", args.project)
# THE SOURCE'S OWN BRANCH, UNLESS ONE IS NAMED (#479): the branch `plan`
# reads the source at is the one its pull request can be opened against.
Expand Down Expand Up @@ -3437,10 +3486,9 @@ def _checked_plan_inputs(args, source: Source,
args.spec_path = checked_value(*spec)
args.code_path = checked_value(*code)
# Whether `--id` was passed is read BEFORE it is filled in: a refusal of
# the id names where it came from (#601).
id_passed = "id" in flags_given
args.id = args.id or args.project.lower()
_refuse_bad_id(args.id, args.project, id_passed)
# the id names where it came from (#601), and says what to change.
args.id = args.id if "id" in flags_given else args.project.lower()
_refuse_bad_id(args.id, args.project, flags_given, source)
# The organisation, given or read from the source's `origin`, and named
# by which of the two it is should it be refused (#292, #427).
org_given = bool(args.org)
Expand All @@ -3459,7 +3507,8 @@ def _checked_plan_inputs(args, source: Source,
# A value `plan` filled in by itself is named by where it came from, as
# #181 names one, and not as a flag the person never passed.
elector = _flag_named(ELECTED_BY_KEY, flags_given)
args.elected_by = args.elected_by or _elector()
args.elected_by = (args.elected_by if ELECTED_BY_KEY in flags_given
else _elector())
# Before the plan is written, in the words `check` would use (#255): a
# value `execute` writes into a file must be text one can hold. The
# date has had its answer above, `election_date`.
Expand Down Expand Up @@ -11352,8 +11401,7 @@ def _check_names(policy: NamingPolicy, names: dict[str, str],
"naming-unclassified",
f"{name!r} matches no family in the naming policy",
renamed or
"Remediation: --project takes one CamelCase token with no "
"hyphen, underscore, dot or space.")
f"Remediation: --project takes {PROJECT_NAME_RULE}.")
if not accepts_role(found, role):
raise Refusal(
"naming-role-mismatch",
Expand Down
26 changes: 26 additions & 0 deletions tests/test_adopt_git_reads.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,32 @@ def test_plan_records_the_elector_given_where_user_name_is_unset(tmp_path):
assert load_yaml(tmp_path / PLAN_FILE)["elected_by"] == "Test Human"


#: What `plan` says of a `--elected-by ''` the person passed (L2-53): the
#: flag as typed, in the validator's words for an empty elector.
EMPTY_ELECTOR = (b"REFUSED adopt-manifest-invalid: --elected-by is '', which "
b"the project's own validate-manifest.py refuses: "
b"manifest-elected-by: elected_by is empty.")


@pytest.mark.parametrize("user_name", ["unset", "configured"])
def test_plan_refuses_an_empty_elector_that_was_passed(tmp_path, user_name):
"""L2-53. `--elected-by ''` was read as not passed: with a `git config
user.name` the plan recorded that name, exit 0, though `--help` says
"not blank"; with none, the refusal said `--elected-by (not given, so
`git config user.name`) is ''` of a flag the person passed. It is the
flag's own value now, refused by name as a blank one is (#261), and
`git config user.name` does not stand in for it."""
extra = (no_user_name(tmp_path, "unset") if user_name == "unset"
else global_user_name(tmp_path, b"Jose"))
source = make_source_repo(tmp_path / "Thing")
refused = by_git_config(tmp_path, source, extra, "--elected-by", "")
assert_no_traceback_and_no_leg(tmp_path, {"plan": refused})
assert refused.returncode == 2, said(refused)
assert EMPTY_ELECTOR in refused.stderr, said(refused)
assert b"not given" not in refused.stderr, said(refused)
assert not (tmp_path / PLAN_FILE).exists()


@pytest.mark.parametrize("name", [b"Jose", "José".encode("utf-8")],
ids=["ascii", "utf8"])
def test_plan_records_a_user_name_that_is_utf8(tmp_path, name):
Expand Down
27 changes: 21 additions & 6 deletions tests/test_adopt_manifest_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,14 +407,16 @@


def test_a_value_plan_filled_in_is_named_by_where_it_came_from(adopter):
"""#529: the three values `plan` fills in when its flag is not given are
named by the default, in the words `_flag_fields` and the unwritable-value
path use, and no other value is renamed. Two of the three (the id, the
tracking branch) are refused before the validator is asked, so this is
where their names are held."""
"""#529 and L1-39: the four values `plan` fills in when its flag is not
given are named by the default, in the words `_flag_fields` and the
unwritable-value path use, and no other value is renamed. Two of the four
(the id, the tracking branch) are refused before the validator is asked,
so this is where their names are held. The organisation is in
`_planned_org`'s words: `--org is '~x'` named a flag nobody passed."""
defaults = {"ELECTED_BY": "`git config user.name`",
"PROJECT_ID": "--project lowercased",
"TRACKING_BRANCH": "the source's default branch"}
"TRACKING_BRANCH": "the source's default branch",
"ORG": "the organisation of the source's `origin`"}
typed = adopter._manifest_flag_names(set(adopter.FLAG_DEFAULTS))
filled = adopter._manifest_flag_names(set())
assert filled == {
Expand All @@ -427,6 +429,19 @@
"--elected-by")


def test_the_derived_ids_words_are_the_tables(adopter, monkeypatch):
"""rv601's nit, L2-54: #620 wrote the derived id's sentence by hand, so
an edit to `FLAG_DEFAULTS["id"]` would not have reached it. Its label
and its remedy are both read from the table."""
monkeypatch.setitem(adopter.FLAG_DEFAULTS, "id", "a stand-in")
with pytest.raises(adopter.Refusal) as refused:

Check warning on line 437 in tests/test_adopt_manifest_values.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaFE364RxY3cDb_Iu6eT&open=AaFE364RxY3cDb_Iu6eT&pullRequest=699
adopter._refuse_bad_id("north_wind", "North_Wind", set(),
Namespace(repository=None))
assert refused.value.detail.startswith(
"--id (not given, so a stand-in) is 'north_wind'"), refused.value
assert "The id is a stand-in, and" in refused.value.remediation


# --- the question is asked with no git at all -------------------------------
#
# The manifest is rendered here to be ASKED ABOUT and never written: the
Expand Down
22 changes: 22 additions & 0 deletions tests/test_adopt_origin_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,28 @@ def test_an_org_read_from_origin_is_refused_by_where_it_came_from(
assert not (tmp_path / PLAN).exists()


@pytest.mark.parametrize("org, named", [(None, ORG_FROM_ORIGIN),
("~x", "--org")],
ids=["read-from-origin", "passed"])
def test_an_org_the_validator_refuses_is_named_by_where_it_came_from(
tmp_path, org, named):
"""L1-39, #427's pattern in the validator's refusal: a url can end in an
organisation `checked_value` takes and the project's own validator
refuses, `~x`, and with no `--org` that refusal said `--org is '~x'`, a
flag nobody passed. It names the value by where it came from, in the
words `checked_value`'s refusal above uses; a `--org ~x` that was
passed is named as the flag."""
source = with_origin(tmp_path, f"https://example.invalid/~x/{OTHER}.git")

refused = planned(tmp_path, source, OTHER, org)

assert refused.returncode == 2, refused.stdout + refused.stderr
assert refused.stderr.splitlines()[0].startswith(
f"REFUSED adopt-manifest-invalid: {named} is '~x', which the "
"project's own validate-manifest.py refuses: "), refused.stderr
assert not (tmp_path / PLAN).exists()


@pytest.mark.parametrize("directory", ["caf\u00e9", "my repos"],
ids=["not-ascii", "space"])
def test_a_path_origin_names_no_organisation(tmp_path, directory):
Expand Down
Loading
Loading