check_compat(
old_doc: str, new_doc: str, *, samples: int = 300
) -> list[str]
Report backward-incompatible changes from old_doc to new_doc, most reliable part first.
Every entry is a definite break - an invocation the old usage accepts that the new one rejects: a
removed option, a removed command, or a concrete argument vector the new grammar no longer accepts
(found by sampling the old grammar's accepted set and replaying it against the new).
An empty list means no break was found, not a proof of compatibility. The accepted set is infinite
and only samples invocations are checked, so read it like a passing test ("no breakage detected"),
never as a guarantee. It never claims "compatible", it only surfaces breaks it can prove.
Parameters:
| Name |
Type |
Description |
Default |
old_doc
|
str
|
The usage message as it stands today.
|
required
|
new_doc
|
str
|
The usage message as it would be after the change.
|
required
|
samples
|
int
|
How many invocations to draw from the old grammar when hunting for counterexamples.
More samples widen the search, and never turn an empty result into a guarantee.
|
300
|
Source code in src/docopt2/_compat.py
| def check_compat(old_doc: str, new_doc: str, *, samples: int = 300) -> list[str]:
"""Report backward-incompatible changes from ``old_doc`` to ``new_doc``, most reliable part first.
Every entry is a *definite* break - an invocation the old usage accepts that the new one rejects: a
removed option, a removed command, or a concrete argument vector the new grammar no longer accepts
(found by sampling the old grammar's accepted set and replaying it against the new).
An empty list means **no break was found**, not a proof of compatibility. The accepted set is infinite
and only ``samples`` invocations are checked, so read it like a passing test ("no breakage detected"),
never as a guarantee. It never claims "compatible", it only surfaces breaks it can prove.
Args:
old_doc: The usage message as it stands today.
new_doc: The usage message as it would be after the change.
samples: How many invocations to draw from the old grammar when hunting for counterexamples.
More samples widen the search, and never turn an empty result into a guarantee.
"""
old_examples = generate_examples(old_doc, count=samples, seed=0)
removed_options = _spellings(old_doc) - _spellings(new_doc)
# A command literal absent from new is only a break if it actually rejects something. `(add|rm) <p>`
# generalized to `<cmd> <p>` drops the `add`/`rm` literals, yet new accepts every old invocation (they
# match `<cmd>`). Report a command only when a sampled old invocation that used it is rejected by new.
gone = _usable(old_doc, Command) - _usable(new_doc, Command)
removed_commands = {name for name in gone if _command_removal_breaks(new_doc, name, old_examples)}
breaks = [f"option `{name}` removed" for name in sorted(removed_options)]
breaks += [f"command `{name}` removed" for name in sorted(removed_commands)]
removed = removed_options | removed_commands
seen_shapes: set[tuple[str, ...]] = set()
examples: list[str] = []
for argv in old_examples:
shape = tuple(re.sub(r"v\d+", "<val>", token) for token in argv) # collapse placeholder values
if shape in seen_shapes or _explained(argv, removed): # one example per shape; skip structural repeats
seen_shapes.add(shape)
continue
seen_shapes.add(shape)
if _rejects(new_doc, argv):
examples.append(f"`{' '.join(argv)}` no longer accepted")
if len(examples) >= _MAX_EXAMPLES:
break
return breaks + examples
|