Storing the configuration in INI files

The INI file format has historical origins from early Windows OS. The module to parse these files is configparser. For additional details on the INI file, you can refer to this Wikipedia article for numerous useful links: http://en.wikipedia.org/wiki/INI_file.

An INI file has sections and properties within each section. Our sample main program has three sections: the table configuration, player configuration, and overall simulation data gathering. For this simulation, we will use an INI file that is similar to the following example:

; Default casino rules 
[table] 
    dealer= Hit17 
    split= NoResplitAces 
    decks= 6 
    limit= 50 
    payout= (3,2) 
 
; Player with SomeStrategy 
; Need to compare with OtherStrategy 
[player] 
    play= SomeStrategy 
    betting= Flat 
    max_rounds= 100 
    init_stake= 50 
 
[simulator] 
    samples= 100 
    outputfile= p2_c13_simulation.dat 

We've broken the parameters into three sections. Within each section, we've provided some named parameters that correspond to the class names and initialization values shown in our preceding model application initialization.

A single file can be parsed with the code shown in this example:

import configparser 
config = configparser.ConfigParser() 
config.read('blackjack.ini') 

Here, we've created an instance of the parser and provided the target configuration filename to that parser. The parser will read the file, locate the sections, and locate the individual properties within each section.

If we want to support multiple locations for files, we can use config.read(config_names). When we provide the list of filenames to ConfigParser.read(), it will read the files in a particular order. We want to provide the files from the most generic first to the most specific last. The generic configuration files that are part of the software installation will be parsed first to provide defaults. The user-specific configuration will be parsed later to override these defaults.

Once we've parsed the file, we need to make use of the various parameters and settings. Here's a function that constructs our objects based on a given configuration object created by parsing the configuration files. We'll break this into three parts; here's the part that builds the Table instance:

def main_ini(config: configparser.ConfigParser) -> None:
dealer_nm = config.get("table", "dealer", fallback="Hit17")
dealer_rule = {
"Hit17": Hit17(),
"Stand17": Stand17(),
}.get(dealer_nm, Hit17())
split_nm = config.get("table", "split", fallback="ReSplit")
split_rule = {
"ReSplit": ReSplit(),
"NoReSplit": NoReSplit(),
"NoReSplitAces": NoReSplitAces(),
}.get(split_nm, ReSplit())
decks = config.getint("table", "decks", fallback=6)
limit = config.getint("table", "limit", fallback=100)
payout = eval(
config.get("table", "payout", fallback="(3,2)")
)
table = Table(
decks=decks, limit=limit, dealer=dealer_rule,
split=split_rule, payout=payout
)

We've used properties from the [table] section of the INI file to select class names and provide initialization values. There are three broad kinds of cases here:

  • Mapping a string to a class name: We've used a mapping to look up an object based on a string class name. This was done to create dealer_rule and split_rule. If the pool of classes was subject to considerable change, we might move this mapping into a separate factory function. The .get() method of a dictionary includes a default object instance, for example, Hit17().
  • Getting a value that ConfigParser can parse for us: The class can directly handle values of built-in types such as str, int, float, and bool. Methods such as getint() handle these conversions. The class has a sophisticated mapping from a string to a Boolean, using a wide variety of common codes and synonyms for True and False.
  • Evaluating something that's not built-in: In the case of payout, we had a string value, '(3,2)', that is not a directly supported data type for ConfigParser. We have two choices to handle this. We can try and parse it ourselves, or we can insist that the value be a valid Python expression and make Python do this. In this case, we've used eval(). Some programmers call this a security problem. The next section deals with this.

Here's the second part of this example, which uses properties from the [player] section of the INI file to select classes and argument values:

    player_nm = config.get(
"player", "play", fallback="SomeStrategy")
player_rule = {
"SomeStrategy": SomeStrategy(),
"AnotherStrategy": AnotherStrategy()
}.get(player_nm, SomeStrategy())
bet_nm = config.get("player", "betting", fallback="Flat")
betting_rule = {
"Flat": Flat(),
"Martingale": Martingale(),
"OneThreeTwoSix": OneThreeTwoSix()
}.get(bet_nm, Flat())
max_rounds = config.getint("player", "max_rounds", fallback=100)
init_stake = config.getint("player", "init_stake", fallback=50)
player = Player(
play=player_rule,
betting=betting_rule,
max_rounds=max_rounds,
init_stake=init_stake
)

This uses string-to-class mapping as well as built-in data types. It initializes two strategy objects and then creates Player from those two strategies, plus two integer configuration values.

Here's the final part; this creates the overall simulator:

outputfile = config.get(
"simulator", "outputfile", fallback="blackjack.csv")
samples = config.getint("simulator", "samples", fallback=100)
simulator = Simulate(table, player, samples=samples)
with Path(outputfile).open("w", newline="") as results:
wtr = csv.writer(results)
wtr.writerows(simulator)

We've used two parameters from the [simulator] section that are outside the narrow confines of object creation. The outputfile property is used to name a file; the samples property is provided as an argument to a method function.

The next section demonstrates how to handle more literals through the eval() variants.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset