Package: python-dev-cli
Python Developer CLIPython developer CLI enables you to run custom scripts defined in yourpyproject.tomlfile, eliminating the need for
shell scripts and Makefiles, and reducing extraneous cognitive load on your teammates and contributors.OptionalJinja2template support enables you to reference built-in Python syntax, arbitrary Python modules, and even
other scripts from within your script configurations.String together multiple scripts with a single command, to simplify complex workflows.All custom scripts are automatically documented in thedevCLI help page. Just rundev --helpto see exactly what
each script does.Installationpipinstallpython-dev-cliUsageDefine custom scripts in yourpyproject.tomlfile:# pyproject.toml[tool.python-dev-cli.scripts]up="docker compose up -d"down="docker compose down -v --remove-orphans"Then run thedevcommand followed by the script name:devup# docker compose up -ddevdown# docker compose down -v --remove-orphansAll scripts defined in yourpyproject.tomlfile will be automatically documented in thedevCLI help page:dev--help# usage: dev [-h] [-d] {down,up} ...## Python developer CLI for running custom scripts defined in pyproject.toml## options:#   -h, --help            show this help message and exit#   -d, --debug           enable debug logging## available scripts:#   {down,up}#     down                ['docker compose down -v --remove-orphans']#     up                  ['docker compose up -d']Any script that is prefixed with an underscore (_) will be hidden from the help page and cannot be run directly:# pyproject.toml[tool.python-dev-cli.scripts]_foo="foo"_bar="bar"foobar="echo {{ dev._foo }}{{ dev._bar }}"dev-h# usage: dev [-h] [-d] {foobar} ...# Python developer CLI for running custom scripts defined in pyproject.toml## options:#   -h, --help            show this help message and exit#   -d, --debug           enable debug logging## available scripts:#   {foobar}#     foobar              ['echo foobar']You can also define scripts as a list of script references, which will be run in order:# pyproject.toml[tool.python-dev-cli.scripts]black="black --check --config pyproject.toml ."black_fix="black --config pyproject.toml ."ruff="ruff --config pyproject.toml ."ruff_fix="ruff --fix --exit-non-zero-on-fix --config pyproject.toml ."lint=["black","ruff"]lint_fix=["black_fix","ruff_fix"]devlint# black --check --config pyproject.toml .# ruff --config pyproject.toml .devlint_fix# black --config pyproject.toml .# ruff --fix --exit-non-zero-on-fix --config pyproject.toml .By default, scripts can utilizeJinja2template syntax, enabling you to reference built-in Python syntax, arbitrary
Python modules, and even other scripts:# pyproject.toml[tool.python-dev-cli.settings]include=["uuid:uuid4 as uuid"][tool.python-dev-cli.scripts]python="echo {{ 1 + 1 }}"module="echo {{ uuid() }}"other_scripts="echo {{ dev._foo }}{{ dev._bar }}"_foo="foo"_bar="bar"devpython# 2devmodule# 2b2e0b9e-0b9e-4a4a-9a9a-9a9a9a9a9a9adevother_scripts# foobarScript template functionality can be disabled, if you prefer to keep things simple. See theSettingssection below for
more information.SettingsYou can configure this package by adding atool.python-dev-cli.settingssection to yourpyproject.tomlfile (default
values shown):# pyproject.toml[tool.python-dev-cli.settings]enable_templates=trueparse_help=trueinclude=[]script_refs="dev"enable_templatesEnable or disable the use ofJinja2templates in your scripts. If you disable this, all other settings will be ignored
and scripts will be run as-is, without any preprocessing.NOTE:Disabling this setting will prevent you from referencing other scripts from within a script, but you will
still be able to reference other scripts in a list of scripts.For example, this will still work:# pyproject.toml[tool.python-dev-cli.settings]enable_templates=false[tool.python-dev-cli.scripts]_foo="echo foo"_bar="echo bar"foobar=["_foo","_bar"]devfoobar# foo# barHowever, this will not work as intended because the template will not be parsed:# pyproject.toml[tool.python-dev-cli.settings]enable_templates=false[tool.python-dev-cli.scripts]_foo="foo"_bar="bar"foobar="echo {{ dev._foo }}{{ dev._bar }}"devfoobar# {{ dev._foo }}{{ dev._bar }}parse_helpEnable or disable the parsing of script templates in thedevCLI help page. If you disable this, the help page will
show the raw script template, rather than the parsed script command.# pyproject.toml[tool.python-dev-cli.settings]parse_help=false[tool.python-dev-cli.scripts]_foo="foo"_bar="bar"foobar="echo {{ dev._foo }}{{ dev._bar }}"dev-h# usage: dev [-h] [-d] {foobar} ...# Python developer CLI for running custom scripts defined in pyproject.toml## options:#   -h, --help            show this help message and exit#   -d, --debug           enable debug logging## available scripts:#   {foobar}#     foobar              ['echo {{ dev._foo }}{{ dev._bar }}']includeA list of modules to include in theJinja2environment, when parsing scripts. This enables you to reference Python
modules in your scripts:# pyproject.toml[tool.python-dev-cli.settings]include=["os:getcwd","os:getenv as env"][tool.python-dev-cli.scripts]test_docker="docker run -it --rm -v {{ getcwd() }}:/app -w /app --name test {{ env('MY_DOCKER_IMAGE', 'python:3.11-alpine') }} echo test"devtest_docker# docker run -it --rm -v /Users/username/Projects/dev:/app -w /app --name test python:3.11-alpine echo testMY_DOCKER_IMAGE="python:3.11-slim-bookworm";devtest_docker# docker run -it --rm -v /Users/username/Projects/dev:/app -w /app --name test python:3.11-slim-bookworm echo testValid formats for including modules are:Include SyntaxPython Equivalent"os"import os"os:path"from os import path"os.path:join"from os.path import join"os.path:join as path_join"from os.path import join as path_joinNOTE:Any module available in your project can be made available to your scripts, including third-party modules
and even your own modules.script_refsScripts can contain references to other scripts, using the{{ dev.my_script }}syntax:# pyproject.toml[tool.python-dev-cli.scripts]_foo="foo"_bar="bar"foobar="echo {{ dev._foo }}{{ dev._bar }}"devfoobar# foobarThe name of thedevobject is configurable using thescript_refssetting. For example, you could change it toscripts:# pyproject.toml[tool.python-dev-cli.settings]script_refs="scripts"[tool.python-dev-cli.scripts]_foo="foo"_bar="bar"foobar="echo {{ scripts._foo }}{{ scripts._bar }}"devfoobar# foobarCaveatsShell SyntaxScripts are run in a Python subprocess usingsubprocess.run(), not the shell interpreter that thedevCLI is being
run in. As a result, shell features such as pipes|; filename wildcards*; redirects>,>>,<; backgrounding&; and expansion of~to a user's home directory are not supported.NOTE:Environment variables can still be referenced as you would in a shell script (e.g.$HOMEor${HOME}),
because thedevCLI usesos.path.expandvars()when resolving scripts.To work around this limitation, you can use thesubprocessmodule to run shell commands with theshell=Trueflag:# pyproject.toml[tool.python-dev-cli.settings]include=["subprocess"][tool.python-dev-cli.scripts]shell="echo {{ subprocess.run('echo foo | tr a-z A-Z', shell=True, capture_output=True).stdout.decode().strip() }}"devshell# FOOHowever, by not running scripts with theshell=Trueflag, thedevCLI has the benefit of making your scripts more
cross-platform compatible, as they do not rely on any shell-specific syntax. This means your scripts will typically work
on Windows, Linux, and macOS (unless you do something like in the example above).Also, it's worth pointing out that the example above would be better written as a Python script, rather than adevscript in yourpyproject.tomlfile:# scripts/shell.pyimportsubprocessdefmain()->str:returnsubprocess.run('echo foo | tr a-z A-Z',shell=True,capture_output=True).stdout.decode().strip()if__name__=='__main__':output:str=main()print(output)Then, you could reference it in your script definition:# pyproject.toml[tool.python-dev-cli.scripts]shell="python -m scripts.shell"This is a better design pattern, as it keeps your Python logic in a Python file rather than a TOML file, where it cannot
be easily tested or linted.An even better approach would be to use the various Python implementations of shell-like features (e.g.glob,fnmatch,os.walk(),os.path.expanduser(), andshutil) to achieve the desired result, rather than calling
out to a shell command.Script NamesScript names must be valid Python identifiers, which means they must start with a letter or underscore (_), and can
only contain letters, numbers, and underscores (_).  This is because script names are used as attribute names on thedevobject, which means they must be valid Python identifiers.Also, keep in mind that any scripts prefixed with an underscore (_) will be hidden from the help page and cannot be
run directly. Think of these as "private" script variables, which can only be referenced by other scripts.Template ParsingBy default, script templates are parsed usingJinja2before being run. They are also parsed whenever thedevCLI
help page is displayed, to generate examples of the actual commands that will be run for each script. This means that
any function calls or other Python syntax in your scripts will be evaluatedeven when a script is not being run(potentially multiple times, depending on your configuration). This is important to understand, because it could have
unintended consequences.For example, let's say you have one script that generates a random UUID and another script that runs the first script
three times:# pyproject.toml[tool.python-dev-cli.settings]include=["uuid:uuid4 as uuid"][tool.python-dev-cli.scripts]uuid="echo {{ uuid() }}"uuids=["uuid","uuid","uuid"]When you rundev uuids, theuuid()function gets called three times, generating three unique IDs as expected.devuuids# affa5cf5-8d1d-43c6-806f-c561d91f7d05# d8dd276d-1cc8-4dcf-9e0d-6f3fcbdbcc22# ac5a0a59-4510-4609-9734-7c238652f59aWhen you rundev --help, the same thing happens, this time to generate the help page examples:dev-h# usage: dev [-h] [-d] {foobar} ...# Python developer CLI for running custom scripts defined in pyproject.toml## options:#   -h, --help            show this help message and exit#   -d, --debug           enable debug logging## available scripts:#   {uuid,uuids}#     uuid                ['echo 2afa5b31-159c-45e5-b7e3-ad935a48cef0']#     uuids               ['echo 2c7fe6e2-01a0-4cd9-90fe-50816a2ed316', 'echo 51ca85c8-b69b-4151-ab3f-5bf2a148a058', 'echo 6595e224-3e24-463c-9cc4-98c9bf0ecddb']Now, consider the following scripts that make API calls to a third-party service:# pyproject.toml[tool.python-dev-cli.settings]include=["json","requests"][tool.python-dev-cli.scripts]curl="curl https://cat-fact.herokuapp.com/facts/random?amount=1"req="echo {{ json.loads(requests.get('https://cat-fact.herokuapp.com/facts/random?amount=1').content).text }}"When you rundev curl, the API call is made as expected:devcurl# {"status":{"verified":true,"sentCount":1},"_id":"591f98783b90f7150a19c1c5","__v":0,"text":"Baking chocolate is the most dangerous chocolate to your cat.","source":"api","updatedAt":"2020-08-23T20:20:01.611Z","type":"cat","createdAt":"2018-04-17T20:20:02.627Z","deleted":false,"used":false,"user":"5a9ac18c7478810ea6c06381"}When you rundev req, the API call is also made as expected:devreq# Cats are amazing animals.When you rundev --help, the API call is made while parsing thereqscript template, even though the script is not
being run:dev--help# usage: dev [-h] [-d] {curl,req} ...# Python developer CLI for running custom scripts defined in pyproject.toml## options:#   -h, --help            show this help message and exit#   -d, --debug           enable debug logging## available scripts:#   {curl,req}#     curl                ['curl https://cat-fact.herokuapp.com/facts/random?amount=1']#     req                 ['echo When asked if her husband had any hobbies, Mary Todd Lincoln is said to have replied cats.']If that were an API call that had a side effect, such as creating a new record in a database, then that side effect
would happen every time thedev --helpcommand was run. This is probably not what you want.To prevent this from happening, you can disable template parsing in thedevCLI help page by setting theparse_helpvalue tofalse:# pyproject.toml[tool.python-dev-cli.settings]parse_help=falseHowever, a better solution in most cases would be to use thecurlcommand, or move the API call to a Python script
that can be run using thepython -msyntax:# pyproject.toml[tool.python-dev-cli.scripts]req="python -m scripts.req"# scripts/req.pyimportjsonimportrequestsdefmain()->str:returnjson.loads(requests.get('https://cat-fact.herokuapp.com/facts/random?amount=1').content).textif__name__=='__main__':output:str=main()print(output)LicenseThis open source project is licensed under the terms of theBSD 3-Clause License.ContributingContributions are welcome! Please readCONTRIBUTING.mdfor details.Code of ConductThis project has adopted theContributor Covenant. For more information, see theCode of Conductpage.