Xacto started with a directory of Python tools. At work we had a thousand little command-line tools and entry points, added at different times in different contexts, and they were obnoxious to keep track of. The functions already had names, arguments, defaults, and docstrings. Writing the same information into an argument parser meant maintaining another description of the tools.
I wanted the functions to be the description. Put them in ordinary modules, export the intended callables, and get a command tree from the package tree.
The README used a tools/work.py module with a function shaped like this:
def hard(method, speed=32, *tasks, **params):
"""The difficult version"""
pprint(locals())Its example invocation was:
./do work hard --method=cheat --code=iddqd taskNmethod arrived as a named argument, speed retained its default, the task landed in *tasks, and code passed through into **params. Xacto supplied the argparse structure around the callable.
What the signature could say
The parser builder used inspect.getargspec. It aligned arguments with their defaults, removed the bound instance parameter where appropriate, and derived requiredness and conversion rules. A non-None default supplied its own class as the argument converter. An integer default therefore gave a different interface from a string default.
Boolean defaults became switches. False selected store_true; True selected store_false. That second case was also in the README’s limitations: an option named after a setting could turn the setting off. Deriving the parser removed duplication but did not automatically produce good option names.
Classes supplied another layer. A callable instance could expose methods with the same names as its arguments. Those methods acted as prototypes: their signatures described argument arity, and their docstrings supplied help. A prototype with several fields could produce a named tuple for the parsed value.
This let a tool describe a structured argument using Python syntax it already understood. It also meant the conventions were becoming an API of their own. The schema had not vanished entirely; some of it had moved into the meaning Xacto assigned to otherwise ordinary methods. It was a little strange, and more magical than some people liked. A lot of it rested on convention, because it was written for a Python that had no function annotations to hang anything on.
The executable name selected the tools
The quickstart installed Xacto once and made a symlink named do. The invoked name participated in discovery: try a matching package’s tools module, search nearby filesystem shapes, then consult registered tool entry points. A different executable name could expose another toolbox through the same entry point.
Zippy carried that further. Its bundled interpreter resolved an executable name through package metadata and exposed the selected export as sys.export. Xacto checked sys.export.key before falling back to its usual name discovery, connecting the two tools.
Tool discovery also handled directories inside ZIPs. Where os.walk could not see a real directory, Xacto reconstructed a walk from the zip importer’s file table. The same command hierarchy could live in a checkout or inside the application appended to a Zippy binary.
Help should not import the world
Walking the tools introduced another problem: importing every command could load dependencies for commands the user would never run. Xacto intercepted imports, recorded bindings, and resolved them later. I have described the namespace trick separately in Deferred Namespace.
The deferred-import layer had bugs. In issue 2, a user showed that importing two Jinja2 names on one line made Xacto look for the second as an attribute of the first. A list of requested imports had become an attribute chain. I posted a correction, then immediately questioned whether it broke aliased imports, and left the issue open pending a complete check.
Issue 3 records a separate ordering problem: deferred imports were grouped by the symbol they bound, which could reorder valid statements that reused a name. Both issues remain open.
These are not cosmetic gaps. Import order is observable Python behavior. A tool that offers ordinary Python modules has taken on an obligation to retain it, even if its primary job is generating --help.
Where it stopped
The final commits added basic Python 3 support. They did not complete the README’s testing, completion, output, and forwarding plans. The code belongs to the getargspec era and needs more than a new version number to serve as a current package.
The command interface came from function signatures; discovery and startup pulled the tool much farther into Python’s import machinery. The parser was only part of the tool by the end.