from fastcore.test import test,test_eq,expect_failpyskills API
Overview
A plugin system allowing Python packages to register “skills” — units of LLM-usable functionality — via standard Python entry points. An LLM harness (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context.
Entry Point Convention
Packages register pyskills under the group pyskills:
[project.entry-points.pyskills]
my_skill = "mypackage.skill"The value is a module path (no :attribute needed). The module’s docstring first paragraph serves as the pyskill description.
Skill Module Contract
A pyskill module MUST have:
- Docstring — first paragraph is the short description shown to the LLM for pyskill selection; remaining will be read by the LLM to get full details on the pyskill.
A pyskill module MAY have:
__all__— the available symbols imported.
Discovery API
def list_pyskills() -> dict[str, str]Returns {name: description} for all registered pyskills, using find_spec + AST parsing — no imports.
import mypackage.skillStandard python native import
doc(mypackage.skill) # module overview: classes, functions, submodules
doc(SomeClass) # class detail: bases, __init__, methods, properties
doc(some_func) # function detail: full signature with docments
xdir(mypackage.skill) # filtered names for public symbolsInspect at increasing detail — works on any Python module, not just pyskills.
Host Integration
The harness (e.g. solveit, claude code, codex, …) would:
- Call
list_pyskills()at startup to build a pyskill catalogue - Include the list with each prompt
- Call
import {module}followed bydoc({module})for chosen pyskills
Listing pyskills
ep = entry_points()
es = first(ep.select(group='pyskills', name='pyskills.skill'))
esEntryPoint(name='pyskills.skill', value='pyskills.skill', group='pyskills')
ep_desc
def ep_desc(
ep
):First paragraph of docstring for entry point ep, without importing it
print(ep_desc(es))Pyskills is a plugin system allowing Python packages to register "skills" (units of LLM-usable functionality) via standard Python entry points. An LLM harness (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context using standard imports.
list_pyskills
def list_pyskills():Returns {module: description} for all pyskills. To load a module, use import {module} then view `doc({module}). NB: pyskills are THE critical way to extend functionality. ALWAYS check for pyskills to help with tasks. If unsure whether a particular pyskill might help, load it and grabs its docs to see!
list_pyskills(){'pyskills.skill': 'Pyskills is a plugin system allowing Python packages to register "skills" (units of LLM-usable functionality) via standard Python entry points. An LLM harness (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context using standard imports.',
'ghapi.skill': 'GitHub REST API access via `GhApi`, plus local git operations via `fastgit.Git`. Use this for day-to-day GitHub work: reading/creating issues and PRs, checking CI status, managing releases/branches/gists, and repo-local git operations -- all from Python, no shelling out to `gh`/`git` needed.',
'dialoghelper.solveitskill': 'Read, search, edit, and manage Solveit dialogs using dialoghelper.core, including dialog/message addressing, line-numbered inspection, targeted message edits, add/update/delete/copy/paste workflows, and safe editing patterns.',
'dialoghelper.termskill': 'Read and edit Solveit dialog (or Jupyter) .ipynb files from a CLI / script. Solveit is an online notebook application (like Jupyter with AI integration) where each notebook is called a "dialog" and is stored as an `.ipynb` file containing `code`, `note` (markdown), and `prompt` (markdown with a special delimiter) messages (aka "cells"). The `dialoghelper` package provides tools for reading, searching, adding, updating, and deleting those messages.',
'fastanki.skill': 'Anki flashcard tools for LLM-driven spaced repetition. Direct sqlite + AnkiWeb sync, no Anki app needed.',
'clikernel.skill': 'Use the persistent `clikernel` MCP session as the default workspace for any task advanced through live Python execution -- stateful inspection, file-editing workflows, debugging, experiments, API probes, data transforms, or notebook-style work. Read this before writing, running, or debugging Python code in a session with `clikernel` connected.',
'llmsurgery.skill': 'Read and work with Claude Code and Codex session transcripts.',
'tracefunc.skill': "Trace a Python function's execution at AST-line level: per-line hit counts and live variable values, via `sys.monitoring`. Use when debugging *why* code takes a branch, loops, recurses, or computes a wrong value, without editing the code under investigation or using an interactive debugger.",
'remold': "Structural search and rewrite for Python source: declarative ast-grep pattern rules, LibCST matcher transforms for everything patterns can't express, and tree-based symbol queries. Use this to edit code by structure (rename calls, move methods, rewrite APIs) where regexes break and `ast` loses comments.",
'exhash.skill': 'Universal hash-verified text editing for local files. Use this when an LLM needs one safe editing interface for reading, previewing, and modifying text files.',
'jph': "Jeremy's tools, which for now is just his notes system. Use if Jeremy says 'create a note', or 'check my notes' or similar.",
'cordslite.skill': 'Load this skill when an agent needs to search, summarize, or find information in Discord using cordslite. It covers read-only workflows for connecting to Discord, opening a guild, orienting through channels, searching messages, reading threads, and fetching attachments.',
'nbdev.skill': 'Author clear, executable nbdev notebooks where code, prose, examples, outputs, and tests form one coherent narrative.',
'fastmux.skill': 'Live handles for tmux sessions, windows, and panes, plus named background sessions driven by sid. Use this when code needs to read terminal screens or scrollback, drive interactive processes, build pane layouts, search text across terminals, or keep a persistent terminal session that both an agent and the user can inspect and drive.',
'rgapi.skill': 'Fast and flexible file discovery and search for Python. Use this when code needs `fd`-style file finding or `rg`-style searching.',
'toolslm.read_md': 'Read long Markdown documents by section number: search sections, follow links, and retrieve text by short dotted addresses, so nothing is displayed but what the task needs.',
'fastcdp.skill': 'Drive Chrome via the DevTools Protocol: navigate, click and fill pages, read them as an LLM-friendly accessibility tree, buffer console/network/dialog activity for debugging, and call any raw CDP command.',
'aai_coding.coding_patterns': "Jeremy's coding style and conventions: read before writing, reviewing, or assessing any code.",
'aai_coding.harness_docs': 'Official docs for the current LLM harness (Claude Code or codex): read before answering questions about harness behavior, config, or features.',
'aai_coding.looker': 'Answer questions about images and PDFs via an isolated codex checker, without loading them into your own context (macOS-only). Use it to verify rendered output: PDFs, screenshots, captured windows.',
'aai_coding.write_prose': "How to write prose that doesn't read as AI slop: read before writing anything for human readers.",
'fastcore.editskill': 'Text, file, cell, and notebook editing from `fastcore.tools` and `fastcore.nbio`, plus the conventions the whole fastai editing toolkit follows. Read this before working with the editing tools in any package that shares them.',
'aidialog.dlgskill': 'Read, search, and edit dialogs and notebooks through the aidialog `Dialog`/`Message` model',
'test.skill': 'A test skill.'}
allow
allow registers the callables a pyskill trusts to perform side-effecting operations under a sandbox (e.g. safepyrun): pass functions, {cls: ['method']} dicts (... for all public methods), or callable instances, and they land in the __pytools__ registry; an object defining __allow__ delegates registration to the items it returns. Outside a sandbox allow is a no-op, and inside one, sandboxed code can’t broaden its own permissions, since allow itself raises an audit event.
allow
def allow(
*c, allow_policy:NoneType=None, # Callable that raises if call not allowed
):Add all items in c to __pytools__, optionally constrained by allow_policy
__pytools__ is a defaultdict(set) mapping classes, modules, or instances to their allowed method/function names (or ... for all public methods). Values can be plain strings or (name, AllowPolicy) tuples for allow-checked methods. allow registers entries — plain functions are added under their module, methods under their class, callable instances under the instance itself (as '__call__'), and dicts go directly into __pytools__. If an object defines __allow__, that’s called instead and its result registered.
def _test_fn(): pass
_test_fn.__module__ = '__main__'
_test_fn.__name__ = 'my_test_func'
allow(_test_fn)
assert 'my_test_func' in __pytools__[sys.modules['__main__']]
allow({str: ['zfill']})
assert 'zfill' in __pytools__[str]
allow({list: ...})
assert ... in __pytools__[list]
__pytools__[sys.modules['__main__']].discard('my_test_func')
allow(collections.Counter.most_common)
assert 'most_common' in __pytools__[collections.Counter]
__pytools__[collections.Counter].discard('most_common')import httpxdef chk_url(url, *args, **kwargs):
if not url.startswith('https://'): raise PermissionError()
allow({httpx.get: chk_url, httpx.post: chk_url})
assert ('get', chk_url) in __pytools__[httpx._api]
assert ('post', chk_url) in __pytools__[httpx._api]from fastcore.xtras import timed_cache@timed_cache(60)
def wrapped_tool(): return "ok"
allow(wrapped_tool)
assert 'wrapped_tool' in __pytools__[sys.modules['__main__']]Callable instances — like the dynamically-generated ops of a spec-driven client — have no useful qualname, so they’re registered under the instance itself with '__call__' (and the class’s __call__ is wrapped for call-tracking). An object can also define __allow__, returning a list of items to register in its place; allow recurses into it, so a container can register all its callables at once.
class _CallableTool:
async def __call__(self, x): return x
tool = _CallableTool()
allow(tool)
assert '__call__' in __pytools__[tool]
assert getattr(_CallableTool.__call__, '_fastaudit_orig', None) is not None
class _ToolGroup:
def __init__(self, *ops): self.ops = list(ops)
def __allow__(self): return self.ops
tool2 = _CallableTool()
allow(_ToolGroup(tool2))
assert '__call__' in __pytools__[tool2]allow raises a pyskills.allow audit event before registering anything. Outside a sandbox that’s a no-op; inside safepyrun’s audit context it means sandboxed code can’t broaden its own permissions by calling allow, since the event is denied like any other unapproved operation:
seen = []
sys.addaudithook(lambda ev,args: seen.append(args) if ev=='pyskills.allow' else None)
allow(_test_fn)
test_eq(seen[-1], ((_test_fn,),))
__pytools__[sys.modules['__main__']].discard('my_test_func')Allow policies
chk_dest
def chk_dest(
p, ok_dests
):Call self as a function.
chk_dest resolves a path and verifies it falls under one of the allowed destination prefixes. Raises PermissionError if not. Used by all AllowPolicy subclasses.
chk_dest('/tmp/foo.txt', ['/tmp'])
chk_dest('~/tmp/foo.txt', [Path.home()/'tmp'])
try: chk_dest('/etc/passwd', ['/tmp'])
except PermissionError: print("Correctly blocked /etc/passwd")Correctly blocked /etc/passwd
OpenWritePolicy
def OpenWritePolicy(
*args, **kwargs
):Check open() only when mode is writable
PathWritePolicy
def PathWritePolicy(
target_pos:NoneType=None, target_kw:NoneType=None
):Check resolved Path self, optionally also target args
PosAllowPolicy
def PosAllowPolicy(
pos:int=0, kw:NoneType=None
):Check positional/keyword path arg is an allowed destination; non-path buffers/handles are skipped
AllowPolicy
def AllowPolicy(
*args, **kwargs
):Base for allow destination policies
Three AllowPolicy subclasses handle different allow-checking patterns. PosAllowPolicy only checks str/os.PathLike/bytes arguments: callables like DataFrame.to_excel or openpyxl’s Workbook.save also accept already-open file objects, whose destination was checked when they were opened — a policy crash on those would abort the run instead of denying it.
pp = PosAllowPolicy(1, 'dst')
pp(None, ['src', '/tmp/ok'], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): pp(None, ['src', '/root/bad'], {}, {'ok_dests': ['/tmp']})
pwp = PathWritePolicy()
pwp(Path('/tmp/f.txt'), [], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): pwp(Path('/etc/f.txt'), [], {}, {'ok_dests': ['/tmp']})
owp = OpenWritePolicy()
owp(None, ['/tmp/f.txt', 'w'], {}, {'ok_dests': ['/tmp']})
owp(None, ['/etc/passwd', 'r'], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): owp(None, ['/root/f.txt', 'w'], {}, {'ok_dests': ['/tmp']})
import io
pp(None, ['src', io.BytesIO()], {}, {'ok_dests': ['/tmp']})
pp(None, ['src', open('/dev/null', 'wb')], {}, {'ok_dests': ['/tmp']})
pp(None, ['src', b'/tmp/ok'], {}, {'ok_dests': ['/tmp']})
with expect_fail(PermissionError): pp(None, ['src', b'/root/bad'], {}, {'ok_dests': ['/tmp']})Allow policies are stored as (name, AllowPolicy) tuples directly inside __pytools__ sets.
doc / xdir
resolve
def resolve(
sym_nm:str, # Dotted symbol path, with optional [n] indexing, e.g. "module.attr.subattr[1]"
):Resolve a dotted symbol string to its Python object, with optional [n] indexing
SymbolNotFound
def SymbolNotFound(
*args, **kwargs
):Common base class for all non-exit exceptions.
with expect_fail(SymbolNotFound): resolve('abc')
resolve('resolve'), resolve('len')(<function __main__.resolve(sym_nm: str)>, <function len(obj, /)>)
import pyskills.skillassert _is_own(pyskills.skill, 'skill_test_func')
assert _is_own(pyskills.skill, 'SkillTestClass')
assert not _is_own(pyskills.skill, 'inspect')
assert not _is_own(pyskills.skill, '_private')xdir returns public names for a module, class, or instance. Modules respect __all__ and include explicitly imported sibling submodules; classes include __init__ and public methods; instances opt in by defining __dir__. Pass q to filter the names with a case-insensitive regex.
xdir
def xdir(
sym:str | object, # Module, class, or instance to inspect
q:str=None, # Optional case-insensitive regex over names
):Filtered names for public symbols of a module or class (or anything with __dir__)
x = xdir(pyskills.skill)
test_eq(x, xdir('pyskills.skill'))
x['SkillTestClass',
'async_skill_test_func',
'skill_test_func',
'pyskills.createskill']
doc
def doc(
sym:str | object, # Object (or dotted name) to document
*syms:str | object, # More objects: each doc appended as its own blank-line-separated section
all:bool=False, # Show symbol listings elided by `__pyskill_sigs__=False`?
)->str:Docstring of modules, classes, functions, instances or any other Python objects.
doc_key
def doc_key(
sym
):The 4-hex proof-of-sight key shown in doc(sym) output (None where docs carry no key): llmdojo’s doced(sym='key') verifies it against this live rendering
Unreadable source – typically a live kernel holding function objects whose recorded locations have drifted from an edited file – degrades in layers rather than raising: fastcore’s renderer swaps the docments table for the bare signature (with a warning naming the symbol), so doc and doc_key still succeed; and if rendering fails beyond that, doc_key returns None (the symbol lists keylessly) while doc falls back to the docstring plus a note naming the error. Either way the fix for the stale state is a kernel restart:
def _brk(): pass
_brk.__doc__ = 'A stale function.'
_brk.__code__ = _brk.__code__.replace(co_firstlineno=99999)
with warnings.catch_warnings():
warnings.simplefilter('ignore')
assert doc_key(_brk)
res = str(doc(_brk))
assert 'A stale function.' in res
resFor a function, doc renders the docstring and full signature with docments (parameter comments). The closing # doced: name='key' line is the proof-of-sight key (doc_key): verified declarations (llmdojo’s doced) require it, so holding the exact output is what makes a declaration possible:
d = doc(pyskills.skill.skill_test_func)
test_eq(d, doc('pyskills.skill.skill_test_func'))
test_eq(d.splitlines()[-1], f"# doced: skill_test_func={doc_key(pyskills.skill.skill_test_func)!r}")
ddef skill_test_func(
x:int=0, # the input
)->str: # the output"""A test function"""
# doced: skill_test_func='dbcd'
fmt_sig
def fmt_sig(
f, ps:NoneType=None
):Call self as a function.
This is a simple helper that removes module names from str(signature(...)):
def f(a:Path|str = "aa"): ...
print(fmt_sig(f))(a: Path | str = 'aa')
For a class, doc shows the class hierarchy, docstring, __init__ signature, and all public methods/properties with their first docstring line:
doc(pyskills.skill.SkillTestClass)class SkillTestClass(str):
"""Some class.
More info about it."""
def __init__(self): ...
def f(self, x: int = 0) -> str: ... # A test method
@property
def g(self) -> str: ... # A test prop
# doced: SkillTestClass='2d21'
For a module, doc shows the docstring, all public classes and functions with their signatures and first docstring line, submodules, and any allow() calls:
print(doc(pyskills.skill)[-450:]))` them as a smoke test.
## Creating pyskills
`from pyskills import createskill; doc(createskill)` for how to build and register your own pyskill modules.
"""
## types:
- class SkillTestClass(str): ... # Some class.
## functions:
- async def async_skill_test_func(x: int = 0) -> str: ... # A test function…
- def skill_test_func(x: int = 0) -> str: ... # A test function…
## elided: 1 submodules. `doc('pyskills.skill', all=True)` lists them.
A module with a very large API surface can set __pyskill_sigs__ = False to elide the types/functions listing from its doc() output: its docstring is then the whole answer, with one line noting the elision. Pass all=True to doc to list the elided symbols anyway.
A package with a substantive docstring — more than the default summary line plus docs link — is treated as curated, so doc() elides its mechanical submodule listing too: a curated package docstring, like the ones nbdev generates from a project’s notebooks, already says which modules matter. all=True restores the listing:
import pyskillss = doc(pyskills)
assert '## submodules:' not in s
assert '## submodules:' in doc(pyskills, all=True)
print(s[-350:])" (units of LLM-usable functionality) via standard Python entry points. An LLM harness (e.g. solveit) discovers available pyskills without importing them, reads lightweight descriptions via AST inspection, and selectively loads chosen pyskills into context using standard imports.
"""
## elided: 3 submodules. `doc('pyskills', all=True)` lists them.
A trailing … on an overview function line marks elided detail: the function has docments, or a docstring beyond its first line, so doc(func) will show more than the overview line did. A complete overview line instead ends with its bracketed doc key: the line already shows everything doc(func) would, so the key can be declared straight from the summary.
def _plain(x): return x
def _rich(x:int=0 # some param
): return str(x)
def _long(x):
"""Summary
More detail."""
_m = types.ModuleType('_m')
_m.__all__ = ['plain','rich','long']
_m.plain,_m.rich,_m.long = _plain,_rich,_longs = doc(_m)
fl = {l.split()[2].split('(')[0]:l for l in s.splitlines() if l.startswith('- def ')}
test_eq([fl[k].endswith('…') for k in ('plain','rich','long')], [False,True,True])
test_eq(fl['plain'], f"- def plain(x) [{doc_key(_plain)}]") # a complete overview line carries its doced key
assert '[' not in fl['rich'] and '[' not in fl['long'] # elided lines carry none: the summary is not the full doc
s# module _m:
## functions:
- def plain(x) [c74a]
- def rich(x: int = 0): … # …
- def long(x): ... # Summary…
docfind
def docfind(
o:str | object, q:str, n:int=2, _pre:str=''
):Search doc() recursively through xdir(o), looking at submodules, classes, and functions, to depth n
docfind(pyskills.skill, 'test')[' // # module pyskills.skill:',
'SkillTestClass // class SkillTestClass(str):',
'SkillTestClass.f // def f(',
'SkillTestClass.g // def g()->str:"""A test prop"""',
'async_skill_test_func // async def async_skill_test_func(',
'skill_test_func // def skill_test_func(']
import fastcore.toolsdoc(fastcore.tools)# module fastcore.tools:
"""Text and file editing primitives shared by the fastai editing tools
The editors here are string-level: each takes `text` plus edit parameters and returns the new text, raising `ValueError` when an edit can't apply. The file tools below wrap them with path I/O and diff reporting; message-level wrappers live in aidialog. (This module previously held experimental LLM path-editing and command tools, superseded by safecmd, rgapi, and the tools here.) Naming, parameter, and workflow conventions for the whole editing toolkit, this module included, are documented in `fastcore.editskill`, which also re-exports these tools alongside `fastcore.nbio`'s.
`line_hash`, `lnhash`, and `lnhash_at` implement the [exhash](https://answerdotai.github.io/exhash) line-address format in pure Python: `lineno|hash|`, where the hash is 4 hex chars of crc32. They let any tool create lnhash-addressed views of text it holds, without depending on the exhash package.
File tools wrap the primitives with path I/O, returning unified diffs of what changed ("none: No changes." / "error: ..." otherwise). The path is the first argument, e.g:
view_file('~/a/b.py', 3)
create_file('~/a/b/c.py', 'content here')
file_str_replace('myfile.py', 'old_name', 'new_name')
file_del_lines('myfile.py', 2, 4)
file_replace_lines('myfile.py', new_content=src) # no line numbers: replace the entire contents
`file_str_replace`, `file_strs_replace`, and `file_del_lines` support `re_filter` and `invert_filter` for targeting only lines matching (or not matching) a regex, like ex's `g//` and `g!//`, combined with `start_line`/`end_line` to restrict to a region. `ast_replace(text, repls)` and `file_ast_replace(path, repls)` apply ast-grep `(pattern, replacement)` rules with `$VAR` metavariables (requires the optional `remold` package).
Docs: https://fastcore.fast.ai/tools.html.md
"""
## functions:
- def file_insert_line(path: str, insert_line: int, new_str: str): ... # Insert new_str at specified line number…
- def file_str_replace(path: str, old_str: str, new_str: str, **replace_params): ... # Replace occurrence(s) of old_str with new_str…
- def file_strs_replace(path: str, old_strs: list[str], new_strs: list[str], **replace_params): ... # Replace multiple strings simultaneously…
- def file_replace_lines(path: str, start_line: int = None, end_line: int = None, new_content: str = ''): ... # Replace line range with new content; the defaults replace the entire contents…
- def file_del_lines(path: str, start_line: int, end_line: int, re_filter: str = None, invert_filter: bool = False): ... # Delete line range; deletion is destructive, so both line numbers must be given explicitly (`1, -1` for all lines)…
- def file_ast_replace(path: str, repls: list): ... # Apply ast-grep structural pattern replacements to python source…
- def insert_line(text: str, insert_line: int, new_str: str): ... # Insert new_str at specified line number…
- def str_replace(text: str, old_str: str, new_str: str, **replace_params): ... # Replace occurrence(s) of old_str with new_str…
- def strs_replace(text: str, old_strs: list[str], new_strs: list[str], **replace_params): ... # Replace multiple strings simultaneously…
- def replace_lines(text: str, start_line: int = None, end_line: int = None, new_content: str = ''): ... # Replace line range with new content; the defaults replace the entire contents…
- def del_lines(text: str, start_line: int, end_line: int, re_filter: str = None, invert_filter: bool = False): ... # Delete line range; deletion is destructive, so both line numbers must be given explicitly (`1, -1` for all lines)…
- def line_hash(line: str) -> str: ... # 4-char hex hash of `line`…
- def lnhash(lineno: int, line: str) -> str: ... # `lineno|hash|` exhash address for `line` at `lineno`…
- def lnhash_at(s: str | list | tuple, line: int) -> str: ... # `lineno|hash|` exhash address of line `line` of `s`…
- def view_file(path: str, start_line: int = 1, end_line: int = None, nums: bool = True, lnhashs: bool = False): ... # Read file contents, optionally limited to 1-based line range…
- def create_file(path: str, contents: str, overwrite: bool = False): ... # Create a new file with contents. Error if file exists, unless `overwrite`.…
- def file_edit(f, name=None): ... # Wrap text editor `f` as a file editing function: `path` addressing, diff-or-error return [f739]
- def ast_replace(text: str, repls: list): ... # Apply ast-grep structural pattern replacements to python source…
## shared params:
- **replace_params (from `file_str_replace`):
start_line: int = None, # Optional 1-based start line to limit search
end_line: int = None, # Optional 1-based end line to limit search
n_matches: int = None, # Max replacements (None=all)
re_filter: str = None, # If provided, only process lines matching this regex (like g// in ex)
invert_filter: bool = False, # Invert the filter (like g!// in ex)
use_regex: bool = False, # Treat old_str as a regex, and new_str as an `re.sub` template?
doc(fastcore.tools.create_file)def create_file(
path:str, # Path to create (expands `~` if needed)
contents:str, # Contents of file to create
overwrite:bool=False, # Replace the file if it already exists?
):"""Create a new file with contents. Error if file exists, unless `overwrite`."""
# doced: create_file='6883'
Passing several objects returns each one’s doc as its own blank-line-separated section – so a module overview and the functions you’re about to use can be read in one call:
d2 = doc(fastcore.tools, 'fastcore.tools.create_file')
test_eq(d2, doc(fastcore.tools) + '\n\n' + doc(fastcore.tools.create_file))Skills registration
Pyskills can be added as standard modules with pyproject entrypoints. But for convenience, they can also be added to a custom pyskills XDG directory, which is automatically added to sys.path.
ensure_pyskills_dir
def ensure_pyskills_dir():Create xdg pyskills dir and .pth file if needed
pyskills_dir
def pyskills_dir():Directory for user pyskills
pyskills_dir returns the XDG data home path for user pyskills. ensure_pyskills_dir creates that directory if needed and writes a .pth file into site-packages so Python automatically adds it to sys.path. You can drop pyskill modules there without manual path configuration and which can be available across venvs.
clear_mod
def clear_mod(
prefix
):Clear modules starting with prefix from python caches
clear_mod purges all cached modules matching a prefix from sys.modules and invalidates import caches, ensuring a fresh import on next access. Used after enabling/disabling pyskills so changes take effect immediately.
register_pyskill
def register_pyskill(
name, docstr, code:str=''
):Register a pyskill module name in the xdg pyskills dir
enable_pyskill
def enable_pyskill(
name
):Enable pyskill name by creating its dist-info entry point
enable_pyskill creates a minimal dist-info directory with an entry point so the pyskill is listed. register_pyskill also writes an actual module file (with docstring and code) into the pyskills directory, and creates any needed __init__.py files for nested packages.
This lets you programmatically create and register a pyskill without a full package install.
disable_pyskill
def disable_pyskill(
name
):Disable pyskill name by removing its dist-info entry point
disable_pyskill removes the dist-info directory for a pyskill, so it no longer appears in entry point discovery. It also clears the module cache so the pyskill is fully unloaded.
delete_pyskill
def delete_pyskill(
name
):Delete pyskill name module files and dist-info