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
208 changes: 154 additions & 54 deletions scaffold-project.py

Large diffs are not rendered by default.

223 changes: 160 additions & 63 deletions scripts/family.py

Large diffs are not rendered by default.

68 changes: 63 additions & 5 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,10 @@ def readable_branch(what: str, value) -> str:
#: empty working tree) alike.
CANNOT_WRITE_IN = "is a folder this user cannot write in"

#: How `target_in_the_way` begins its answer for a working tree this user
#: cannot list (#930): `cannot be read: <the system's words>`.
CANNOT_READ = "cannot be read"

#: Said when the real run's own `mkdir` fails, after the question said yes:
#: the run has made folders by then, and no remote.
MKDIR_REMEDIATION = (
Expand Down Expand Up @@ -1595,6 +1599,18 @@ def _unwritable_remediation(choose: str) -> str:
f"folder, or choose another {choose}, and run this again.")


def _unreadable_remediation(choose: str) -> str:
"""What a refusal of a working tree this user cannot list says to do
(#930), after `_unwritable_remediation`: the run cannot tell whether the
folder holds a tree, so it names the permission that would let it ask,
and the flag `choose` that chooses another folder."""
return ("Remediation: nothing was created, and `--dry-run` refuses the "
"same. This user cannot list that folder, so whether it holds a "
"tree cannot be asked: give this user read, write and search "
f"permission on it, or choose another {choose}, and run this "
"again.")


def refuse_unmakeable_folder(refusal: str, named: str, path: Path,
choose: str | None = None) -> None:
"""Raise `Refusal(refusal)` when `folder_cannot_be_made(path)` says why.
Expand Down Expand Up @@ -1661,7 +1677,7 @@ def target_in_the_way(target: Path) -> str | None:
if any(target.iterdir()):
return "is not empty"
except OSError as exc:
return f"cannot be read: {exc.strerror or exc}"
return f"{CANNOT_READ}: {exc.strerror or exc}"
if not os.access(target, os.W_OK | os.X_OK):
return CANNOT_WRITE_IN
return None
Expand All @@ -1676,10 +1692,19 @@ def target_remediation(why: str, holds: str, choose: str) -> str:
for an EMPTY folder whose only fault is its permissions
(`CANNOT_WRITE_IN`, L2-60), which is told to be given write and search
permission for this user, or to be replaced by another folder, named by
`choose`, the flag the person met it by."""
if why != CANNOT_WRITE_IN:
return holds
return _unwritable_remediation(choose)
`choose`, the flag the person met it by.

AND FOR A FOLDER THIS USER CANNOT LIST (`CANNOT_READ`, #930). An empty
folder at mode 000, 100, 200 or 300 was told "choose an empty
--work-dir. There is no --force: re-running over a live tree is not a
scaffold", a cause the run cannot see: it cannot tell an empty folder
from a live tree there, and the one cause it can see is a permission.
It is told what `_unreadable_remediation` says, naming `choose`."""
if why == CANNOT_WRITE_IN:
return _unwritable_remediation(choose)
if why.startswith(f"{CANNOT_READ}: "):
return _unreadable_remediation(choose)
return holds


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -3699,6 +3724,39 @@ def pin_source_path(typed: str | None, *, nothing_made: str) -> Path | None:
return Path(typed)


def folder_flag_path(typed, flag: str, code: str, *, names: str,
absent: str) -> Path | None:
"""A folder flag as the `Path` the tool reads, or None when it was not
given; a value that names no path is refused as `code`.

`pin_source_path`'s question, asked of the folders the scaffold and
`family.py init` make things in (#972's review, S2): `--work-dir` and
`--local-remote-dir` in both, and `init`'s `--into`. Each was parsed as
a `Path`, and `Path("")` is `Path(".")`, so a flag passed empty was read
as the directory the tool ran in, without a word: from the root of a
checkout of openRepoShape, `init --into ''` planned the holder inside
it, where no `--into` is refused as `family-landing-in-standard`. Each
tool takes them as TEXT now and asks here, before anything exists and
before any process starts, under `--dry-run` alike. A typed `.` still
means the current directory, and a flag not given keeps its default.

`names` says what the folder is for and `absent` what the run does with
no flag, each in the tool's words, so the remedy names the three ways
on: a path, `.`, or no flag at all.
"""
if typed is None:
return None
if not str(typed):
raise Refusal(
code,
f"`{flag} ''` names no path. An empty path would be read as the "
"directory this runs in.",
"Remediation: nothing was created, and `--dry-run` refuses the "
f"same. Give {names} (`{flag} <dir>`), or `.` to mean this "
f"directory; with no {flag}, {absent}.")
return Path(typed)


def refuse_an_unread_pin_source(pin_source, pins, chain=None, *,
nothing_made: str) -> None:
"""`--pin-source` is read by this run, or this refuses it as
Expand Down
192 changes: 155 additions & 37 deletions tests/test_derived_id_refusals.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@
(not given, so --project lowercased)`, the label each tool's manifest
refusals already give it, and its remedy is to change the flag it came from,
which also names what the run makes. A `--id` the person passed keeps
today's sentence and remedy, value as typed, even when it equals the derived
one: what is read is whether the flag was passed, not what its value is. An
empty `--id ''` derives the id as no `--id` does (`args.id or ...`), and is
named as empty, because it was passed.
today's sentence, value as typed, even when it equals the derived one: what
is read is whether the flag was passed, not what its value is.

AND #911 (#459's U156, U158, U159 and U162). A passed `--id` was told "pass
an explicit lowercase --id" whatever its fault, `foo_bar` included, which is
both; it is told what an id may hold now (`PROJECT_ID_RE` in words). A
passed one was refused before the name, so `--project North_Wind --id
north_wind` was sent to retype the id and then met `naming-unclassified`;
the name speaks first now. An empty `--id ''` derived the id as no `--id`
does (`args.id or ...`); it is a passed value now, refused by name. And
`init`'s derived remedy led `--family open_glass` to `openGlass`, which
`naming-not-a-family` refuses; the rule it names says so now.

Every run is a `--dry-run` into `--local-remote-dir`, which never calls `gh`.
"""
Expand All @@ -40,21 +48,31 @@
PATTERN = PROJECT_ID_RE.pattern

#: The rule each tool's `naming-unclassified` holds a name to, in its own
#: words: the scaffold's and `init`'s are the same.
#: words: the scaffold's is the token rule; `init`'s adds the two forms no
#: holder can be declared as (#911), in `naming-not-a-family`'s words.
NAME_RULE = "one CamelCase token with no hyphen, underscore, dot or space"
NOT_A_HOLDER = "and not an `open<Product>` or `<X>-Install` name"

#: Per tool: the command, the flag naming whoever creates it, the flag the
#: id is derived from and what else that flag names, and the finding's code.
#: id is derived from and what else that flag names, the finding's code, and
#: the naming rule its derived remedy names.
TOOLS = {
"scaffold": {"command": (SCAFFOLD,), "who": "--elected-by",
"flag": "--project", "code": "scaffold-bad-id",
"names": "the assembly root and both legs"},
"names": "the assembly root and both legs",
"rule": NAME_RULE},
"family": {"command": (FAMILY, "init"), "who": "--created-by",
"flag": "--family", "code": "family-bad-id",
"names": "the holder and, unless --work-dir is given, its "
"family folder"},
"family folder",
"rule": f"{NAME_RULE}, {NOT_A_HOLDER}"},
}
PASSED_REMEDY = "Remediation: pass an explicit lowercase --id."
#: What a passed `--id` is told (#911): `PROJECT_ID_RE` in words, the same
#: sentence from both tools.
PASSED_REMEDY = ("Remediation: pass an --id of lowercase letters, digits and "
"hyphens, starting with a letter or digit.")
#: The remedy both tools gave a passed `--id` before #911.
OLD_PASSED_REMEDY = "Remediation: pass an explicit lowercase --id."


def derived(tool: str, unset: str) -> str:
Expand All @@ -67,9 +85,9 @@ def derived_remedy(tool: str) -> str:
"""The remedy `tool` gives for an id derived from its flag: change the
flag, to a name the naming policy takes."""
flag = TOOLS[tool]["flag"]
return (f"Remediation: change {flag} to {NAME_RULE}. The id is {flag} "
f"lowercased, and an explicit --id would not change {flag}, "
f"which also names {TOOLS[tool]['names']}.")
return (f"Remediation: change {flag} to {TOOLS[tool]['rule']}. The id is "
f"{flag} lowercased, and an explicit --id would not change "
f"{flag}, which also names {TOOLS[tool]['names']}.")


#: Every character a name may hold on a command line (`SAFE_ARG_RE`, which
Expand Down Expand Up @@ -102,11 +120,13 @@ def refusal(tool: str, what: str, remedy: str) -> str:

def assert_named_as_passed(tool: str, stderr: str, value: str) -> None:
"""A passed `--id` gets none of a derived id's words, neither its label
("not given") nor its remedy ("change --project"), and keeps today's
sentence and remedy, value as typed. The derived words are asked first,
so a run that gives a passed id either of them fails on that word."""
("not given") nor its remedy ("change --project"), and keeps its
sentence, value as typed, with the remedy that says what an id may hold
(#911). The derived words are asked first, so a run that gives a passed
id either of them fails on that word."""
assert "not given" not in stderr
assert f"change {TOOLS[tool]['flag']}" not in stderr
assert OLD_PASSED_REMEDY not in stderr
assert refusal(tool, f"--id {value!r} must match {PATTERN}",
PASSED_REMEDY) in stderr

Expand All @@ -125,30 +145,51 @@ def test_a_derived_id_is_named_by_where_it_came_from_and_sent_to_that_flag(


@pytest.mark.parametrize("tool", TOOLS)
def test_an_empty_id_is_named_as_empty_and_not_as_not_given(tmp_path, tool):
"""`--id ''` was passed, so "not given" would be false of it; the id is
still the derived one, and the remedy is still the flag's."""
stderr = run_tool(tmp_path, tool, "North_Wind", "--id=")
assert refusal(
tool,
f"{derived(tool, 'empty')} is 'north_wind', which must match "
f"{PATTERN}",
derived_remedy(tool)) in stderr
assert "not given" not in stderr
def test_an_empty_id_is_a_passed_id_refused_by_name(tmp_path, tool):
"""#911 (U156): `--id ''` was read by its truth, so it planned the
derived id and exited 0, and a script's `--id "$ID"` with `ID` unset
went through without a word. It is a passed value now, held to the
id's rule and refused by name, before `checked_value` would call it
`unsafe-value`."""
stderr = run_tool(tmp_path, tool, "NorthWind", "--id=")
assert_named_as_passed(tool, stderr, "")
assert "empty, so" not in stderr
assert "unsafe-value" not in stderr


@pytest.mark.parametrize("tool", TOOLS)
def test_a_passed_id_equal_to_the_derived_one_is_still_named_as_passed(
tmp_path, tool):
"""Passed means typed, not "differs from the name lowercased"."""
stderr = run_tool(tmp_path, tool, "North_Wind", "--id", "north_wind")
assert_named_as_passed(tool, stderr, "north_wind")
@pytest.mark.parametrize("value",
["Foo_Bar", "foo_bar", "-north", "north wind"])
def test_a_passed_bad_id_is_told_what_an_id_may_hold(tmp_path, tool, value):
"""#911 (U158): `foo_bar` is explicit and lowercase already, and was
told to be both; its fault is the underscore. Each row is refused as
the id it is not, a leading hyphen and a space included, which
`checked_value` refused as `unsafe-value` before the id's own rule was
asked."""
stderr = run_tool(tmp_path, tool, "NorthWind", f"--id={value}")
assert_named_as_passed(tool, stderr, value)
assert "unsafe-value" not in stderr


@pytest.mark.parametrize("tool", TOOLS)
def test_a_passed_bad_id_keeps_its_sentence_and_its_remedy(tmp_path, tool):
stderr = run_tool(tmp_path, tool, "NorthWind", "--id", "Foo_Bar")
assert_named_as_passed(tool, stderr, "Foo_Bar")
def test_an_id_the_id_rule_lets_through_still_meets_the_alphabet(
tmp_path, tool):
"""`PROJECT_ID_RE.match` takes a trailing line break (`$` matches before
one), so the id is still held to `checked_value`'s alphabet after its
own rule, and refused as before."""
stderr = run_tool(tmp_path, tool, "NorthWind", "--id=north" + chr(10))
assert "REFUSED unsafe-value: --id is 'north" in stderr
assert TOOLS[tool]["code"] not in stderr


def test_the_passed_remedy_is_the_id_rule_in_words():
"""The words say what `PROJECT_ID_RE` holds: lowercase letters, digits
and hyphens, the first a letter or a digit."""
assert PATTERN == "^[a-z0-9][a-z0-9-]*$"
for good in ("a", "0", "north-wind", "9-lives", "a--b"):
assert PROJECT_ID_RE.match(good), good
for bad in ("", "-a", "A", "a_b", "a.b", "a b"):
assert not PROJECT_ID_RE.match(bad), bad


def test_the_characters_a_derived_id_is_refused_for():
Expand All @@ -158,17 +199,94 @@ def test_the_characters_a_derived_id_is_refused_for():


@pytest.mark.parametrize("tool", TOOLS)
@pytest.mark.parametrize("passed", ["north-wind", "north_wind", ""])
@pytest.mark.parametrize("char", REFUSED_IN_AN_ID)
def test_a_name_whose_derived_id_is_refused_is_refused_by_the_naming_policy(
tmp_path, tool, char):
tmp_path, tool, char, passed):
"""Why a derived id's remedy is its flag's and not `--id`'s: the naming
policy reads the name itself and refuses every one whose lowercase is no
id, so a `--id` that is an id only meets that refusal. Run with one, so
this is the run the old remedy led to. And the refusal states the rule
the derived remedy names, in the same words."""
the derived remedy names, in the same words.

A REFUSED NAME SPEAKS FIRST, WHATEVER THE ID (#911, U159): with a
passed id that is no id either (`north_wind`, `''`) the id was refused
first, and its remedy's retype met this refusal; now this one comes
first and the id's does not appear."""
name = f"North{char}Wind"
stderr = run_tool(tmp_path, tool, name, "--id", "north-wind")
stderr = run_tool(tmp_path, tool, name, f"--id={passed}")
assert (f"REFUSED naming-unclassified: {name!r} matches no family in the "
"naming policy") in stderr
assert NAME_RULE in stderr
assert TOOLS[tool]["rule"] in stderr
assert TOOLS[tool]["code"] not in stderr
assert PASSED_REMEDY not in stderr


@pytest.mark.parametrize("name, form", [("Foo-Install", "install"),
("Atlas-spec", "project-leg/spec"),
("Atlas-code", "project-leg/code")])
def test_a_form_no_assembly_root_has_speaks_before_a_passed_id(tmp_path, name,
form):
"""The scaffold's half (#911): a `--project` the policy holds in no form
an assembly root may have, with nothing declared, is refused as
`naming-role-mismatch` before a passed id that is no id, as it is after
a good one; no `--id` mends it. `Foo-Install` is decided by its
characters alone. `Atlas-spec` and `Atlas-code` are a leg's form with a
pin or without, and were refused for the id first, whose retype then
met this refusal (#972's review, M1)."""
for passed in ("foo_bar", "foo-bar"):
stderr = run_tool(tmp_path / passed, "scaffold", name,
f"--id={passed}")
assert (f"REFUSED naming-role-mismatch: {name!r} classifies as "
f"{form}, not as the 'assembly' form of a project leg"
) in stderr
assert "scaffold-bad-id" not in stderr


@pytest.mark.parametrize("name", ["openGlass", "MedxGlass"])
def test_a_name_the_scaffold_takes_leaves_the_passed_id_refused(tmp_path,
name):
"""THE CONTROL: a neutral product may be an assembly root, and a
descendant form's role turns on what the project declares, which is
read later; neither is refused early, so the passed id is."""
stderr = run_tool(tmp_path, "scaffold", name, "--id=foo_bar")
assert_named_as_passed("scaffold", stderr, "foo_bar")
assert "naming-" not in stderr


@pytest.mark.parametrize("name, refused", [
pytest.param("openGlass", "REFUSED naming-not-a-family: 'openGlass' "
"classifies as neutral-product", id="openGlass"),
pytest.param("NUL", "REFUSED family-bad-holder-name: --family is 'NUL', "
"which names the holder and the folders `init` makes for "
"it, and ", id="NUL")])
def test_a_name_no_holder_can_have_speaks_before_a_passed_id(tmp_path, name,
refused):
"""`init` holds `--family` to the holder's form whole, so a name that
classifies as another form, `openGlass`, is refused as
`naming-not-a-family` before a passed id that is no id (#911); and a
name no Windows disk can make a folder of, `NUL`, as
`family-bad-holder-name`, which the passed id spoke before (#972's
review, S1). No `--id` mends either."""
stderr = run_tool(tmp_path, "family", name, "--id=foo_bar")
assert refused in stderr
assert "family-bad-id" not in stderr


def test_init_s_derived_remedy_rules_out_what_naming_not_a_family_refuses(
tmp_path):
"""#911 (U162): `--family open_glass` was told to change `--family` to
one CamelCase token, and `openGlass`, which is one, was then refused as
`naming-not-a-family`. The remedy names the two forms no holder can be
declared as, in that refusal's own words, and the retype it ruled out
is still refused there."""
stderr = run_tool(tmp_path, "family", "open_glass")
assert refusal(
"family",
f"{derived('family', 'not given')} is 'open_glass', which must match "
f"{PATTERN}",
derived_remedy("family")) in stderr
assert NOT_A_HOLDER in derived_remedy("family")
retyped = run_tool(tmp_path / "retyped", "family", "openGlass")
assert "REFUSED naming-not-a-family: 'openGlass' " in retyped
assert "An `open<Product>` or `<X>-Install` name" in retyped
Loading
Loading