Quick Start
Installation
The best way to install ActiveConfigProgramOptions is to install it via pip:
$ python3 -m pip install ActiveConfigProgramOptions
It can also be cloned from Gitlab and installed locally:
$ git clone https://gitlab.com/semantik-software/code/python/ActiveConfigProgramOptions.git
$ cd ActiveConfigProgramOptions
$ python3 -m pip install .
Running the Examples
Once installed, you can go to the Examples directory and run the examples:
$ cd examples
$ python3 ActiveConfigProgramOptions-example-01.py
...
Using ActiveConfigProgramOptions in Code
The first step is to include the ActiveConfigProgramOptions class:
1#!/usr/bin/env python3
2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
3from pathlib import Path
4import activeconfigprogramoptions
5
Examples
Example 1 - Command Line
In ActiveConfigProgramOptions-example-01 we have a .ini file that
demostrates utilization of the use operation that is inherited
from ActiveConfigParser
ActiveConfigParser.
In this example we are going to demonstrate how the opt-set operations
in our .ini file can be used to generate a custom bash command with
customizable argument sets added.
In this case, we will process the [MY_LS_COMMAND] section to generate
a bash command that would generate a directory listing in list form by
reverse time order of last modified with a custom timestamp format.
While this example is quite simple we can see how a complex environment in a DevOps setting might use this to bundle “common” operations to reduce the amount of copying and pasting that is used.
First, we need a .ini file:
1#
2# ActiveConfigProgramOptions-example-01.ini
3#
4[LS_COMMAND]
5opt-set ls
6
7[LS_LIST_TIME_REVERSED]
8opt-set "-l -t -r"
9
10[LS_CUSTOM_TIME_STYLE]
11opt-set --time-style : "+%%Y-%%m-%%d %%H:%%M:%%S"
12
13[MY_LS_COMMAND]
14use LS_COMMAND
15use LS_LIST_TIME_REVERSED
16use LS_CUSTOM_TIME_STYLE
This ini file demonstrates how we can use sections to set up a command that could be used to generate different options.
First, the LS_COMMAND section defines the executable application option that is to be called. In this case it’s just ls to get a directory listing.
The next two sections, LS_LIST_TIME_REVERSED and LS_CUSTOM_TIME_STYLE provides the command line options create a directory listing to sort the most recently modified files to the end and to make a custom date/time format, respectively.
Finally the last section MY_LS_COMMAND would be the section that we use to generate a full directory listing command that will combine the executable with the sorting and timestamp format options applied.
The following code shows how our application can use ActiveConfigProgramOptions to generate the full command line from this toy problem:
1#!/usr/bin/env python3
2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
3from pathlib import Path
4import activeconfigprogramoptions
5
6# print a banner
7print(80 * "-")
8print(f"- {Path(__file__).name}")
9print(80 * "-")
10
11filename = "ActiveConfigProgramOptions-example-01.ini"
12
13# create a ActiveConfigProgramOptions object using the .ini file
14popts = activeconfigprogramoptions.ActiveConfigProgramOptions(filename)
15
16# Parse a section that generates a LS command
17section = "MY_LS_COMMAND"
18popts.parse_section(section)
19
20# Extract a list containing the options
21bash_options = popts.gen_option_list(section, generator="bash")
22
23# Print the option list
24print(" ".join(bash_options))
We see in this code the call to gen_option_list() parses the
MY_LS_COMMAND section and generates a list containing the
arguments for our command. We can join these together to create a
single string containing our command or send the list directly
to subprocess.run to execute the command.
In our example, we’re just printing out the results of our generator:
1--------------------------------------------------------------------------------
2- ActiveConfigProgramOptions-example-01.py
3--------------------------------------------------------------------------------
4ls -l -t -r --time-style="+%Y-%m-%d %H:%M:%S"
Example 2 - CMake Use Case
In this example we show use of ActiveConfigProgramOptionsCMake which lets us generate a .ini file that provides some flexibility in what CMake configuration commands we could generate based on the contents of our .ini file with re-use of common sections and arguments.
The configuration file is:
1#
2# ActiveConfigProgramOptions-example-02.ini
3#
4[CMAKE_COMMAND]
5opt-set cmake
6
7[CMAKE_GENERATOR_NINJA]
8opt-set -G : Ninja
9
10[MYPROJ_OPTIONS]
11opt-set-cmake-var MYPROJ_CXX_FLAGS STRING : "-O0 -fopenmp"
12opt-set-cmake-var MYPROJ_ENABLE_OPTION_A BOOL FORCE : ON
13opt-set-cmake-var MYPROJ_ENABLE_OPTION_B BOOL : ON
14
15[MYPROJ_SOURCE_DIR]
16opt-set /path/to/source/dir
17
18[MYPROJ_CONFIGURATION_NINJA]
19use CMAKE_COMMAND
20use CMAKE_GENERATOR_NINJA
21use MYPROJ_OPTIONS
22use MYPROJ_SOURCE_DIR
In this file we have the base CMake command in CMAKE_COMMAND followed by an optional argument to enable the ninja generator in CMAKE_GENERATOR_NINJA.
CMake flags are enabled in MYPROG_OPTIONS. These follow the ActiveConfigParser style of:
opt-set-cmake-var VARNAME [TYPE] [FORCE] [PARENT_SCOPE]: VALUE
and in our file we’re setting cmake flags: MYPROJ_CXX_FLAGS,
MYPROJ_ENABLE_OPTION_A, and MYPROJ_ENABLE_OPTION_B.
For explanation of what the optional values mean, please check
CMake’s documentation on the
set
command and how it relates to the -D command line option for
CMake.
We note that there are some differences between how CMake treates
an argument sent at the command line using a -D option and how
the set() function inside a .cmake file behave. We will get into
this in Example 3.
We can process this in our sample python script:
1#!/usr/bin/env python3
2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
3from pathlib import Path
4import activeconfigprogramoptions
5
6
7
8def print_separator(label):
9 print("")
10 print(f"{label}")
11 print("-" * len(label))
12 return
13
14
15
16print(80 * "-")
17print(f"- {Path(__file__).name}")
18print(80 * "-")
19
20filename = "ActiveConfigProgramOptions-example-02.ini"
21popts = activeconfigprogramoptions.ActiveConfigProgramOptionsCMake(filename)
22
23section = "MYPROJ_CONFIGURATION_NINJA"
24popts.parse_section(section)
25
26# Generate BASH output
27print_separator("Generate Bash Output")
28bash_options = popts.gen_option_list(section, generator="bash")
29print(" \\\n ".join(bash_options))
30
31# Generate a CMake Fragment
32print_separator("Generate CMake Fragment")
33cmake_options = popts.gen_option_list(section, generator="cmake_fragment")
34print("\n".join(cmake_options))
35
36print("\nDone")
This code extends the gen_option_list() method that is found in
the base class ActiveConfigProgramOptions to add a second generator type,
cmake_fragment. We show the output from each in this code:
1--------------------------------------------------------------------------------
2- ActiveConfigProgramOptions-example-02.py
3--------------------------------------------------------------------------------
4
5Generate Bash Output
6--------------------
7cmake \
8 -G=Ninja \
9 -DMYPROJ_CXX_FLAGS:STRING="-O0 -fopenmp" \
10 -DMYPROJ_ENABLE_OPTION_A:BOOL=ON \
11 -DMYPROJ_ENABLE_OPTION_B:BOOL=ON \
12 /path/to/source/dir
13
14Generate CMake Fragment
15-----------------------
16set(MYPROJ_CXX_FLAGS "-O0 -fopenmp" CACHE STRING "from .ini configuration")
17set(MYPROJ_ENABLE_OPTION_A ON CACHE BOOL "from .ini configuration" FORCE)
18set(MYPROJ_ENABLE_OPTION_B ON CACHE BOOL "from .ini configuration")
19
20Done
The general idea behind creating multiple generators is to enable this tool to be used to generate either a command line outptut that could be run directly or appended to a script that is constructed from some template or generate a .cmake fragment file that can be added to a CMake script.
As mentioned earlier, the goal of this tool is that the generated behaviour from CMake is the same for both the command-line method and for .cmake fragment files. Our next example provides additional details on this:
Example 3 - CMake Options and FORCE
This example is a bit more complicated and gets into some of the
differences in how CMake treats a command line variable being
set via a -D option versus a set() operation within a
CMakeLists.txt file.
The script prints out a notice explaining the nuance but the general
idea is that CMake treats options provided by a -D option at the
command line as though they are CACHE variables with the FORCE
option enabled. This is designed to allow command-line parameters to
generally take precedence over what a CMakeLists.txt file might set
as though it’s a user-override, but if the same option is provided
multiple times on a command line then the last one wins.
This is different from how it works inside a .cmake file where the first time a CACHE varaible is set it is considered immutable unless the FORCE option is given. This effectively means that a CACHE var inside .cmake files are treated as first one wins unless the FORCE option is provided.
This can have a subtle yet profound effect on how we must process our
opt-set-cmake-var operations within a .ini file if our goal is to
ensure that the resulting CMakeCache.txt file generated by a CMake
run would be the same for both bash and cmake fragment generators.
In this case, in order to have a variable set by the bash generator
it must be a CACHE variable – which can be accomplished by either
adding a TYPE or a FORCE option.
We will note here that if FORCE is given without a TYPE then we
use the default type of STRING.
If the same CMake variable is being assigned, such as in a case where
we have a section that is updating a flag, then the FORCE option must
be present on the second and all subsequent occurrences of opt-set-cmake-var
or the bash generator will skip over that assignment since a non-forced
set() operation in CMake would not overwrite an existing cache var.
This situation can occur frequently if our .ini file(s) are structured to
have some common configuration option set and then a specialization which
updates one of the arguments. One example of this kind of situation might
be where we have a specialization that adds OpenMP and we would want to add
the -fopenmp flags to our linker flags.
1#
2# ActiveConfigProgramOptions-example-03.ini
3#
4[TEST_VAR_EXPANSION_COMMON]
5opt-set-cmake-var CMAKE_CXX_FLAGS STRING : "${LDFLAGS|ENV} -foo"
6# note: STRING type implies this is a CACHE var.
7
8
9[TEST_VAR_EXPANSION_UPDATE_01]
10opt-set cmake
11use TEST_VAR_EXPANSION_COMMON
12
13opt-set-cmake-var CMAKE_CXX_FLAGS STRING: "${CMAKE_CXX_FLAGS|CMAKE} -bar"
14# This will be skipped by the BASH generator without a FORCE option added
15# because .cmake files treate CACHE vars as immutable and won't overwrite
16# the value unless forced, but all bash `-D` options are CACHE and FORCE
17# so without FORCE the only way we can ensure the bash and cmake_fragment
18# generators create a result that generates the same CMakeCache.txt file
19# upon generation is to remove this option from the bash generated result.
1#!/usr/bin/env python3
2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
3from pathlib import Path
4from pprint import pprint
5import activeconfigprogramoptions
6
7
8
9def print_separator(label):
10 print("")
11 print(f"{label}")
12 print("-" * len(label))
13 return
14
15
16
17filename = "ActiveConfigProgramOptions-example-03.ini"
18print(f"filename: {filename}")
19
20section_name = "TEST_VAR_EXPANSION_UPDATE_01"
21print(f"section_name = {section_name}")
22
23parser = activeconfigprogramoptions.ActiveConfigProgramOptionsCMake(filename=filename)
24parser.debug_level = 0
25parser.exception_control_level = 4
26parser.exception_control_compact_warnings = True
27
28data = parser.activeconfigparserdata[section_name]
29print_separator(f"parser.activeconfigparserdata[{section_name}]")
30pprint(data, width=120)
31
32print_separator("Show parser.options")
33pprint(parser.options, width=200, sort_dicts=False)
34
35print_separator("Bash Output")
36print("Note: The _second_ assignment to `CMAKE_CXX_FLAGS` is skipped by a BASH generator")
37print(" without a `FORCE` option since by definition all CMake `-D` options on a ")
38print(" BASH command line are both CACHE and FORCE. Within a CMake source fragment")
39print(" changing an existing CACHE var requires a FORCE option to be set so we should")
40print(" skip the second assignment to maintain consistency between the bash and cmake")
41print(" fragment generators with respect to the CMakeCache.txt file that would be")
42print(" generated.")
43print(" The `WARNING` message below is terse since it's in compact form -- disable")
44print(" the `exception_control_compact_warnings` flag to get the full warning message.")
45print("")
46option_list = parser.gen_option_list(section_name, generator="bash")
47print("")
48print(" \\\n ".join(option_list))
49
50print_separator("CMake Fragment")
51option_list = parser.gen_option_list(section_name, generator="cmake_fragment")
52if len(option_list) > 0:
53 print("\n".join(option_list))
54else:
55 print("-")
56print("")
We will see in the generated output that the assignment to CMAKE_CXX_FLAGS in section TEST_VAR_EXPANSION_UPDATE_01 is omitted from the bash output because it is not FORCEd:
1filename: ActiveConfigProgramOptions-example-03.ini
2section_name = TEST_VAR_EXPANSION_UPDATE_01
3
4parser.activeconfigparserdata[TEST_VAR_EXPANSION_UPDATE_01]
5-----------------------------------------------------------
6{}
7
8Show parser.options
9-------------------
10{'TEST_VAR_EXPANSION_UPDATE_01': [{'type': ['opt_set'], 'value': None, 'params': ['cmake']},
11 {'type': ['opt_set_cmake_var'], 'value': '${LDFLAGS|ENV} -foo', 'params': ['CMAKE_CXX_FLAGS', 'STRING']},
12 {'type': ['opt_set_cmake_var'], 'value': '${CMAKE_CXX_FLAGS|CMAKE} -bar', 'params': ['CMAKE_CXX_FLAGS', 'STRING']}]}
13
14Bash Output
15-----------
16Note: The _second_ assignment to `CMAKE_CXX_FLAGS` is skipped by a BASH generator
17 without a `FORCE` option since by definition all CMake `-D` options on a
18 BASH command line are both CACHE and FORCE. Within a CMake source fragment
19 changing an existing CACHE var requires a FORCE option to be set so we should
20 skip the second assignment to maintain consistency between the bash and cmake
21 fragment generators with respect to the CMakeCache.txt file that would be
22 generated.
23 The `WARNING` message below is terse since it's in compact form -- disable
24 the `exception_control_compact_warnings` flag to get the full warning message.
25
26!! EXCEPTION SKIPPED (WARNING : ValueError) @ File "/builds/semantik-software/code/python/ActiveConfigProgramOptions/venv-examples/lib/python3.14/site-packages/activeconfigprogramoptions/ActiveConfigProgramOptionsCMake.py", line 306, in _program_option_handler_opt_set_cmake_var_bash
27
28cmake \
29 -DCMAKE_CXX_FLAGS:STRING="${LDFLAGS} -foo"
30
31CMake Fragment
32--------------
33set(CMAKE_CXX_FLAGS "$ENV{LDFLAGS} -foo" CACHE STRING "from .ini configuration")
34set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -bar" CACHE STRING "from .ini configuration")
We will note here that the CMake fragment generator will still generate
all of the commands. In this case the second set() command would be ignored
by CMake since it’s not FORCEd but the main take-away here is that the
bash generator omitted the second -D operation since that would be
a FORCE operation by default which is not representative of what was
specified in the .ini file.
Additional Examples
See the Examples section for additional usage examples and the full code.