I’m available for freelance work. Let’s talk »

Set_env.py

Sunday 21 July 2019

A good practice when writing complicated software is to put in lots of debugging code. This might be extra logging, or special modes that tweak the behavior to be more understandable, or switches to turn off some aspect of your test suite so you can focus on the part you care about at the moment.

But how do you control that debugging code? Where are the on/off switches? You don’t want to clutter your real UI with controls. A convenient option is environment variables: you can access them simply in the code, your shell has ways to turn them on and off at a variety of scopes, and they are invisible to your users.

Though if they are invisible to your users, they are also invisible to you! How do you remember what exotic options you’ve coded into your program, and how do you easily see what is set, and change what is set?

I’ve been using environment variables like this in coverage.py for years, but only recently made it easier to work with them.

To do that, I wrote set_env.py. It scans a tree of files for special comments describing environment variables, then shows you the values of those variables. You can type quick commands to change the values, and when the program is done, it updates your environment. It’s not a masterpiece of engineering, but it works for me.

As an example, this line appears in coverage.py:

# $set_env.py: COVERAGE_NO_PYTRACER - Don't run the tests under the Python tracer.

This line is found by set_env.py, so it knows that COVERAGE_NO_PYTRACER is one of the environment variables it should fiddle with.

When I run set_env.py in the coverage.py tree, I get something like this:

$ set_env
Read 298 files
 1:              COVERAGE_AST_DUMP                  Dump the AST nodes when parsing code.
 2:               COVERAGE_CONTEXT                  Set to 'test_function' for who-tests-what
 3:                 COVERAGE_DEBUG                  Options for --debug.
 4:           COVERAGE_DEBUG_CALLS                  Lots and lots of output about calls to Coverage.
 5:                COVERAGE_ENV_ID                  Use environment-specific test directories.
 6:              COVERAGE_KEEP_TMP                  Keep the temp directories made by tests.
 7:          COVERAGE_NO_CONTRACTS                  Disable PyContracts to simplify stack traces.
 8:            COVERAGE_NO_CTRACER                  Don't run the tests under the C tracer.
 9:           COVERAGE_NO_PYTRACER = '1'            Don't run the tests under the Python tracer.
10:               COVERAGE_PROFILE                  Set to use ox_profile.
11:            COVERAGE_TRACK_ARCS                  Trace every arc added while parsing code.
12:                 PYTEST_ADDOPTS                  Extra arguments to pytest.

(# [value] | x # ... | ? | q)>

All of the files were scanned, and 12 environment variables found. We can see that COVERAGE_NO_PYTRACER has the value “1”, and none of the others are in the environment. At the prompt, if I type “4”, then COVERAGE_DEBUG_CALLS (line 4) will be toggled to “1”. Type “4” again, and it is cleared. Typing “4 yes please” will set it to “yes please”, but often I just need something or nothing, so toggling “1” as the value works.

One bit of complexity here is that a program you run in your shell can’t change environment variables for subsequent programs, which is exactly what we need. So “set_env” is actually a shell alias:

alias set_env='$(set_env.py $(git ls-files))'

This runs set_env.py against all of the files checked-in to git, and then executes whatever set_env.py outputs. Naturally, set_env.py outputs shell commands to set environment variables. If ls-files produces too much output, you can use globs there also, so “**/*.py” might be useful.

Like I said, it’s not a masterpiece, but it works for me. If there are other tools out there that do similar things, I’d like to hear about them.

Comments

[gravatar]
I usually check in some kind of configuration file, for example an env.dist that users can fill in and rename to .env (.env is supported by pipenv and some other tools). So I solve the discovery problem for them.

.env is sort of a standard, so that makes it a better choice IMHO, though it is already supported differently in different tools (`export` prefix or no, inconsistent quoting).

I can see a tool like yours be useful for me the maintainer though, to keep that file up to date.
[gravatar]

@Remram: nearly five years later, I felt like I needed a way to create .env files, so I added it as a command to set_env.py today.

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.