sane is a command runner made simple.

Overview

Sane

sane is a command runner made simple.

Bit depressing, eh?

What

sane is:

  • A single Python file, providing
  • A decorator (@recipe), and a function (sane_run)

sane does not:

  • Have its own domain specific language,
  • Have an install process,
  • Require anything other than python3,
  • Restrict your Python code.

Why

  • More portable

At ~600 lines of code in a single file, sane is extremely portable, being made to be distributed alongside your code base. Being pure Python makes it cross-platform and with an extremely low adoption barrier. sane does not parse Python, or do otherwise "meta" operations, improving its future-proofness. sane aims to do only as much as reasonably documentable in a single README, and aims to have the minimum amount of gotchas, while preserving maximum flexibility.

  • More readable

Its simple syntax and operation make it easy to understand and modify your recipe files. Everything is just Python, meaning neither you nor your users have to learn yet another domain specific language.

  • More flexible

You are free to keep state as you see fit, and all correct Python is valid. sane can function as a build system or as a command runner.

Example

Below is a sane recipes file to compile a C executable (Makefile style).

"""make.py

Exists in the root of a C project folder, with the following structure


   └ make.py
   └ sane.py

   └ src
      └ *.c (source files)

The `build` recipe will build an executable at the root.
The executable can be launched with `python make.py`.
"""

import os
from subprocess import run
from glob import glob

from sane import *
from sane import _Help as Help

CC = "gcc"
EXE = "main"
SRC_DIR = "src"
OBJ_DIR = "obj"

COMPILE_FLAGS = '-g -O2'

# Ensure source and objects directories exist
os.makedirs(SRC_DIR, exist_ok=True)
os.makedirs(OBJ_DIR, exist_ok=True)

sources = glob(f'{SRC_DIR}/*.c')

# Define a compile recipe for each source file in SRC_DIR
for source_file in sources:
    basename = os.path.basename(source_file)
    obj_file = f'{OBJ_DIR}/{basename}.o'
    objects_older_than_source = (
        Help.file_condition(sources=[source_file], targets=[obj_file]))
    
    @recipe(name=source_file,
            conditions=[objects_older_than_source],
            hooks=['compile'],
            info=f'Compiles the file \'{source_file}\'')
    def compile():
        run(f'{CC} {COMPILE_FLAGS} -c {source_file} -o {obj_file}', shell=True)

# Define a linking recipe
@recipe(hook_deps=['compile'],
        info='Links the executable.')
def link():
    obj_files = glob(f'{OBJ_DIR}/*.o')
    run(f'{CC} {" ".join(obj_files)} -o {EXE}', shell=True)

# Define a run recipe
@recipe(recipe_deps=[link],
        info='Runs the compiled executable.')
def run_exe():
    run(f'./{EXE}', shell=True)

sane_run(run_exe)

The Flow of Recipes

sane uses recipes, conditions and hooks.

Recipe: A python function, with dependencies (on either/both other recipes and hooks), hooks, and conditions.

Conditions: Argument-less functions returning True or False.

Hook: A non-unique indentifier for a recipe. When a recipe depends on a hook, it depends on every recipe tagged with that hook.

The dependency tree of a given recipe is built and ran with sane_run(recipe). This is done according to a simple recursive algorithm:

  1. Starting with the root recipe,
  2. If the current recipe has no conditions or dependencies, register it as active
  3. Otherwise, if any of the conditions is satisfied or dependency recipes is active, register it as active.
  4. Sort the active recipes in descending depth and order of enumeration,
  5. Run the recipes in order.

In concrete terms, this means that if

  • Recipe A depends on B
  • B has some conditions and depends on C
  • C has some conditions

then

  • If any of B's conditions is satisfied, but none of C's are, B is called and then A is called
  • If any of C's conditions is satisfied, C, B, A are called in that order
  • Otherwise, nothing is ran.

The @recipe decorator

Recipes are defined by decorating an argument-less function with @recipe:

@recipe(name='...',
        hooks=['...'],
        recipe_deps=['...'],
        hook_deps=['...'],
        conditions=[...],
        info='...')
def my_recipe():
    # ...
    pass

name: The name ('str') of the recipe. If unspecified or None, it is inferred from the __name__ attribute of the recipe function. However, recipe names must be unique, so dynamically created recipes (from, e.g., within a loop) typically require this argument.

hooks: list of strings defining hooks for this recipe.

recipe_deps: list of string names that this recipe depends on. If an element of the list is not a string, a name is inferred from the __name__ attribute, but this may cause an error if it does not match the given name.

hook_deps: list of string hooks that this recipe depends on. This means that the recipe implicitly depends on any recipe tagged with one of these hooks.

conditions: list of callables with signature () -> bool. If any of these is True, the recipe is considered active (see The Flow of Recipes for more information).

info: a description string to display when recipes are listed with --list.

sane_run

sane_run(default=None, cli=True)

This function should be called at the end of a recipes file, which will trigger command-line arguments parsing, and run either the command-line provided recipe, or, if none is specified, the defined default recipe. (If neither are defined, an error is reported, and the program exits.)

(There are exceptions to this: --help, --list and similars will simply output the request information and exit.)

By default, sane_run runs in "CLI mode" (cli=True). However, sane_run can also be called in "programmatic mode" (cli=False). In this mode, command-line arguments will be ignored, and the default recipe will be ran (observing dependencies, like in CLI mode). This is useful if you wish to programmatically call upon a recipe (and its subtree).

To see the available options and syntax when calling a recipes file (e.g., make.py), call

python make.py --help

Installation

It is recommended to just include sane.py in the same directory as your project. You can do this easily with curl

curl 'https://raw.githubusercontent.com/mikeevmm/sane/master/sane.py' > sane.py

However, because it's convenient, sane is also available to install from PyPi with

pip install sane-build

Miscelaneous

_Help

sane provides a few helper functions that are not included by default. These are contained in a Help class and can be imported with

from sane import _Help as Help

Help.file_condition

Help.file_condition(sources=['...'],
                    targets=['...'])

Returns a callable that is True if the newest file in sources is older than the oldest files in targets, or if any of the files in targets does not exist.

sources: list of string path to files.

targets: list of string path to files.

Logging

The sane logging functions are exposed in Help as log, warn, error. These take a single string as a message, and the error function terminates the program with exit(1).

Calling python ... is Gruesome

I suggest defining the following alias

alias sane='python3 make.py'

License

This tool is licensed under an MIT license. See LICENSE for details. The LICENSE is included at the top of sane.py, so you may redistribute this file alone freely.

Support

💕 If you liked sane, consider buying me a coffee.

You might also like...
A CLI tool to build beautiful command-line interfaces with type validation.
A CLI tool to build beautiful command-line interfaces with type validation.

Piou A CLI tool to build beautiful command-line interfaces with type validation. It is as simple as from piou import Cli, Option cli = Cli(descriptio

A minimal and ridiculously good looking command-line-interface toolkit
A minimal and ridiculously good looking command-line-interface toolkit

Proper CLI Proper CLI is a Python package for creating beautiful, composable, and ridiculously good looking command-line-user-interfaces without havin

Simple cross-platform colored terminal text in Python
Simple cross-platform colored terminal text in Python

Colorama Makes ANSI escape character sequences (for producing colored terminal text and cursor positioning) work under MS Windows. PyPI for releases |

Amazing GitHub Template - Sane defaults for your next project!

🚀 Useful README.md, LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, GitHub Issues and Pull Requests and Actions templates to jumpstart your projects.

Sane and flexible OpenAPI 3 schema generation for Django REST framework.

drf-spectacular Sane and flexible OpenAPI 3.0 schema generation for Django REST framework. This project has 3 goals: Extract as much schema informatio

Sane color handling of osx's accent and highlight color from the commandline
Sane color handling of osx's accent and highlight color from the commandline

osx-colors Sane command line color customisation for osx, no more fiddling about with defaults, internal apple color constants and rgb color codes Say

pyLodeRunner - Classic Lode Runner clone made in pyxel (Python)
pyLodeRunner - Classic Lode Runner clone made in pyxel (Python)

pyLodeRunner Classic Lode Runner clone made in pyxel (Python) Controls arrow key : move the player X : dig right side Z : dig left side ESC : quit gam

Green is a clean, colorful, fast python test runner.
Green is a clean, colorful, fast python test runner.

Green -- A clean, colorful, fast python test runner. Features Clean - Low redundancy in output. Result statistics for each test is vertically aligned.

Green is a clean, colorful, fast python test runner.
Green is a clean, colorful, fast python test runner.

Green -- A clean, colorful, fast python test runner. Features Clean - Low redundancy in output. Result statistics for each test is vertically aligned.

Django test runner using nose

django-nose django-nose provides all the goodness of nose in your Django tests, like: Testing just your apps by default, not all the standard ones tha

BDD library for the py.test runner

BDD library for the py.test runner pytest-bdd implements a subset of the Gherkin language to enable automating project requirements testing and to fac

Selects tests affected by changed files. Continous test runner when used with pytest-watch.

This is a pytest plug-in which automatically selects and re-executes only tests affected by recent changes. How is this possible in dynamic language l

Local continuous test runner with pytest and watchdog.

pytest-watch -- Continuous pytest runner pytest-watch a zero-config CLI tool that runs pytest, and re-runs it when a file in your project changes. It

A bare-bones POC container runner in python

pybox A proof-of-concept bare-bones container written in 50 lines of python code. Provides namespace isolation and resource limit control Usage Insta

GitLab CI security tools runner
GitLab CI security tools runner

Common Security Pipeline Описание проекта: Данный проект является вариантом реализации DevSecOps практик, на базе: GitLab DefectDojo OpenSouce tools g

Basic python tools to generate shellcode runner in vba

vba_bin_runner Basic python tools to generate shellcode runner in vba. The stub use ZwAllocateVirtualMemory to allocate memory, RtlMoveMemory to write

Scripts for training an AI to play the endless runner Subway Surfers using a supervised machine learning approach by imitation and a convolutional neural network (CNN) for image classification
Scripts for training an AI to play the endless runner Subway Surfers using a supervised machine learning approach by imitation and a convolutional neural network (CNN) for image classification

About subwAI subwAI - a project for training an AI to play the endless runner Subway Surfers using a supervised machine learning approach by imitation

Example project demonstrating using Django’s test runner with Coverage.py

Example project demonstrating using Django’s test runner with Coverage.py Set up with: python -m venv --prompt . venv source venv/bin/activate python

The example shows using local self-hosted runners on-premises by making use of a runner on a Raspberry Pi with LED's attached to it

The example shows using local self-hosted runners on-premises by making use of a runner on a Raspberry Pi with LED's attached to it

Owner
Miguel M.
Hi, nice to meet you. I study physics, program (games) and make music. You can find my portfolio below!
Miguel M.
Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object.

Python Fire Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object. Python Fire is a s

Google 23.6k Dec 31, 2022
Command line animations based on the state of the system

shell-emotions Command line animations based on the state of the system for Linux or Windows 10 The ascii animations were created using a modified ver

Simon Malave 63 Nov 12, 2022
Python composable command line interface toolkit

$ click_ Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Comm

The Pallets Projects 13.3k Dec 31, 2022
Library for building powerful interactive command line applications in Python

Python Prompt Toolkit prompt_toolkit is a library for building powerful interactive command line applications in Python. Read the documentation on rea

prompt-toolkit 8.1k Dec 30, 2022
Cleo allows you to create beautiful and testable command-line interfaces.

Cleo Create beautiful and testable command-line interfaces. Cleo is mostly a higher level wrapper for CliKit, so a lot of the components and utilities

Sébastien Eustace 984 Jan 2, 2023
Color text streams with a polished command line interface

colout(1) -- Color Up Arbitrary Command Output Synopsis colout [-h] [-r RESOURCE] colout [-g] [-c] [-l min,max] [-a] [-t] [-T DIR] [-P DIR] [-d COLORM

nojhan 1.1k Dec 21, 2022
Pythonic command line arguments parser, that will make you smile

docopt creates beautiful command-line interfaces Video introduction to docopt: PyCon UK 2012: Create *beautiful* command-line interfaces with Python N

null 7.7k Dec 30, 2022
Python Command-line Application Tools

Clint: Python Command-line Interface Tools Clint is a module filled with a set of awesome tools for developing commandline applications. C ommand L in

Kenneth Reitz Archive 82 Dec 28, 2022
Corgy allows you to create a command line interface in Python, without worrying about boilerplate code

corgy Elegant command line parsing for Python. Corgy allows you to create a command line interface in Python, without worrying about boilerplate code.

Jayanth Koushik 7 Nov 17, 2022
prompt_toolkit is a library for building powerful interactive command line applications in Python.

Python Prompt Toolkit prompt_toolkit is a library for building powerful interactive command line applications in Python. Read the documentation on rea

prompt-toolkit 8.1k Jan 4, 2023