Sprk is a versatile command line tool, tool template and sample tool set.
To create customized command sets that can be applied, extended and adapted from any directory, by default with a single source file.
Similar in concept to the argparse
module in the Python standard library, it follows a more visual 'help page first' approach, for a clear overview when developing. It allows tasks to be grouped and ordered, makes options for file tree creation especially simple and provides an integrated means of composing content.
In essence, each new tool is created via the layout of its help page. Given that a help page acts as a summary of a tool, identifying its capabilities and how to access them, this provides a useful structure.
The layout as code is simply a list of instances. Each represents a command line option, a process or a resource, e.g. an instruction, or a blank line to space other entries. The instances are ordered as they appear on the page.
For example, a simple tool might have the following help page:
Usage: sprk [--option/-o [ARG ...]]
-g, --greet [NAME] print a greeting, containing NAME if given
-h, --help show the help page
This tool could be created in the sprk source file as follows:
TOOLS.update({"greeter": Sprker()})
TOOLS["greeter"].provide_resources([
USAGE,
BLANK,
Option({
"desc": "print a greeting, containing NAME if given",
"word": "greet",
"char": "g",
"call": lambda _, pars=[]: print(f"Hi{', ' + pars[0] if len(pars) >= 1 else ''}!")
}),
Option(HELP)
])
The USAGE
and BLANK
entries in the list are instances of the Resource class used for features of the page, while HELP
is a dictionary used to instantiate the --help
option, as in the case of the --greet
option above it. The dictionary's call
property could of course be assigned not simply a lambda as here, but a function defined elsewhere in the code.
For basic setup and some ready commands, see Getting started below.
For more detail on creation, see Creating a tool.
For more complex examples, see The sample tools.
- Getting started
- Creating a tool
- Providing resources
- Inserting templates
- Runtime overview
- Code verification
- Development plan
Sprk has been developed with Python versions ranging from 3.8.5 to 3.10.12. The source code should be compatible with versions 3.8.0 onwards.
On a Linux system with a compatible version of Python installed, the source file can be run with the command python3 sprk
while in the same directory, and from elsewhere using the pattern python3 path/to/sprk
. With the same setup, it should be possible to run it from any directory with sprk
alone by a) making it executable, if not already, with chmod +x sprk
and b) placing it in a directory listed on the $PATH
environment variable, e.g. '/bin' or '/usr/bin'.
From here on, for simplicity, the base command is assumed to be sprk
.
The command sprk
, sprk -h
or sprk --help
will show a help page.
sprk -h
On the help page you'll see that the command sprk -B
or sprk --backup
calls a copy of source code to the current directory, as a so-called sprkfile, with the default name 'Sprkfile'. Changes can be made to the code and the changed file copied over the existing sprk source file with the command sprk -U
or sprk --update
.
The source code in this repository provides three sample command line tools:
- creator, with options to create a project folder, initialize Git, create root files and a 'public' directory, open a browser tab pointing to a list of possible licenses and start a local static file server, plus the base sprk options;
- adapter, with options to open browser tabs pointing to this repository and the Python3 documentation and run the docstring interactive examples in the source code, plus the base sprk options;
- combined, the default tool, which includes the options specific to each of the other two, plus the base sprk options.
The three are offered as examples for reference and a starting point for other uses. The wider code provides the underlying logic for tools of far greater scope and complexity.
It is possible to switch among the available tools with the command sprk -S
or sprk --switch
followed by the preferred tool name. Using either of these commands without a tool name will confirm the tool currently being used and list all tools available.
It may be best to take a look at the source file and experiment with the options and code before reading further.
If you'd like to use more than one version of the source file and avoid a new version's sprkfile being overwritten in error, you can change the value of its SPRKFILENAME
constant.
The sprk source code can be made available for use in another file by adding the .py
filename extension to the source file.
If the importing file and source file are in the same directory, it can then be imported using import sprk
. If the source file is in a folder in the same directory, this becomes import <name>.sprk
, with '' being the folder name.
Once imported, the classes, function definitions and variables in the source file are available in the importing file under their usual identifiers prefixed with the identifier in the import
statement. For example, if import sprk
is used, TOOLS
is available under sprk.TOOLS
.
A tool can be created by instantiating either the Runner
class or the Sprker
class.
A configuration dictionary containing certain initial values can be passed when doing so, as below:
tool_1 = Sprker({
"prep": [lambda tool: print("Starting...")],
"show": "all",
"lead": ["project"],
"tidy": [lambda tool: print("Finished.")]
})
The values in this case are:
- one lambda to be run before the standard tasks (
prep
) and one before the program ends (tidy
), each receiving the tool instance; - a messaging level, in this case
all
to override the default and show all messages (options:all
,err
(the default, for errors only) andoff
, for no messaging); - the name of the 'project' pool as a
lead
pool, those which are given priority over other pools, meaning its tasks will be run before tasks in any pools listed later or not listed (see Pools & ranks) below.
Other possible keys are name
for the project name string value, root
, code
and main
for path string values (passed when assigned to pathlib.Path
), batches
for instances of the tool internal Batch class containing items to be built (see Runtime overview below), caps
for a list of call cap dictionaries (see Calls below) and wait
for functions to be run before those in tidy
, intended for blocking processes dependent on tasks or built items.
A tool can also be extended by providing resources and inserting templates (see Providing resources and Inserting templates below).
Each new tool should be added to the TOOLS
dictionary and one of the tools in this dictionary should be assigned to the ACTIVE_TOOL
constant.
In the current source file, the three tools are added to the dictionary immediately.
The Runner
is the basis for the standard tool. It provides for:
- a set of optional actions to be run before the standard tasks (
prep
); - the running of the standard tasks;
- a composition stage (see Runtime overview below);
- a build stage for creation of any folders and files;
- two sets of final optional actions (
wait
andtidy
).
The above order is the order in which these events occur (see Runtime overview below).
The Runner
also provides methods to show the current sprk version number and the help page.
The Sprker
is a descendant of the Runner
providing three additional methods, one to switch among tools, one to back up sprk in the form of a sprkfile and one to update sprk from a sprkfile (see Getting started above).
A tool will usually instantiate the Runner
's Task
class once for each flag in the sprk
command, using the Option
instance corresponding to the flag and any arguments passed to that flag.
It will also instantiate a task for certain instances of the Process
resource (see Resource, Process & Option below).
Once created, tasks are run in the order in which the flags appear in the sprk
command, subject to the effects of any pool
and rank
value (see Pools & ranks below).
A resource is an instance of the Resource
class or one of its descendant Process
and Option
classes.
One or more resources can be provided using the provide_resources
method:
tools["tool_1"].provide_resources([resource_1, ...])
The order in which the resources are passed is the order in which their info values appear on the help page.
The Resource
class has an info
attribute which takes a string value used on the help page.
The string "{BLANK}"
can be passed to create an empty line, "{SPRKV}"
for the sprk version number and "{USING}"
for the current tool name.
The variables BLANK
, SPRKV
and USING
each contain a ready resource instance for a corresponding line. Also available is USAGE
, for the standard usage guide.
The Process
class is a descendant of the Resource
, also accepting an info
value.
If the info
value is not provided, a task is instantiated for this resource every time at least one Option
instance with the same pool
string value is used, in an order determined by the respective rank
integer values (see Pools & ranks below). This may be useful for auxilliary actions or actions always required for a given pool.
This class also has a call
attribute - for a function to be run by the given task - and an items
attribute - for a list of dictionaries defining folders and files to be built by the task (see Runtime overview below). One of the two values is used by default when the task is run.
The Option
class is a descendant of the Process
, also accepting the pool
, rank
, call
and items
values, as well as the char
, word
, args
and desc
string values.
The char
value is a corresponding single-character flag (e.g. 'a'), the word
value a multi-character flag (e.g. 'add'), the args
value any arguments the flag expects, and the desc
value a description of the task. The four are combined automatically into an info
value.
Below is an example of an Option
instantiation to enable creation of a project folder, as in the source file in this repository.
Option({
"pool": "project",
"rank": 1,
"desc": "create a project folder here, with NAME if given",
"word": "folder",
"char": "f",
"call": start_project,
"args": ["[NAME]"]
})
The values in this case are:
- a
pool
value of 'project' which ensures that the task is run with other tasks having this value (see next section); - a
rank
value of 1, ensuring that the task is run before any 'project' pool tasks with a higher integer value (see next section); char
,word
,args
anddesc
values giving aninfo
value approximating '-f, --folder [NAME] create a new folder here', meaning the task will be run if the '-f' or '--folder' flag is used;- a function to be called by the task (
call
).
Four option configurations are assigned to constants for ease of reuse in multiple tools, specifically:
- a standard
--backup
option toBACKUP
; - a standard
--update
option toUPDATE
; - a standard
--switch
option toSWITCH
; - a standard
--help
option toHELP
.
The Process
and Option
classes have a pool
attribute which can be used to group tasks so that the tasks are run together. To do so, give each Process
and Option
instance in the group the same pool
string value.
If one pool needs to be run before another pool, or before any unpooled tasks, the pool
string value can be added to the tool's lead
list, e.g. by including the value in the configuration dictionary when instantiating the tool (see Creating a tool above). If the lead
list contains more than one pool the order of the pools in the list is the order in which the pools are run.
Instances with a pool
value can also be given a rank
integer value to set the order that tasks are run inside their pool. A task created from an instance with a lower integer value will be run before a task from an instance with a higher integer value, e.g. a task with a rank
value of 1 will run before a task with a rank
value of 2.
The Process
and Option
classes have a call
attribute which can take a function to be called when the corresponding task is run. If a resource instance has a call
attribute, no other action will be taken by its task.
A function given as a call
value is passed two arguments:
- the resource instance itself as the first parameter;
- any arguments passed to the flag in the
sprk
command as the second parameter (see Task above).
Passing the resource instance allows resource attributes to be used by the function, e.g. any items
value, but also gives the function access to the host tool (see next section).
The number of uses of a particular function can be capped under the caps
key in the tool's state
attribute, with a call cap dictionary present for show_help
by default. A list of additional call cap dictionaries can be included in the configuration dictionary when instantiating a tool (see Creating a tool above).
New or changed tool state can be returned from the function as a dictionary (see Host tool use below).
Instances can also be given an items
list of dictionaries defining folders and/or files to be built, with the following possible keys:
- a
dirname
string value to create a folder or afilename
string value to create a file, in each case with the string as the name (without which a sequentially numbered placeholder is generated); - in the case of a file, a
content
string value for the file content and/or aninput
dictionary further defining content use; - in the case of a folder, an optional
items
list containing dictionaries for any nested folders and files;
The input
dictionary can take a flag
key with the string value 'w' to write the content
value over any existing file with the given name, 'a' (the default value) to append the content or 'i' to insert it. In the case of insertion, the input
dictionary can take the following additional keys:
- an
indent
key with an integer value for number of spaces of indentation (default 0); - an
anchor
key for the insertion point, with a dictionary containing astring
key with the string value after which to insert or anindex
key for an integer value being the index at which to insert (defaultNone
); - a
delims
key, with a dictionary containing anopening
key with the string value to be inserted ahead of the new content and aclosing
key with the string value to be inserted after it (in each case the default being an empty string).
If a resource instance has no call
attribute but has an items
attribute, the items
value is passed to an instance of the tool internal Batch class and queued to be built (see Runtime overview below).
A call
function can access any items
value by use of its first parameter, i.e. the resource instance itself (see Calls above).
Below is an example of an items
dictionary for a simple tree with a use of insertion.
{
"dirname": "folder1",
"items": [
{
"dirname": "folder2"
},
{
"filename": "file1",
"content": "This is appended content."
},
{
"filename": "file2",
"content": "this is inserted content",
"input": {
"flag": "i",
"anchor": {
"string": "Insert here:"
},
"delims": {
"opening": "\n-",
"closing": ";"
},
"indent": 2
}
}
]
}
This creates a directory named 'folder1' containing an empty sub-directory named 'folder2', a file named 'file1' with its content appended and a file named 'file2' with its content inserted. The inserted content is indented by two spaces and positioned following the string 'Insert here:', preceded by a newline and a hyphen and followed by a semi-colon.
A tool's provide_resources
method assigns the tool instance itself to each resource's tool
attribute. This gives the resource instance access to the tool's attributes and methods.
In addition, when a task is run the resource instance passes itself to any call
value (see Calls & items above), making the tool accessible also to that function.
Most notably, the tool has a state
attribute which takes a dictionary. This can be supplemented or updated in the form of a dictionary returned by any resource instance's call
function, allowing values to be stored and used in later tasks.
String values can be provided with substrings from elsewhere in the source file at runtime by use of content variable identifiers.
Top-level values from tool state can be accessed by use of the state variable identifier {STATE:key}
, where 'key' is the top-level key in the state
attribute.
Strings or functions placed on the 'utils' attribute can be accessed by use of the utils variable identifier {UTILS:key}
, where key is the top-level key. Currently available are date
, time
and zone
, the latter for UTC offset.
In each case, the entire identifier is replaced with the given value if it exists or a failure message otherwise.
Four other variables are defined for use in generating the help page. Three of these - BLANK
for an empty string, USING
for the current tool and SPRKV
for sprk name and version - can be applied as is in other contexts. The fourth - ALIGN
- is used to align columns within a text by means of the following procedure:
- placing the identifier on each line of the text at the point at which a number of spaces of offset is required;
- passing the lines with identifier as a list of strings to the
get_offsets
static method on theRunner
class, to get a list of integers each of which is the number of spaces of offset for the respective line; - storing the list of integers on the
state
attribute with a name following the pattern '_offsets', where '' is an arbitrary string; - calling the
handle_variables
method on theBuilder
class for each string with '' as the second argument to replace the identifier.
See the show_help
method on the Runner
class for the existing implementation.
A new variable can be created by adding a corresponding dictionary to the values
dictionary in the tool vars
attribute. The string
property is the value to be sought in the content and the source
property can be either:
- a string value with which the variable identifier is to be replaced;
- a function, the return value of which is used to replace the variable identifier;
- the key for a property of a top-level state value in which a string or a function for replacement can be found.
A function given as a source
property is passed two arguments:
- the
content
string value in which the variable identifier is present as the first parameter; - the corresponding
name
string value as the second parameter.
The delimiters used in handling variables are set in the delims
dictionary and can be changed as preferred.
The Template
class can be used in preparing for and performing actions at the composition stage (see Runtime overview below).
One or more templates can be inserted using the insert_templates
method:
tools["tool_1"].insert_templates([template_1, ...])
The Template
class has a name
attribute which takes a string value for internal reference (without which a sequentially numbered placeholder is generated), a core
attribute containing by default a list with one item ('parts') and a form
attribute which takes a dictionary having by default a parts
key and a calls
key, each containing an empty list.
The core
, parts
and calls
lists can be extended by values passed at instantiation and provided by tasks at Sprk runtime. The parts
list, and any other list added to form
, is intended for values to be processed at the composition stage, while the calls
list is for functions expected to perform this processing. If a key present in the form
dictionary is listed in the core
attribute and that key's list holds at least one item at the composition stage, each function in calls
is called once with the template instance itself as the sole argument.
Below is an example of a Template
instantiation, for composition of '-ignore'-type files, e.g. '.gitignore'. This particular configuration can be found in the source file in this repository.
Template({
"name": "ignores",
"core": ["files"],
"form": {
"rein": [],
"nonr": [],
"sens": [],
"files": [],
"calls": [create_ignores]
}
})
In this case, there are four lists for values provided by tasks at Sprk runtime, three of which identify items to be listed in '-ignore'-type files, specifically:
rein
for items which are reinstalled;nonr
for non-runtime items;sens
for sensitive items.
The fourth list, files
, is for the names of the files to be created. Its key is listed in core
, meaning that if any filenames are added to the files
list at runtime, create_ignores
will be called at the composition stage along with any other functions added to calls
.
This is a fairly complex example. Take a look at the option instance functions in the source file to see how the tool's modify_template
method is used to append new values dynamically and how the create_ignores
function composes the content and queues it for creation at the build stage.
- All tools are instantiated, receive any instances of a resource or template class and are added to the
TOOLS
dictionary, with one assigned to theACTIVE_TOOL
constant. - The name of the tool and any relevant command line arguments are passed to the active tool's
use
method, otherwise theshow_help
method is called. - The tool adds its name to the
state
attribute for later reference. - The tool's
do_work
method calls anyprep
functions, passing the tool instance to each. - At the task execution stage, via the
run_tasks
method, the tool: matches each flag to an option instance, subject to the availability of anycall
function present; queues each option instance and any relevant process instances in instances of the tool internal Task class, each option instance with any relevant arguments; reorders these task instances to prioritize lead pools in lead attribute order and resource instances within pools by rank; for each task instance calls anycall
function present, or otherwise queues in an instance of the tool internal Batch class anyitems
list present, to be built at the build stage. - At the composition stage, via the
compose_items
method, the tool: queues any template instance where any list referenced by key in itscore
attribute contains one or more items; for each such template calls each function listed incalls
. - At the build stage, via the
build_batches
method, the tool: for each batch instance and for each dictionary listed initems
calls anycall
function present; creates any file or folder, descending through any nested items, and generates any names required; in the case of file content, prepares any insertion and replaces identifiers for any variables defined in the tool'svars
attribute. - The tool's
do_work
method calls anywait
functions, passing the tool instance to each. - The tool's
do_work
method calls anytidy
functions, passing the tool instance to each.
The two verification scripts - 'verify.py' and 'verify.sh' - can be used to check types and run the interactive examples.
The verification scripts can be run as follows:
python3 verify.py
sh verify.sh
Either of the two can also be run with the command ./<filename>
while in the same directory, and from elsewhere using the pattern path/to/<filename>
, by first making the file executable, if not already, with chmod +x <filename>
. Both the Python and shell binary are assumed to be accessible via the '/usr/bin' directory, per the hashbang at the top of each file.
./verify.py
./verify.sh
Either of the two - type checking and interactive examples - can instead be run individually using the specific command in 'verify.sh'.
The sprk source code imports from the typing
module in the Python standard library. Type checking uses Mypy, an external tool. The Mypy-related dependencies per Python 3.11 are listed in the file 'requirements.txt'.
To run the type checking only, for sprk:
mypy --python-version=3.8 sprk
The sprk source code includes docstrings with interactive examples verified using the doctest
module in the Python standard library.
To run the interactive examples only, for sprk:
sprk SPRK_TEST_DOCS
A summary is provided for each failure, with no summary indicating success.
The --test
or -t
flag, supported by both the adapter and combined sample tools, will also run the examples, with a more verbose output, providing an overview even on success
Both methods ultimately call the function run_docstring_interactive_examples
, which can be called whenever sprk itself is run by uncommenting the final line of the source code. The more verbose output requires the is_verbose
keyword argument to be set to True
.
run_docstring_interactive_examples(is_verbose=True)
To omit the status message, the is_managed
keyword argument can be set to True
.
The following are possible next steps in the development of the code base. The general medium-term aim is a flexible and fluid toolkit able to support a wide variety of tasks with a low-friction interface. Pull requests are welcome for these and any other potential improvements.
- allow for a confirmation request when overwriting and for precise positioning when appending and inserting content
- add a runtime undo option for rollback on error at the build stage
- provide a Sprker method and sample tool option to modify messaging level at runtime
- support a list of current project directories for ease of movement among them
- enable viewing of snippets stored in source file variables
- enable assignment of snippets to source file variables from the command line or a file, possibly by line number or identifier
- enable extraction of configuration, template insertions and resource provisions to extension file for sharing
- annotate remaining functions
- continue inclusion of interactive examples for testing with
doctest
- add fuller testing with
unittest
- reduce method time and space complexity where possible
- revise to more closely conform to PEP 8
- refactor as more Pythonic