Extendable skeletons for Vim using Python, Click and Jinja2

Problem statement As a member of the software team at Datawise, Inc., I want my part of code to be identifiable. Not just due to vanity, but also in order to be able to clearly identify the responsible when some part of our system fails.

For a long time, I believed that git is a perfect solution to this problem, as while being almost seemless, it allows one to track the authorship line-wise, which is more than enough.

However, something happened recently, which made rethinking this belief. My coworker moved the folder containing my part of code during some routine refactoring. As a result, git changed the authorship of every single line of my code, so my coworker became the "author".

I decided that while git is good most of the time, it would not be bad to supplement it with something simple and crude, so to distinguish my code more clearly and robustly. Inspired by vim-perl plugin for Perl in Vim, I decided that from now on, every single Python file I create, would start with the following header:

"""===============================================================================

        FILE: {filename}.py

       USAGE: ./{filename}.py

 DESCRIPTION: 

     OPTIONS: ---
REQUIREMENTS: ---
        BUGS: ---
       NOTES: ---
      AUTHOR: Alex Leontiev ({email})
ORGANIZATION: 
     VERSION: ---
     CREATED: 2020-11-13T15:54:47.995590
    REVISION: ---

==============================================================================="""

Main work

Part 1: Python Script

It was not very difficult. I put together the following simple script (you can find it at https://github.com/nailbiter/for/blob/master/forpython/new_file.py):

#!/usr/bin/env python3
from os.path import realpath, split, basename, join, splitext
import os.path
from os import access, X_OK, walk
from jinja2 import Template
import click
from datetime import datetime


def _get_template_dirname():
    dirname, _ = split(realpath(__file__))
    return join(dirname, "_new_file")


def _get_template_names():
    _, _, fns = next(walk(_get_template_dirname()))
    _TEMPLATE_EXT = ".jinja.py"
    return [fn[:-len(_TEMPLATE_EXT)] for fn in fns if fn.endswith(_TEMPLATE_EXT)]


def _render_template(fn, **kwargs):
    with open(join(_get_template_dirname(), fn)) as f:
        return Template(f.read()).render({
            **kwargs,
            "os": {"path": os.path},
            "converters": {
                "snake_to_camel": lambda s: "".join([s_.capitalize() for s_ in s.split("_")]),
            }})


@click.command()
@click.argument("fn", type=click.Path())
@click.option("-s", "--stdout", is_flag=True)
@click.option("-e", "--email", envvar="EMAIL", default="***@gmail.com")
@click.option("-o", "--organization", envvar="ORGANIZATION", default="")
@click.argument("archetype", type=click.Choice(_get_template_names()), default="default")
def new_file(fn, email, organization, archetype, stdout=False):
    s = _render_template(f"{archetype}.jinja.py",
                         filename=fn,
                         now=datetime.now(),
                         email=email,
                         is_executable=access(fn, X_OK),
                         organization=organization
                         )
    if stdout:
        print(s)
    else:
        with open(fn, "w") as f:
            f.write(s)


if __name__ == "__main__":
    new_file()

The script is basically used as

./new_file.py filename.py [archetype]

where optional archetype (hi, maven) is the name of any of the predefined templates:

  1. default (which is the default)
  2. click (the template for a click script)
  3. class (the template for a Python file containing a definition of a single class)
  4. test (the template for unittest)

When being called as above, the script takes the corresponding template, performs the necessary substititions, and writes it to filename.py. The additional flag -s/--stdout overwrites this behaviour, instead writing the rendered template to stdout. Also, -e and -o flags and corresponding EMAIL and ORGANIZATION environment variables are used to insert one's email and organization to the header. Incidentally, I found that it is useful to use direnv to change these environment variables based on the directory where source code is located, so to distinguish my own codes and the ones I do during my worktime.

Part 2: Hooking it into Vim Finally, I need to somehow hook in into my Vim, so that ideally this script gets run every time I open the new file. Unfortunately, I did not find a way to do this prettily. Moreover, now that script supports multiple archetypes, it is in principle impossible to decide which one to use at the time of file creation.

Therefore, I added the following to my .vimrc:

...
:au BufNewFile *.py 0r ~/.vim/skeletons/skeleton.py
...

where file skeleton.py is defined as follows:

(maybe, set `chmod +x` and) run `Init`!

Now, every time I create the new .py file, it greets me with the message above, reminding me to run Init <archetype> Vim command, which is defined in my python.vim as

command! -nargs=? Init execute "!~/for/forpython/new_file.py % <args>"

so that it runs the script we made in Part 1.

Future work

  1. if anyone knows how to hook this or similar script into Emacs, I would be happy to hear
  2. maybe, there is some way to hook the script into my Vim more seamlessly?

Recommended Posts

Extendable skeletons for Vim using Python, Click and Jinja2
Initial settings for using Python3.8 and pip on CentOS8
Searching for pixiv tags and saving illustrations using Python
[Python] Accessing and cropping image pixels using OpenCV (for beginners)
This and that for using Step Functions with CDK + Python
Authentication using tweepy-User authentication and application authentication (Python)
[TouchDesigner] Tips for for statements using python
Clustering and visualization using Python and CytoScape
[Python] Reasons for overriding using super ()
[Python] Multiplication table using for statement
Dump, restore and query search for Python class instances using mongodb
Reading and creating a mark sheet using Python OpenCV (Tips for reading well)
Notes for using OpenCV on Windows10 Python 3.8.3.
Notes using cChardet and python3-chardet in Python 3.3.1.
From Python to using MeCab (and CaboCha)
Python development environment for macOS using venv 2016
[50 counts] Key transmission using Python for Windows
Using Python and MeCab with Azure Databricks
[python, multiprocessing] Behavior for exceptions when using multiprocessing
6 Python libraries for faster development and debugging
Tips for using python + caffe with TSUBAME
Implementation and explanation using XGBoost for beginners
Notes for using python (pydev) in eclipse
I'm using tox and Python 3.3 with Travis-CI
SublimeText2 and SublimeLinter --Syntax check for Python3--
Python Jinja2
Effective Python Note Item 12 Avoid using else blocks after for and while loops
Instant method grammar for Python and Ruby (studying)
Head orientation estimation using Python and OpenCV + dlib
vprof --I tried using the profiler for Python
I tried web scraping using python and selenium
[Vim] [Python] Bugs around jedi-vim and multibyte characters?
Causal reasoning and causal search with Python (for beginners)
(Windows) Causes and workarounds for UnicodeEncodeError on Python 3
Notes on installing Python3 and using pip on Windows7
Develop and deploy Python APIs using Kubernetes and Docker
Python development flow using Poetry, Git and Docker
I tried object detection using Python and OpenCV
Python pandas: Search for DataFrame using regular expressions
Get note information using Evernote SDK for Python 3
Create a web map using Python and GDAL
Refined search for Pokemon race values using Python
[Python for Hikari] Chapter 09-02 Classes (Creating and instantiating classes)
[Python / Chrome] Basic settings and operations for scraping
[Python3] Automatic sentence generation using janome and markovify
PDF files and sites useful for learning Python 3
Try using tensorflow ① Build python environment and introduce tensorflow
Create a Mac app using py2app and Python3! !!
Let's make a module for Python using SWIG
Template network config generation with Python and Jinja2
Try using ChatWork API and Qiita API in Python
Install Python and libraries for Python on MacOS Catalina
Install PyCall on Raspberry PI and try using GPIO's library for Python from Ruby