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
25 changes: 21 additions & 4 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,11 +1068,28 @@ def _refuse_an_unwritable_clone_url(local: bool, url: str) -> None:
CLONE_URL_REMEDIATION)


def referent_chain_links(raw: str) -> tuple:
def referent_chain_links(raw: str, pins=()) -> tuple:
"""`--referent-chain`'s links: split on commas, each stripped, an empty
one dropped. ONE reading, for `_declaration` and for the check below, so
the links that are judged are the links that are recorded."""
return tuple(link.strip() for link in raw.split(",") if link.strip())
the links that are judged are the links that are recorded.

A FIRST LINK THAT IS A `--pin` IS SPELLED AS THAT PIN (#909). The first
link is matched to the `--pin` products, `pins`, by `pin_product_key`,
the pin file's own fold (#444), so `open<U+212A KELVIN SIGN>dox` is the
pin `openKdox`: `lower()` folds the Kelvin sign to `k`. It was recorded
as typed, and the plan printed that spelling, beside one pin file,
`contracts/openkdox-pin.yaml`. It is recorded in the `--pin`'s spelling
now, and so is a first link differing from it by case alone. An owner
typed before the name is kept: the manifest records the name alone."""
# The fold `resolve_referent` matches the first link to a pin by.
from repo_shape import pin_product_key
links = tuple(link.strip() for link in raw.split(",") if link.strip())
if not links:
return links
head, slash, first = links[0].rpartition("/")
pinned = next((pin for pin in pins
if pin_product_key(pin) == pin_product_key(first)), None)
return links if pinned is None else (head + slash + pinned, *links[1:])


def _refuse_malformed_chain_links(policy, chain: tuple) -> None:
Expand Down Expand Up @@ -1161,7 +1178,7 @@ def _declaration(args) -> tuple[dict, set, tuple, dict]:
"""
pins = declared_pin_values(args)
declared_pins: set[str] = set(pins)
chain = referent_chain_links(args.referent_chain)
chain = referent_chain_links(args.referent_chain, declared_pins)
link_pins = link_pins_from_trees(
chain, None, {}, args.pin_source, with_reasons=True,
remedies=_link_remedies(chain)) if chain else {}
Expand Down
54 changes: 45 additions & 9 deletions scripts/repo_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,10 @@ def recorded_gitlink(repo: Path, path: str) -> str | None:
#: files. Every existing name is kept, because an operator may have set it and
#: the docs name it. What is done instead (`pin_source_env_collisions`): a
#: variable two declared products share answers for NEITHER, wherever it is
#: read; `scaffold-project.py --pin` refuses to create such a pair; and
#: read, and a chain reader asks that over every product its run reads, the
#: declared pins and every chain's links (#909, `link_pins_from_trees`'
#: `run_products`); `scaffold-project.py --pin` refuses to create such a
#: pair; and
#: `validate-pins.py` NOTES a pair a project already declares, and never fails
#: it, because the project did nothing wrong that an update could undo.
PIN_SOURCE_ENV_PREFIX = "SHAPE_PIN_SOURCE_"
Expand Down Expand Up @@ -1762,15 +1765,25 @@ def _chain_link_findings(chain, link_pins) -> tuple:
(#444): a tree declaring `openss` does not declare `openß`, whose pin
file is another. A holder's `remedies` entry is found by it too, so in a
chain naming both, each is offered its own sources and not the other's.

THE VARIABLE IS OFFERED AS IT WAS READ (#909). A `LinkPins` carries
`env_shared`, the links whose variable was not read because another
product of the RUN shares it: a declared pin, or a link of this chain or
of another one the same run reads (`link_pins_from_trees`'
`run_products`). Asked over this chain alone, two legs whose chains
begin at `openß` and `openss` were each offered
`SHAPE_PIN_SOURCE_OPENSS`, which would have answered for both.
"""
available = _link_declarations(link_pins)
unreadable = getattr(link_pins, "unreadable", None) or {}
remedies = getattr(link_pins, "remedies", None) or {}
warnings: list[str] = []
unverified: list[str] = []
# A link whose variable another link of the chain shares is never offered
# that variable: it would be read for both (#275).
shared = pin_source_env_shared(chain)
# A link whose variable another link of the chain, or another product of
# the run, shares is never offered that variable: it would be read for
# both (#275, #909).
shared = (pin_source_env_shared(chain)
| set(getattr(link_pins, "env_shared", None) or ()))
for holder, held in zip(chain, chain[1:]):
declared = available.get(pin_product_key(holder))
why = unreadable.get(pin_product_key(holder))
Expand Down Expand Up @@ -3220,13 +3233,20 @@ class LinkPins(dict):
and has no `--link-source`, so it names its own here. A link with no
entry, and every link for a caller that gives none, gets the naming
validator's words, as ever.

`env_shared`, a set of `pin_product_key(link)`, is the links whose
`SHAPE_PIN_SOURCE_<PRODUCT>` was not read because another product the
run reads shares it (#275, #909), so that `_chain_link_findings` offers
that variable to none of them.
"""

def __init__(self, found=(), unreadable: dict | None = None,
remedies: dict | None = None) -> None:
remedies: dict | None = None,
env_shared=None) -> None:
super().__init__(found)
self.unreadable: dict[str, str] = dict(unreadable or {})
self.remedies: dict[str, str] = dict(remedies or {})
self.env_shared: set[str] = set(env_shared or ())


def link_pins_from_trees(links, root: Path | None = None,
Expand All @@ -3235,7 +3255,8 @@ def link_pins_from_trees(links, root: Path | None = None,
key: str = CHAIN_LINK_DECLARED_BY, *,
tried: dict | None = None,
with_reasons: bool = False,
remedies: dict | None = None) -> dict[str, set]:
remedies: dict | None = None,
run_products=None) -> dict[str, set]:
"""`{link: the pins ITS manifest declares}` for the links that answered.

A link whose tree is not on the disk is simply ABSENT from the mapping,
Expand Down Expand Up @@ -3265,6 +3286,19 @@ def link_pins_from_trees(links, root: Path | None = None,
`scaffold-project.py` takes every link but the first from the naming
policy's `open<Product>` form, which has no `-`, `.` or `_` to fold.

ONE VARIABLE, ONE PRODUCT OF THE RUN (#909). `run_products` is every
product the caller's run reads: the project's declared pins, and every
link of every chain it reads. The variable guard is asked over them as
well as over `links`, so it gives every chain of the run the same
answer, and a variable two declared pins share is read for no link
(#275's rule). A caller that reads several chains calls this once per
chain, and the guard was asked over that chain alone: in a project whose
two legs' chains begin at `openß` and `openss`,
`SHAPE_PIN_SOURCE_OPENSS` was read for both, so one tree answered for two
products and broke the chain of the one it is not. Given, it returns a
`LinkPins` whose `env_shared` says which variables were not read, so the
warning does not offer them.

`default_source` IS THE FIRST LINK'S TREE AND NO OTHER'S (#441). It is
one tree — the naming validator's bare `--link-source`, the scaffold's
`--pin-source` — and the first link is the pin this project holds, the
Expand All @@ -3284,7 +3318,8 @@ def link_pins_from_trees(links, root: Path | None = None,
found: dict[str, set] = {}
unreadable: dict[str, str] = {}
links = list(links)
shared = pin_source_env_shared(repo_basename(str(link)) for link in links)
shared = pin_source_env_shared(repo_basename(str(link)) for link in
[*links, *(run_products or ())])
for index, link in enumerate(links):
name = repo_basename(str(link))
asked: list = []
Expand All @@ -3303,9 +3338,10 @@ def link_pins_from_trees(links, root: Path | None = None,
unreadable[pin_product_key(name)] = why
continue
found[pin_product_key(name)] = pins
if not (with_reasons or remedies):
if not (with_reasons or remedies or run_products is not None):
return found
return LinkPins(found, unreadable if with_reasons else None, remedies)
return LinkPins(found, unreadable if with_reasons else None, remedies,
shared)


def find_repo_root(start: Path) -> Path:
Expand Down
19 changes: 19 additions & 0 deletions scripts/validate-repository-naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ def __init__(self, name: str, role: str | None, pins: set[str],
self.root = root


def _library_takes(keyword: str) -> bool:
"""Does this root's `link_pins_from_trees` take `keyword`? The skew rule
`_library_takes_remedies` asks of `remedies=`, asked of any keyword:
`run_products=` is new with #909, and an older library ends a call passing
it in a `TypeError`."""
try:
return keyword in inspect.signature(
link_pins_from_trees).parameters
except (TypeError, ValueError):
return False


class LinkSources:
"""Every `--link-source` value AS TYPED, read against the chain links this
run names, and what each lookup made of it (#338).
Expand Down Expand Up @@ -349,6 +361,13 @@ def link_pins(self, policy: NamingPolicy, target: Target) -> dict:
tried: dict = {}
offered = ({"remedies": self.remedies}
if self.remedies and _library_takes_remedies() else {})
# ONE VARIABLE, ONE PRODUCT OF THE RUN (#909). The variable guard is
# asked over the declared pins and every chain this run reads, and
# not over this target's chain alone: two legs whose chains begin at
# `openß` and `openss` read `SHAPE_PIN_SOURCE_OPENSS` for both, and
# its one tree broke the chain of the product it is not.
if _library_takes("run_products"):
offered["run_products"] = [*target.pins, *self.links]
found = link_pins_from_trees(
target.chain, target.root,
{key: path for key, (_typed, path) in self.keyed.items()},
Expand Down
Loading
Loading