-
Notifications
You must be signed in to change notification settings - Fork 0
old_Readme
SKBS for SKeleton BootStrap
As a programmer I encounter many times moment where I'm telling myself "So boring to always copy-paste the same project" and short after "Why is there no SIMPLE project generator based on simple template I can easily customize and create ???"
...Well I fed up...
...Now there is one. =)
Okay, so skbs was originally a python module with a command line interface permiting to bootstrap all the boiler plate of a project. ...Now, it can generate any kind of file structure dynamically... First, you need to write a template (easy enough thanks to a built-in template to... bootstrap templates...), then either use it passing its path, or install it (copy or symlink) to acces it with @<name>.
Then you call it and it's done.
- Static templates are as simple as the file they contain
- Easy to share templates
- Full python-powered template engine with ridiculous easy synthax
- Configurable template synthax (because double braces expression for latex is hellier than anything)
- Full python interpreter available inside a template
- Dynamic file names
- Dynamic directory names
- Click integration to do complex templates that need argument parsing
- Full-featured include system (because writing and maintaining the same header is non sense)
- Write your first template in less than 5 minutes
- Backend heavily tested with pytest unit tests
pip is the prefered way
pip install skbs
Then, generate the first configuration file
skbs create-config
It will display you the location of the installed templates.
By default, templates will be installed to the system's default location for application data (on linux, generaly ~/.local/share/skbs/
).
Then, install the default templates
skbs install-defaults
Usage
Usage: skbs [OPTIONS] COMMAND [ARGS]... Options: -c, --config PATH Overide the default configuration path --help Show this message and exit. Commands: config-path Prints the path to the in-use configuration file. create-config Create / reset to default the configuration file. gen Generate a skeleton from a template. install Install a new template install-defaults Install default provided templates list List installed templates uninstall Uninstall a template
Usage of gen
Usage: skbs gen [OPTIONS] TEMPLATE DEST -- [ARGS]... Generate a skeleton from a template. template : if template starts with an '@', it will look for an installed template. Else, it will be considered as the template path. dest : the output directory (parents will be created if needed) args : argument passed to the template ( skbs gen <template_name> @help , or skbs gen <template_name> _ -- --help for more informations ) Options: --help Show this message and exit.
Example to generate the template for creating a skbs plugins
skbs gen @skbs <dest>
The arguments and options after the double dash (--
) are sent to the template. For example, for the skbs template
Usage: [OPTIONS] skbs Meta-Template =D This is the template to generate the base skeleton of a custom skbs template Options: --click Generate click command bootstrap -s, --sft FILENAME --help Show this message and exit.
The --click
option adds a click command line parser to easily take options that are usable in the template.
The -s
permits to generate a signle-file template, with only the file following the option.
The template engine used is tempiny. By the way, the unit tests of skbs serves as tests for Tempiny. =)
See tempiny for more details. Here a summary of its features :
The synthax used is the Tempiny one.
First an example to demonstrate all its features
## # need to declare variables that may be used in a generator as "globals" because of the behaviour as : https://stackoverflow.com/a/31298828/1745291 ## global a, b, c This text will be printed as it is lines starting with '##' (or a user-configured prefix) are be python code. ## a=5 # this won't be printed ## # this is a comment in the python script. Won't be printed. if/else/for/while/with/try/except etc blocks don't need indentation. instead, a line containing only '## -' marks the block end. ## for i in range(a) : ## b = a + 1 # you may indent ## c = a +2 # or not, still in the for block. This text will be printed 5 times (a = {{a}}) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{be}} and {{ee}} ## for j in range(3) : You can also nest loops ## - ## # ↑ end of inner loop ## - ## # end of outer loop Expression can be as complex you want as long as they are valid python expression returning something that can be transformed to a string : {{ ";".join( str(i) + f' - {a=},{b=},{c=}' for i in range(2)) }}
will be ouputed as
This text will be printed as it is lines starting with '##' (or a user-configured prefix) are be python code. if/else/for/while/with/try/except etc blocks don't need indentation. instead, a line containing only '## -' marks the block end. This text will be printed 5 times (a = 5) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{ and }} You can also nest loops You can also nest loops You can also nest loops This text will be printed 5 times (a = 5) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{ and }} You can also nest loops You can also nest loops You can also nest loops This text will be printed 5 times (a = 5) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{ and }} You can also nest loops You can also nest loops You can also nest loops This text will be printed 5 times (a = 5) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{ and }} You can also nest loops You can also nest loops You can also nest loops This text will be printed 5 times (a = 5) Btw, between a double brace (2 '{'), you can put expression that will be converted to str, and printed instead. To escape it, two variables are defined by skbs (not tempiny) : `be` (begin of expression) and `ee` (end of expression) : {{ and }} You can also nest loops You can also nest loops You can also nest loops Expression can be as complex you want as long as they are valid python expression returning something that can be transformed to a string : 0 - a=5,b=6,c=7;1 - a=5,b=6,c=7
Basically, there are 3 contexts :
Each line starting by the code prefix (specified in plugin.py
, or '##' by default) is basically python code except for the block delimitation :
in python, the indentation level delimits a block while with tempiny, for pratical use, indentation doesn't matter, and a block is ended by a single dash ( "-" ).
Example :
## a = 5 ## for i in range(a) : ## b = 2 + i ## # Do come stuff ## c=3 # this is still in the for ## - ## # end of the for
Any python code is allowed. This is the reason you should use templates only from trusted sources.
Any line that doesn't start with the code prefix is "text", and will be outputed as is each time the execution flow reaches it.
Basically, you can imagine (btw, this is actually how it is implemented...) each Text context is like a call to print
For example, the following :
This is a text ## for i in range(3): To see ## - how it works
Will output :
This is a test To see To see To see how it works
Inside a Text context, you may want to print an expression (for example a variable value or the result of a python call etc.)
You can do it by surrounding it with the expression delimiters (specified in plugin.py
or '{{' and '}}' by default).
It will be replaced by the expression value at the time of execution. Example
## for i in range(3) Item number {{i}} ## -
Will print
Item number 0 Item number 1 Item number 2
Any python cexpression is allowed.
Once again, you sould only execute trusted templates.
A plugin permits to define a file structure, that will be copied and parsed
/template/ | - plugin.py (optional) | - root/ | | - __include/ (optional) | | | - _raw.include_file1 | | | - _template.include_file2 | | \___ | | - file.c | | - __template_file2.c | | | - subdir/ | | | - __include/ (optional) | | | | - include_file3 | | | | - include_file4 | | | \___ | | \___ | | - ... | \___ \___
It is a directory hierarchy with an optionnal plugin.py
that defines options of the template and functions usable in them.
The directories, subdirectories and files under root
are copied following the same structure (except for dynamic names, explained later).
A file name could have a first prefix, either _opt.
or _forced.
, then a second either _raw.
or _template.
.
opt is for "optional", if the file exists alread, it won't be overwritten.
forced is the opposite
raw means the file will be copied as is
template means the file will be parsed by tempiny.
In the output, the prefixes will obviously be removed from the name
if the first prefix is omited, forced is assumed, and if the second is ommited, template is assumed. This behaviour and the prefixes can be changed.
Alternatively, a template could also be a single file. In this case, the first line of the file defines the synthax:
<code block marker> # <expr start>value<expr end>
For example, the Tempiny C synthax would be defined as:
//# # {{value}}
You can also keep the plugin.py way if you want. in this case, the template is named as the containing directory, and root should be a file. The tempiny synthax should be defined in the plugin.py as for regular directory template. A directory __include
can be present at the same level to include files in it from the template. A plugin.py can also be present to add more complexe logic.
In both cases, the file is considered forced and template. The is_opt
global variable can be set from inside a code block of the template to change the optional flag.
The output name of a single file template is the one provided by the user at template invokation. It can be overrided using the new_path
mechanism.
This file is used to define the configuration and all the complex logic of the template, such as the option parser (click) and the function / variables available inside the template.
It can define a variable config
which should be a skbs.pluginutils.Config
(aliased as C
without need for importing it)
providing the following settings
conf = C( # Predefined template syntax are Tempiny.PY, Tempiny.C and Tempiny.TEX : # Tempiny.C = dict(stmt_line_start=r'//#', begin_expr=', end_expr=') # Tempiny.PY = dict(stmt_line_start=r'##', begin_expr=', end_expr=') # Tempiny.TEX = dict(stmt_line_start=r'%#', begin_expr='<<', end_expr='>>') tempiny = [ ('*' : Tempiny.PY), ], opt_prefix = '_opt.', force_prefix = '_force.', raw_prefix = '_raw.', template_prefix = '_template.', pathmod_filename = '__pathmod', ) conf.dir_template_filename = conf.tamplte_prefix
conf.tempiny
permits to change the Tempiny dialect for files matching the pattern in the first element of the pair. The second argument is the dialect. Tempiny
is already defined in the scope, no need for importing it.
if the opt prefix is defined as ""
or None
, files without prefix will be considered optional and to force overwrite, they should have the force prefix.
The same applies for raw
It can also provide a plugin
variable that could be anything and will be define in the templates' scope as plugin
and _p
. Any function, constant etc. defined on it will be accessible. It is recommanded to define it as a skbs.pluginutils.Confg
(or simply C
)
An instance of C
behave like a dict
with the values accessible as attributes
c = C(a=5, b=6) c['a'] # == 5 c.b # == 6
it implements the dict interface. (see the source code for more details)
You can add a docstring at the start of the file to provide an help message when the user ask for the plugin help.
Alternatively, you can also define locally (without global
statement) a variable help
containing this message (the first method is the recommended one though).
You may also call endOfPlugin
to stop the execution without error,
or raise PluginError(<help msg>)
if an error occured.
If the click flag was passed when bootstraping the template, a click command line is added. To add user passed variable to the template via _p.<var_name>, just add it as an option
plugin = C() @click.command(help=__doc__) @click.option('--auther') # <--- Here is an option. its vallue is available with _p.author def main(**kwargs): plugin.update(kwargs) with click.Context(main) as ctx: __doc__ = main.get_help(ctx) if ask_help : raise EndOfPlugin() invokeCmd(main, args)
Template files are files starting with the conf.template_prefix
defined in plugin.py
. These are template using the previously seen tempiny synthax.
Some python symbols are predefined :
plugin
or_p
: reference to theplugin
variable as defined inplugin.py
dest
: The destination file of the template
(see the Reference ofr more details)
A template can define sections to overwrite only some parts of a file.
Depending of the value of keep_only_sections
, either the template will replace only the sections of an existing file, or, it will keep only the sections.
In both cases, you can decide which section is overriten or not.
A section is delimited by a call to beginSection()
and endSection()
. (see the reference for more details).
Sections are retrieved in the existing file using some pattern matching, either static (the n
following line of beginSection()
or preceeding endSection()
), or dynamic (using a callback).
Section not found are added at a placeholder()
. It is matched the same way as a section.
Example
beginSection(placeholder='pl') //START OF THE SECTION section content //END OF THE SECTION endSection() ...Some content... placeholder('pl') //PLACEHOLDER
..."section content" will replace whatever is between "//START OF THE SECTION" and "//END OF THE SECTION" in the existing file. if not found but there is a "//PLACEHOLDER" line, then it will be placed just before.
An include(path, **kwargs)
function is provided in the templates scope. It will search for any __include/path
file existing in any parent directory. inside an __include
directory, the prefixes raw and template workxs, but not the opt and force ones.
Any kwargs will be accessible from the included template as global variable, and can be modified.
This function returns the output of the included file (it is commonly used as an inline expression)
Inside a template, one can define the local variable new_path
that will contain the new path for the file, relative to the destination.
The easiest way is by doing
## new_path = dest.with_name('new_name')
You can also cancel the execution of the file template, and decide to exclude it by calling exclude()
inside.
Providing a new_path
of None
has the same effect but won't stop the file template execution.
The same applies for directories, except you have to define a special file in it called the same as the template prefix alone, or the default template ("_template.
") if the template is set to None
or an empty string.
This file should be raw python (no "## " prefixes).
Click is already available in the scope without need for importing it. To use it to parse the args, inspire you from this example
plugin = C() @click.command() @click.option('--name', '-n', type=str, prompt=True) # prompt=True will prompt the value if not provided @click.option('--with_db/--no-db', prompt=True) def main(**kwargs): plugin.update(kwargs) invokeCmd(main, args) # invoke the click command this way makes it behave nicely with skbs
...This code is all that is needed
the default @skbs template provides the click boiler plate to handle the --help option