Repository navigation
Releases
Releases · yudelevi/mpat
v0.3.4
v0.3.3
- The archive download behind
mpat diffandmpat check --difftreats the
package index as untrusted input. It only picks archives whose name is the
locked project's, so a filename like../../x-1.0.tar.gzcan no longer write
outsideMPAT_CACHE_DIR. It only followshttpandhttpslinks, so an
index cannot point it at a localfile://path. Error messages drop query
strings and credentials from URLs, so a signed download URL does not end up
in a CI log.
v0.3.2
mpat checklists only the targets that are notokand ends with an
N okcount, so a CI log shows the drift instead of burying it in a table
of every watched symbol.--allrestores the full table.--json,
--gitlaband the exit code are unchanged.- A target deleted upstream is reported as
missing. Before,patch()and
watch()resolved their target whilempat checkwas collecting, so the
deletion aborted the run with exit 2, no table and an empty GitLab report.
Outside collection a missing target still raisesTargetNotFound. - Rows with a changed signature, value, location or kind list the change under
them, e.g.value_repr: 16 -> 2, and the GitLab description carries the
same text.--jsongains achangeslist. - GitLab Code Quality findings point at the line of the
patch()orwatch()
call instead of the first line quoting the target, which could be an
unrelated string.--jsongainsdeclared_line. - A change confined to docstrings reports the new
docstringstatus instead of
body, with its own footer line andminorGitLab severity. It still exits 1.
The lock gains acode_hashfield for this; the firstmpat lockafter the
upgrade lists every function and class as changed (~) while it records it,
and until then a docstring change keeps reportingbody. mpat diffandmpat check --diffshow how a drifted target's upstream
source changed, as a diff in the style of atydiagnostic, with the locked
and installed line numbers side by side. The lock holds hashes, not source,
so the locked side comes from the release archive on the package index
(UV_DEFAULT_INDEX,UV_INDEX_URL,PIP_INDEX_URL, else PyPI), cached in
MPAT_CACHE_DIR. Index credentials never follow a redirect to another
host. A fetch failure prints why and leaves the exit code alone.mpat diff
without a target covers every drifted target, and now imports the
configured modules likempat check.- A target used by several declarations, such as a shared
depends_on, lists
the others under its row and in--json(used_by), and GitLab gets one
finding per declaration, so every patch that depends on the drift is marked.
v0.3.1
- A watch can name a file shipped inside an installed package:
file = "nicegui/elements/select.js"in a[[tool.mpat.watch]]table, or
watch("nicegui/elements/select.js")from code. The lock records the sha256
of the file's bytes with kindfile, andmpat checkreports the new
contentstatus when they change, whitespace included, so a fork of an
upstream JavaScript or template file gets re-diffed on the bump that touched
it instead of when someone notices. Files can be watched, not patched or
overridden. mpat check --gitlabrenders the non-OK results as a GitLab Code Quality
report, so drift is listed in the merge request instead of only in the job
log.location.pathis resolved against the repository root rather than the
mpat root, because GitLab reads it relative to the checkout: a run inside
services/apireportsservices/api/src/patches.py, notsrc/patches.py.
The root is the nearest ancestor holding.git, which--repo-root PATH
overrides.--gitlaband--jsonare mutually exclusive, and the exit code
is unchanged, so the CI job needsartifacts: when: alwaysto upload the
report on the run that fails.
0.2.2
- Configuration can live in a standalone
mpat.tomlnext topyproject.toml.
It carries the same keys without the[tool.mpat]prefix, so
[[tool.mpat.watch]]becomes[[watch]]. When both files are in the same
directorympat.tomlwins and the[tool.mpat]section is ignored, the rule
ruffandtyfollow. Error messages,declared_inand the source column of
mpat checkname whichever file the declaration came from. - New
mpat diff <target>prints the drift status of one locked target and every
fingerprint field aslocked -> current. It readsmpat.lockand resolves the
target without importing[tool.mpat] modules. Exit 2 when the target is not
in the lock. on_driftandMPAT_STRICT=1now coverdepends_onat import. A dependency
whose lock entry no longer matches warns, skips or raises together with its
patch, and the message says which entry drifted. Before, onlympat checkand
the pytest plugin looked at dependencies.[tool.mpat] modulesandallowmust be lists of strings,tool.mpatmust
be a table, and unknown keys in the section are configuration errors. A bare
string inmodulesused to be split into single characters.- A source file that cannot be read or parsed no longer crashes
mpat lockor
mpat check; the target is fingerprinted by signature only and marked
no_source, like a.pyc-only install. - A name defined more than once in one file, in
if/elseortry/except
branches, is now hashed at the definition that is actually live, matched by
the code object's first line. The first same-named definition used to win. - A target whose module raises
ImportErroron import now fails as a named
configuration error (exit 2) instead of a bare traceback, and says that a
module that has to be imported first belongs in[tool.mpat] modules. The new
TargetImportErrorsubclasses bothMpatErrorandImportError, so it is
still caught by anything that caught the raw error, and it is not
TargetNotFound, sompat checknever reports a broken environment as
missing.
0.2.1
- New
patch(..., when_imported=True). The declaration registers without
importing the target; the patch is applied by a post-import hook the first
time the target's top-level module is imported, or immediately if it already
is. This makes optional dependencies patchable: a target whose distribution is
not installed no longer raises at declaration, it just never fires.
mpat lockandmpat checkare unchanged, so the target still has to be
importable when you lock. - New
mpat.apply_all(), which imports every module awhen_importedpatch is
still waiting on and so applies them all now. - New
watch(..., track_value=False)records a scalar's existence and kind but not
its value. Use it for an upstream setting your application assigns itself,
wherempat locksees upstream's default and the pytest plugin sees your
override. The lock entry carriestrack_value = falseand novalue_repr, and
changingtrack_valueon a locked target reportsunlockeduntil the next
mpat lock, like a role ordepends_onchange. - Watches can be declared in
pyproject.tomlas[[tool.mpat.watch]]tables
withtarget,depends_on,review_byandnote, so a project with no
patch module still getsmpat lock,mpat checkand the pytest items. The
entry is locked withdeclared_in = "pyproject.toml". A target declared
both in code and inpyproject.tomlis refused as declared twice. - New
[[tool.mpat.override]]tables assign a scalar upstream attribute from
pyproject.toml, and newmpat.apply_overrides()applies them once per
process; that call is the only code an override needs. The target must be an
existing attribute of exactly the value's type,boolandintincluded,
and it goes through the same drift check as awatch()before assignment.
Each override is locked as a watch withtrack_value = false, sompat check
is green whether or not the override was applied in that process. watch()acceptsuntil, the sameVersion,Probe,|and&as
patch. A watch applies nothing, so the condition has no runtime effect; it
gives the workaround a generatedstill-needed[<target>]test, which is what
a per-instancesetattrwrapper or an attribute-creation shim needed instead
of a hand-written one.[[tool.mpat.watch]]entries takeuntilas a
string: a requirement with a version specifier, or a dotted path to a
zero-argument callable that is imported when first evaluated.- Fixed: the pytest plugin looked for
[tool.mpat]only at pytest's rootdir,
so in a monorepo with onepyproject.tomlper service and none at the root,
pytest services/api/testsfrom the root silently collected nothing. The
plugin now walks up from each directory argument to its nearest
pyproject.tomland collects one set of items per[tool.mpat]it finds,
each reading the lockfile next to its ownpyproject.toml. A project that is
not the rootdir's own is collected asmpat[services/api]::...; the rootdir's
own project keeps thempat::...node ids it had.
0.2.0
- The generated drift tests are now collected by a pytest plugin. Installing
mpat is enough: a project with[tool.mpat]in itspyproject.tomlgets
mpat::drift[...]andmpat::still-needed[...]items in its normal pytest
run, with no test module to write. A project with no[tool.mpat]collects
nothing. - The generated items are collected by
pytestand bypytest <dir>. An
argument that names a file or a nodeid selects only what it names, since items
that belong to no file cannot be narrowed by a path. - Fixed: collecting the generated tests imports your patch modules normally, so
the patches apply and later tests in the session see patched behaviour.
Previously both the plugin andmpat.testingimported them in the collect-only
mode the CLI uses, which cached them unpatched and made a suite pass or fail
depending on file order. - New
--no-mpatflag, and ampat = falseini option, to turn the generated
items off. from mpat.testing import test_upstream_drift, test_patch_still_neededkeeps
working. When that module is collected, the plugin adds nothing, so the checks
never run twice.
0.1.0
First release. See https://github.com/yudelevi/mpat/releases/tag/v0.1.0.
mpat 0.3.0
mpat check --gitlabrenders the non-OK results as a GitLab Code Quality
report, so drift is listed in the merge request instead of only in the job
log.location.pathis resolved against the repository root rather than the
mpat root, because GitLab reads it relative to the checkout: a run inside
services/apireportsservices/api/src/patches.py, notsrc/patches.py.
The root is the nearest ancestor holding.git, which--repo-root PATH
overrides.--gitlaband--jsonare mutually exclusive, and the exit code
is unchanged, so the CI job needsartifacts: when: alwaysto upload the
report on the run that fails.
mpat 0.2.2
- Configuration can live in a standalone
mpat.tomlnext topyproject.toml.
It carries the same keys without the[tool.mpat]prefix, so
[[tool.mpat.watch]]becomes[[watch]]. When both files are in the same
directorympat.tomlwins and the[tool.mpat]section is ignored, the rule
ruffandtyfollow. Error messages,declared_inand the source column of
mpat checkname whichever file the declaration came from. - New
mpat diff <target>prints the drift status of one locked target and every
fingerprint field aslocked -> current. It readsmpat.lockand resolves the
target without importing[tool.mpat] modules. Exit 2 when the target is not
in the lock. on_driftandMPAT_STRICT=1now coverdepends_onat import. A dependency
whose lock entry no longer matches warns, skips or raises together with its
patch, and the message says which entry drifted. Before, onlympat checkand
the pytest plugin looked at dependencies.[tool.mpat] modulesandallowmust be lists of strings,tool.mpatmust
be a table, and unknown keys in the section are configuration errors. A bare
string inmodulesused to be split into single characters.- A source file that cannot be read or parsed no longer crashes
mpat lockor
mpat check; the target is fingerprinted by signature only and marked
no_source, like a.pyc-only install. - A name defined more than once in one file, in
if/elseortry/except
branches, is now hashed at the definition that is actually live, matched by
the code object's first line. The first same-named definition used to win. - A target whose module raises
ImportErroron import now fails as a named
configuration error (exit 2) instead of a bare traceback, and says that a
module that has to be imported first belongs in[tool.mpat] modules. The new
TargetImportErrorsubclasses bothMpatErrorandImportError, so it is
still caught by anything that caught the raw error, and it is not
TargetNotFound, sompat checknever reports a broken environment as
missing.
mpat 0.2.1
0.2.1
- New
patch(..., when_imported=True). The declaration registers without
importing the target; the patch is applied by a post-import hook the first
time the target's top-level module is imported, or immediately if it already
is. This makes optional dependencies patchable: a target whose distribution is
not installed no longer raises at declaration, it just never fires.
mpat lockandmpat checkare unchanged, so the target still has to be
importable when you lock. - New
mpat.apply_all(), which imports every module awhen_importedpatch is
still waiting on and so applies them all now. - New
watch(..., track_value=False)records a scalar's existence and kind but not
its value. Use it for an upstream setting your application assigns itself,
wherempat locksees upstream's default and the pytest plugin sees your
override. The lock entry carriestrack_value = falseand novalue_repr, and
changingtrack_valueon a locked target reportsunlockeduntil the next
mpat lock, like a role ordepends_onchange. - Watches can be declared in
pyproject.tomlas[[tool.mpat.watch]]tables
withtarget,depends_on,review_byandnote, so a project with no
patch module still getsmpat lock,mpat checkand the pytest items. The
entry is locked withdeclared_in = "pyproject.toml". A target declared
both in code and inpyproject.tomlis refused as declared twice. - New
[[tool.mpat.override]]tables assign a scalar upstream attribute from
pyproject.toml, and newmpat.apply_overrides()applies them once per
process; that call is the only code an override needs. The target must be an
existing attribute of exactly the value's type,boolandintincluded,
and it goes through the same drift check as awatch()before assignment.
Each override is locked as a watch withtrack_value = false, sompat check
is green whether or not the override was applied in that process. watch()acceptsuntil, the sameVersion,Probe,|and&as
patch. A watch applies nothing, so the condition has no runtime effect; it
gives the workaround a generatedstill-needed[<target>]test, which is what
a per-instancesetattrwrapper or an attribute-creation shim needed instead
of a hand-written one.[[tool.mpat.watch]]entries takeuntilas a
string: a requirement with a version specifier, or a dotted path to a
zero-argument callable that is imported when first evaluated.- Fixed: the pytest plugin looked for
[tool.mpat]only at pytest's rootdir,
so in a monorepo with onepyproject.tomlper service and none at the root,
pytest services/api/testsfrom the root silently collected nothing. The
plugin now walks up from each directory argument to its nearest
pyproject.tomland collects one set of items per[tool.mpat]it finds,
each reading the lockfile next to its ownpyproject.toml. A project that is
not the rootdir's own is collected asmpat[services/api]::...; the rootdir's
own project keeps thempat::...node ids it had.
mpat 0.2.0
0.2.0
- The generated drift tests are now collected by a pytest plugin. Installing
mpat is enough: a project with[tool.mpat]in itspyproject.tomlgets
mpat::drift[...]andmpat::still-needed[...]items in its normal pytest
run, with no test module to write. A project with no[tool.mpat]collects
nothing. - The generated items are collected by
pytestand bypytest <dir>. An
argument that names a file or a nodeid selects only what it names, since items
that belong to no file cannot be narrowed by a path. - Fixed: collecting the generated tests imports your patch modules normally, so
the patches apply and later tests in the session see patched behaviour.
Previously both the plugin andmpat.testingimported them in the collect-only
mode the CLI uses, which cached them unpatched and made a suite pass or fail
depending on file order. - New
--no-mpatflag, and ampat = falseini option, to turn the generated
items off. from mpat.testing import test_upstream_drift, test_patch_still_neededkeeps
working. When that module is collected, the plugin adds nothing, so the checks
never run twice.
mpat 0.1.0
First usable release.
@patch(target, depends_on=..., until=..., on_drift=..., review_by=..., note=...)andwatch(target, ...)- Fingerprints from the installed source file via
ast, never from live objects mpat.lock: TOML, keyed tables, atomic writempat lock,mpat check [--json],mpat show; exit codes 0/1/2- Drift statuses:
ok,missing,moved,signature,body,value,no_source,unlocked,stale,review mpat.testing: generated pytest tests for drift and for patches whoseuntilcondition flipped- Denylist for security-critical modules, overridable only via
[tool.mpat] allow - Python 3.11 to 3.14, typed (
py.typed)
Docs: https://monkeypat.ch