the agent's python libcst codemod duplicated decorators on every run - re-running it mid-migration wasnt safe
Fixes a Python libcst codemod that duplicates decorators on every run. Use when re-running a decorator-adding transform mid-migration isn't safe. Key trigger: the same decorator stacked N times after N runs.
TL;DR: Before adding the decorator, scan the node's existing decorators and skip when it's already there. In libcst, leave_FunctionDef gets the updated node - check updated_node.decorators for a matching name (including the called form, a decorator with arguments) and return the node unchanged. Prove it with a double-apply test: the second run must be a no-op.
The problem as reported:
the agent's python libcst codemod duplicated decorators on every run - re-running it mid-migration wasnt safe- Open the transformer and find
leave_FunctionDef(orleave_ClassDef) where the decorator is appended. - Add an existence check covering the plain, attribute, and called forms:
def _has_decorator(self, node, name):
for d in node.decorators:
target = d.decorator
if isinstance(target, cst.Call):
target = target.func
if isinstance(target, cst.Name) and target.value == name:
return True
if isinstance(target, cst.Attribute) and target.attr.value == name:
return True
return False
def leave_FunctionDef(self, original_node, updated_node):
if self._has_decorator(updated_node, "my_decorator"):
return updated_node
return updated_node.with_changes(
decorators=[*updated_node.decorators,
cst.Decorator(cst.Name("my_decorator"))]
)Expected: the transformer now returns early for already-decorated functions.
- Apply the codemod to a fixture file twice and diff after the second run. Expected: empty diff - the second application changes nothing.
- Add the double-apply test to CI for every codemod in the repo. Expected: future non-idempotent transforms fail the build instead of stacking decorators.
Use this when
- A libcst codemod stacks the same decorator once per run.
- Re-running the transform mid-migration isn't safe.
- You need decorator insertion to be idempotent.
Not for this skill when
- The duplicates come from the codemod running on two copies of the same file (transformed and untransformed) - that's a file-selection problem.
- Decorators are added by a different tool - fix the guard in that tool.
Variant phrasings
- libcst duplicate decorator on re-run
- codemod adds decorator twice
- python codemod not idempotent decorator
- decorator stacked multiple times by transform
Why it happens
The transformer appends unconditionally: its input predicate doesn't exclude its own output, so every run sees a function "missing" the decorator and adds another.
Edge cases
- The decorator imported under an alias: resolve the real name via libcst's scope metadata instead of comparing raw names.
- Decorator factories with arguments: the isinstance(target, cst.Call) unwrap above covers the decorator-with-arguments form.
- Stacked decorators where order matters: appending at the end may be wrong - insert at the correct position, still guarded by the existence check.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_fVxVhIn4v0W8dQIUSWUTJw
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.