pluckit¶
A fluent API for querying, viewing, and mutating source code. CSS selectors over ASTs, backed by DuckDB.
pluckit is a thin Python layer over DuckDB with the
sitting_duck community
extension. You write CSS-like selectors against tree-sitter ASTs for 27
languages; pluckit compiles them to SQL, runs them against read_ast(),
and gives you back a lazy Selection object that chains through filters,
navigation, view rendering, and structural mutations.
Status
Alpha. Query, view, mutate, call-graph, git history, and scope analysis all work end-to-end. See What's new below.
What's new¶
Recent releases (v0.9 – v0.12.0):
--diffflag — preview any mutation as a unified diff without writing files. Pipe topatch,git apply, or pluckit's ownpatchop.--dry-runalso now works (was parsed but never checked).patchmutation — apply a unified diff or raw replacement text to matched nodes. Auto-detects format; pairs naturally with@file.@fileargument syntax — read argument content from files with@path. Works in CLI, JSON, andfrom_dict. Resolution at eval time keeps serialized chains portable.Isolated—Selection.isolate()extracts a code block along with its free-variable dependencies, classifying each name as a parameter, import, or builtin; renders as a standalone function or Jupyter cell.- Pagination —
limit/offset/pagework as chain ops and as Selection methods.Chain.evaluate()attaches pagination metadata;Chain.next_page/prev_page/goto_pagebuild follow-up chains;Chain.with_totalcomputes the exact total on demand. CallsandScopepluckins — call-graph (callers,callees,references) and scope-aware queries (scope,defs,refs) via sitting_duck's pseudo-elements.Selectorclass — a validated, serializablestrsubclass; drop-in replacement for bare selector strings, with.validate()/.to_dict()/.to_json()/.to_argv().- Persistent AST cache —
Plucker(cache=True)or[tool.pluckit] cache = truematerializesread_astoutput into a.pluckit.duckdbfile and re-parses only files whose mtime has changed. Pluckinrename — the extension-point class is nowPluckin(underpluckit.pluckins). The oldPlugin/pluckit.pluginsnames remain as aliases for backward compatibility.
Why pluckit¶
Most AST-aware tooling falls into one of two camps:
- Language-specific, prescriptive tools (Python's
ast, Rust'ssyn, comby, tree-sitter queries directly). Powerful, but tied to one language and one idiom. - Generic text tools (
grep,sed,rg). Fast and universal, but blind to structure — every rename is a regex gamble.
pluckit sits in the middle. You get:
- Cross-language selectors.
.fn:exportedmeans "exported functions" in Python and Go and TypeScript and Rust. The semantic taxonomy lives in sitting_duck, not in hand-written rules. - SQL performance. Every query is a DuckDB SQL query against the
read_ast()table function. You can join against other tables, aggregate, window, and export to Parquet — all with the queries you already know. - Safe mutations. The mutation engine snapshots every affected file, splices changes in reverse order, re-parses to validate, and rolls back everything on any syntax error. Atomic by default.
Install¶
The PyPI distribution name is ast-pluckit — the bare pluckit name is
held by an abandoned 2019 project on PyPI. The import name, CLI name,
and repository name are all pluckit.
After installing, run:
to eagerly install and verify the sitting_duck DuckDB community
extension. This also happens automatically the first time you run any
other command; init just gives you clearer diagnostics if something
fails.
A 30-second tour¶
pluckit's CLI is a chain — source first, then operations. See the CLI reference for the full vocabulary.
Find¶
pluckit src/**/*.py find ".fn:exported" names
# authenticate
# decode_jwt
# get_user
# ...
pluckit tests/*.py find ".fn[name^=test_]" count
# 218
View¶
pluckit src/**/*.py find ".fn#validate_token" view ".fn#validate_token { show: signature; }"
# ```python
# def validate_token(token: str, *, clock_skew: int = 30) -> User:
# ```
pluckit src/config.py find ".cls#Config" view
# Class outline: header + every method signature, inline
Edit¶
# Add a parameter to every exported function AND update every call site
pluckit src/**/*.py \
find ".fn:exported" addParam "trace_id: str | None = None" \
-- \
find ".call:exported" addArg "trace_id=trace_id"
Every file in the transaction rolls back if any of them fails to re-parse.
Add --dry-run to see the exact unified diff before writing.
Python API¶
from pluckit import Plucker, AstViewer
pluck = Plucker(code="src/**/*.py", plugins=[AstViewer])
# Lazy selections chain
tests = pluck.find(".fn[name^=test_]").filter(".fn:not(:has(.try))")
print(tests.count())
print(tests.names()[:10])
# Render as markdown
print(pluck.view(".fn#validate_token { show: signature; }"))
# Mutate via the fluent API
pluck.find(".fn#old_name").rename("new_name")