Skip to content
academia.sh

Lesson 02 / 14

Dependency Declaration

The same declaration gives 2 distinct version sets when the resolution strategy changes, and a lock file drops the number to 1; when the registry view also changes, four installs made from the declaration give 3 distinct sets while the locked four stay at one set.

Contents

The previous lesson drew a boundary and measured its share: on an isolated site, install order and neighbor projects were eliminated, and five distinct outcomes dropped to one. That measurement was reached by holding one thing fixed — the resolution rule was always the same, always the newest eligible version.

This lesson releases that constant. Its question is: on its own, how many distinct version sets does the same declaration specify? The declaration is the one thing the project writes, and it does not change; what changes is the rule of the side reading it. The answer will be measured, then written to a file, and the measurement repeated.

A Declaration States a Range

A dependency declaration is the list sitting in a project’s metadata that says which packages are needed in which version range. The metadata itself carries more than this: the project’s name, its own version, entry points, optional dependency groups. This lesson’s concern is only the dependency list.

How the declaration is written carries a critical distinction. What is written for a package is almost never a single version; it is a range — the smallest accepted version and the version above which it is no longer accepted. There is a reason for this: a declaration pinned to a single version cannot come together with another package that uses the same one. A range is the condition for coming together at all.

Its cost comes from the same place. A range legitimizes every version that falls inside it. The side reading the declaration has to choose one version from the range, and the rule making that choice is not written in the declaration. The rule is the package manager’s own resolution strategy, reading the range.

This lesson compares two strategies: newest and oldest compatible. Both conform to the declaration; both are legitimate. The strategies and their behavior are modeled in the lesson; no product name is written and no real installation is performed.

What a Lock Is, and Is Not

A lock file is a document that writes a resolution’s result line by line: not a range, the exact versions chosen. The declaration is the human-written request, the lock is the machine-produced decision. The two are separate documents, and both are put under version control — the opposite of the previous lesson’s environment, because the environment is an artifact, while the lock is a decision.

Saying what a lock is not matters more. A lock does not eliminate resolution. The lock file itself is a resolution’s output; producing it required the ranges to be read once, a strategy applied, and a set chosen. What the lock does is make that work run once and write its result down. Later installs do not resolve, they read what is written.

This distinction shows up in the measurement as two rows: two installs made with the lock file give the same set even when two separate strategies are requested. The strategy is no longer being read.

The measurement’s assumptions:

  • ED12 — The registry, the declaration, and the resolve function are taken from the shared definition unchanged; the lesson does not change either’s behavior.
  • ED13 — The registry has been made a function parameter, and its default value is the shared definition’s registry. The binding numbers are produced with the default registry.
  • ED14 — The declaration is a range: the smallest version included, the largest excluded.
  • ED15 — There are two strategies: newest picks the last of the eligible candidates; oldest compatible picks the first. Both conform to the declaration.
  • ED16 — The oracle is the rig: we wrote which versions are published and what the declaration accepts, so every row’s correct result is known without measuring.
  • ED17 — A lock is a copy of a resolution’s result. Resolution is not called while a lock exists; this is the model’s one branch in the install function.
  • ED18 — In the measurement, the lock is produced from the newest strategy’s result. Had it been produced from a different strategy, the locked rows would show that set; what will not change is how many distinct outcomes locked installs give.
  • ED19 — The second registry view is derived from the first by adding versions published afterward. The declaration does not change, the registry does; the added versions fall inside the declaration’s range.
  • ED20 — In this lesson, packages do not require each other; every package is chosen independently. What transitive requirements and conflict do is the next lesson’s subject.
  • ED21 — Version numbers are the rig packages’ fictional versions; they are not a real package’s or tool’s version.
  • ED22No real installation is performed, no real package is downloaded. Installation is returning the chosen dict.
  • ED23 — Duration is not measured. What is counted is the distinct version set, that is, the distinct outcome.

Measurement

"""A declaration states a range: how many distinct sets does strategy and registry view give."""

REGISTRY = {
    "metrics": [(1, 0), (1, 1), (1, 2), (2, 0)],
    "report": [(0, 9), (1, 0), (1, 1)],
    "common": [(3, 0), (3, 1), (3, 2), (4, 0)],
}
DECLARATION = {"metrics": ((1, 0), (2, 0)), "report": ((1, 0), (2, 0)),
               "common": ((3, 0), (4, 0))}
# Same registry, with versions published later: the declaration did not change, the registry did.
NEWLY = {"metrics": [(1, 3)], "report": [(1, 2)], "common": [(3, 3)]}
LATER = {p: sorted(REGISTRY[p] + NEWLY[p]) for p in REGISTRY}
PACKAGES = ("metrics", "common", "report")


def eligible(package, registry=REGISTRY):
    lo, hi = DECLARATION[package]
    return [s for s in registry[package] if lo <= s < hi]


def resolve(strategy, registry=REGISTRY):
    """Two separate strategies resolve the same declaration."""
    chosen = {}
    for package in sorted(DECLARATION):
        candidates = eligible(package, registry)
        chosen[package] = candidates[-1] if strategy == "newest" else candidates[0]
    return chosen


def lock(chosen):
    """A lock fixes a resolution's result."""
    return dict(chosen)


def install(lock_file=None, strategy="newest", registry=REGISTRY):
    return lock_file if lock_file else resolve(strategy, registry)


def format_selection(chosen):
    return " ".join(f"{'.'.join(map(str, chosen[p])):>7s}" for p in PACKAGES)


newest, oldest = resolve("newest"), resolve("oldest")
lock_file = lock(newest)
print(f"{'install':<26s} {'metrics':>7s} {'common':>7s} {'report':>7s}")
for name, chosen in (("strategy: newest", newest), ("strategy: oldest", oldest), ("with lock file", install(lock_file)),
                     ("lock, other strategy", install(lock_file, "oldest"))):
    print(f"{name:<26s} " + format_selection(chosen))
distinct = len({tuple(sorted(s.items())) for s in (newest, oldest)})
distinct_locked = len({tuple(sorted(install(lock_file, st).items())) for st in ("newest", "oldest")})
print(f"same declaration, {distinct} distinct sets by strategy; with the lock {distinct_locked}")

print()
print(f"{'registry view / regime':<26s} {'metrics':>7s} {'common':>7s} {'report':>7s}")
installs = []
for registry_name, registry in (("first registry", REGISTRY), ("later registry", LATER)):
    for st in ("newest", "oldest"):
        chosen = install(None, st, registry)
        installs.append(chosen)
        print(f"{registry_name + ', ' + st:<26s} " + format_selection(chosen))
locked = [install(lock_file, st, registry) for registry in (REGISTRY, LATER) for st in ("newest", "oldest")]
print(f"{'lock file, all four':<26s} " + format_selection(locked[0]))
print(f"two registry views and two strategies, four installs: with the declaration "
      f"{len({tuple(sorted(s.items())) for s in installs})} distinct sets, "
      f"with the lock {len({tuple(sorted(s.items())) for s in locked})}")

print()
produced = resolve("newest")
print(f"the lock is produced from a resolution: lock == newest resolution -> "
      f"{lock(produced) == produced}")
print("if the lock is regenerated on the later registry: " + format_selection(lock(resolve("newest", LATER))))
install                    metrics  common  report
strategy: newest               1.2     3.2     1.1
strategy: oldest               1.0     3.0     1.0
with lock file                 1.2     3.2     1.1
lock, other strategy           1.2     3.2     1.1
same declaration, 2 distinct sets by strategy; with the lock 1

registry view / regime     metrics  common  report
first registry, newest         1.2     3.2     1.1
first registry, oldest         1.0     3.0     1.0
later registry, newest         1.3     3.3     1.2
later registry, oldest         1.0     3.0     1.0
lock file, all four            1.2     3.2     1.1
two registry views and two strategies, four installs: with the declaration 3 distinct sets, with the lock 1

the lock is produced from a resolution: lock == newest resolution -> True
if the lock is regenerated on the later registry:     1.3     3.3     1.2

Two Sets, Then One

The top table’s first two rows carry the course’s second claim. Same declaration, same registry, same three packages: with strategy newest, 1.2 / 3.2 / 1.1; with oldest compatible, 1.0 / 3.0 / 1.0. 2 distinct version sets.

There is no error anywhere between the two sets. The declaration accepts both; both conform to the request the project wrote. But these are not the same install. All three packages sit at separate versions; the behavior the code will meet, the fixed defects, and the added methods are separate. “The declaration is the same, so the install is the same” is the sentence the measurement disproves.

There is no ranking of superiority between the two either, because they test separate things. Newest brings fixes, but since it brings the behavior closest to the declaration’s upper bound, it picks the versions the project has tested the least. Oldest compatible does the opposite: it tests whether the declaration’s lower end actually works. A range’s lower bound is often copied in from somewhere and never tried; an install choosing it reveals whether the declaration is telling the truth. What the measurement says is not which one is good, it is that the two are not the same install.

The bottom two rows bring the lock into play. The install made with the lock file gives 1.2 / 3.2 / 1.1; the same set comes back even when the lock file and a different strategy are requested. 1 distinct outcome with the lock. The fourth row says this on its own: the strategy is no longer being read, because there is no range left to read.

The Registry Changes Over Time

The second table adds one more variable. The declaration is still the same, but the registry is at its next view: three new versions falling into the range have been published.

Four installs made with the declaration give 3 distinct sets. The newest strategy produces two distinct sets across the two registry views — 1.2 / 3.2 / 1.1 and 1.3 / 3.3 / 1.2 — because “newest” is not a fixed version, it is a function of the registry’s current state. oldest compatible gives the same set on both views; the range’s lower end is not affected by the registry growing.

This row shows that the uncertainty a declaration carries has two axes: who resolves and when they resolve. The same declaration, even with the same strategy, can give two distinct sets at two separate times. The install’s date is part of its output.

The four locked installs still give 1 distinct outcome. The lock closes both axes at once, because what it writes is not a rule or a range, it is the versions themselves. This is a result that requires reading the two tables together: as the number of installs made with the declaration grows, the number of distinct outcomes grows with it — two with two installs, three with four — while on the locked side the number stays at 1 no matter how many installs are made.

The last two lines show where the lock comes from. The lock equals the result of the newest resolution — producing it required the resolution to run. And when that same lock is regenerated on the later registry, 1.3 / 3.3 / 1.2 comes out. So a lock is a frozen resolution; when the resolution is run again, what it froze changes too.

The Lock’s Scope

The declaration’s and the lock’s lengths are almost never the same, and the reason sits outside the measurement.

The declaration writes what the project directly asks for. The installed set is often larger, because every requested package has its own declaration, and that declaration brings in other packages too. These are called transitive dependencies. The project’s code never names them, but they sit on the site at run time and take part in its behavior. The lock file writes the entirety of the installed set; not what is directly requested, every package that lands on the site. This is why a lock is far longer than a declaration and is not a document meant to be hand-written.

In this lesson’s rig, packages do not require each other, which is why the two sets coincide and the tables can be read in three rows. Where the gap opens is the next lesson’s measurement.

The lock’s second field looks under the version name. A version number is a label; it does not on its own say the content under that label has not changed over time. Lock files can therefore write a content hash next to the version and compare it against what is downloaded at install time. When the version name matches but the hash does not, what has surfaced is not a version problem, it is the same name carrying two separate contents — a distinct outcome a version number could never see.

The third is scope itself. A project’s declaration usually holds more than one group: what is needed to run, what is needed to test, what is needed to generate docs. These are separate sets and are installed separately. If a lock does not write which groups it covers, the same lock can produce two distinct sets across two separate installs — with both installs still conforming to the lock. A lock has to fix not only versions, but also which question it answers.

A Lock Does Not Eliminate Resolution

Every row of the measurement gathers into one sentence: a lock does not eliminate resolution; it makes the decision once and writes it down.

Three direct consequences follow. First, a lock is not a guarantee, it is a record; it does not say how good the frozen set is, only which set was frozen. A bad resolution gets locked with the same fidelity.

Second, renewing a lock is unavoidable work. If the range accepts new versions but the lock does not bring them in, the project is standing not somewhere the declaration permits, but at a point in the past. If that point was chosen deliberately, it is a decision; if it was stayed at unknowingly, it is an accumulation. The only thing separating the two is whether it is written down when and why the lock was renewed.

Third, the declaration and the lock can drift apart. If the lock is hand-edited, or the declaration is changed and the lock is not renewed, the two documents say two separate things, and the install gives a distinct outcome depending on which one is read. The measurement’s last line gives the test for this: regenerated from the same declaration with the same strategy, a lock should come out identical to itself. If it does not, either the registry has changed or one of the documents has been touched by hand — and which one is told apart by looking at the registry’s view.

Summary

  • A dependency declaration states not a version but a range; the range is the condition for coming together with other packages, and its cost is that the choice is made outside the declaration.
  • The same declaration gives 2 distinct version sets when the resolution strategy changes: 1.2 / 3.2 / 1.1 versus 1.0 / 3.0 / 1.0. Both conform to the declaration.
  • A lock file writes a resolution’s result with exact versions; a locked install gives 1 distinct outcome even when the strategy changes, because there is no range left to read.
  • When the registry view also changes, four installs made with the declaration give 3 distinct sets; the uncertainty has two axes — who resolves and when.
  • A lock writes not what is directly requested but the entirety of the installed set; if it does not also write which groups it covers, the same lock can produce two distinct sets.
  • A lock does not eliminate resolution; it makes the decision once and writes it down. A lock is a record, not a guarantee, and the set it freezes changes when it is renewed.

Next Step

In this lesson’s measurement, packages were chosen independently of each other: each was taken from its own range, and the choices never looked at one another. In a real set, packages carry their own declarations, and those can contradict each other. The next lesson measures that situation: what sets does the same declaration give once package requirements are also taken into account, where does it become visible when a strategy picks an inconsistent set, and what does resolution return when no consistent set exists at all?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close