Skip to content

Commit c8293c1

Browse files
authored
add external configurability (#331)
* start integrating settings by config file * tests pass (coverage isn't high enough yet) * add basic docs re: config loading * have code access new locations withing _config; stop accessing garak._config indirectly * clear up references to _config.transient.reportfile * cli_args now stores params directly specified on CLI only (plus command requests, for convenience) and then overrides args; better logging of cli/arg processing * search for plugin spec in config, not in args * amend reporting write call * plugin planning now uses spec & spec parsing * uuid only generated once * configure logging before it's used in _config * re-enable CLI short form options & CLI param typing * reinstate unit on completion timer * add list_config command; avoid configs getting all merged into one subconfig object; correct config var locations in cli * moved model_name, model_type to _config.plugins * track config param name locations in _config.py * add config testing, include CLI option tests * implement config file priority; remove dynaconf (doesn't support hierarchical merging) * runs can be launched from config file alone * support loading generator, probe options from yaml * add stub failing tests for config * reconfig and test probe, generator CLI cfg file loading & structure * clean up cli param setting * set up plugin config-on-load; start removing generator/plugin_options params; do config merging of CLI options * add probespec parsing tests * test generator parameter cli loading * test generator config loading * check that cli probe options override yaml * check that cli probe options override yaml * check that cli probe options override yaml * check CLI generations param overrides run param * test that site cfg overrides core cfg * config tests passing
1 parent ce7e6e3 commit c8293c1

35 files changed

+1089
-395
lines changed

docs/source/basic.rst

+16-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@ garak
44
garak._config
55
-------------
66

7-
This module holds runtime config values.
7+
This module holds config values.
8+
9+
These are broken into the following major categories:
10+
11+
* system: options that don't affect the security assessment
12+
* run: options that describe how a garak run will be conducted
13+
* plugins: config for plugins (generators, probes, detectors, buffs)
14+
* transient: internal values local to a single garak execution
15+
16+
Config values are loaded in the following priority (lowest-first):
17+
18+
* Plugin defaults in the code
19+
* Core config: from ``garak/resources/garak.core.yaml``; not to be overridden
20+
* Site config: from ``garak/garak.site.yaml``
21+
* Runtime config: from an optional config file specified manually, via e.g. CLI parameter
22+
* Command-line options
823

924
Code
1025
^^^^

garak/_config.py

+147-11
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,152 @@
11
#!/usr/bin/env python3
22
"""garak global config"""
33

4+
# SPDX-FileCopyrightText: Portions Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
# plugin code < base config < site config < run config < cli params
8+
9+
# logging should be set up before config is loaded
10+
11+
from dataclasses import dataclass
12+
import logging
13+
import os
414
import pathlib
15+
from typing import List
16+
import yaml
17+
18+
version = -1 # eh why this is here? hm. who references it
19+
20+
system_params = (
21+
"verbose report_prefix narrow_output parallel_requests parallel_attempts".split()
22+
)
23+
run_params = "seed deprefix eval_threshold generations".split()
24+
plugins_params = "model_type model_name extended_detectors".split()
25+
26+
27+
@dataclass
28+
class GarakSubConfig:
29+
pass
30+
31+
32+
@dataclass
33+
class TransientConfig(GarakSubConfig):
34+
"""Object to hold transient global config items not set externally"""
35+
36+
report_filename = None
37+
reportfile = None
38+
hitlogfile = None
39+
args = None # only access this when determining what was passed on CLI
40+
run_id = None
41+
basedir = pathlib.Path(__file__).parents[0]
42+
starttime = None
43+
starttime_iso = None
44+
45+
46+
transient = TransientConfig()
47+
48+
system = GarakSubConfig()
49+
run = GarakSubConfig()
50+
plugins = GarakSubConfig()
51+
plugins.probes = {}
52+
plugins.generators = {}
53+
plugins.detectors = {}
54+
plugins.buffs = {}
55+
56+
# this is so popular, let's set a default. what other defaults are worth setting? what's the policy?
57+
run.seed = None
58+
59+
# placeholder
60+
# generator, probe, detector, buff = {}, {}, {}, {}
61+
62+
63+
def _set_settings(config_obj, settings_obj: dict):
64+
for k, v in settings_obj.items():
65+
setattr(config_obj, k, v)
66+
return config_obj
67+
68+
69+
def _combine_into(d: dict, combined: dict) -> None:
70+
for k, v in d.items():
71+
if isinstance(v, dict):
72+
_combine_into(v, combined.setdefault(k, {}))
73+
else:
74+
combined[k] = v
75+
return combined
76+
77+
78+
def _load_yaml_config(settings_filenames) -> dict:
79+
config = {}
80+
for settings_filename in settings_filenames:
81+
settings = yaml.safe_load(open(settings_filename, encoding="utf-8"))
82+
if settings is not None:
83+
config = _combine_into(settings, config)
84+
return config
85+
86+
87+
def _store_config(settings_files) -> None:
88+
global system, run, plugins
89+
settings = _load_yaml_config(settings_files)
90+
system = _set_settings(system, settings["system"])
91+
run = _set_settings(run, settings["run"])
92+
plugins = _set_settings(plugins, settings["plugins"])
93+
94+
95+
def load_base_config() -> None:
96+
global system
97+
settings_files = [str(transient.basedir / "resources/garak.core.yaml")]
98+
logging.debug("Loading configs from: %s", ",".join(settings_files))
99+
_store_config(settings_files=settings_files)
100+
101+
102+
def load_config(
103+
site_config_filename="garak.site.yaml", run_config_filename=None
104+
) -> None:
105+
# would be good to bubble up things from run_config, e.g. generator, probe(s), detector(s)
106+
# and then not have cli be upset when these are not given as cli params
107+
global system, run, plugins
108+
109+
settings_files = [str(transient.basedir / "resources/garak.core.yaml")]
110+
111+
fq_site_config_filename = str(transient.basedir / site_config_filename)
112+
if os.path.isfile(fq_site_config_filename):
113+
settings_files.append(fq_site_config_filename)
114+
else:
115+
# warning, not error, because this one has a default value
116+
logging.warning("site config not found: %s", fq_site_config_filename)
117+
118+
if run_config_filename is not None:
119+
fq_run_config_filename = str(transient.basedir / run_config_filename)
120+
if os.path.isfile(fq_run_config_filename):
121+
settings_files.append(fq_run_config_filename)
122+
else:
123+
logging.error("run config not found: %s", fq_run_config_filename)
124+
125+
logging.debug("Loading configs from: %s", ",".join(settings_files))
126+
_store_config(settings_files=settings_files)
127+
128+
129+
def parse_plugin_spec(spec: str, category: str) -> List[str]:
130+
from garak._plugins import enumerate_plugins
131+
132+
if spec is None or spec.lower() in ("", "auto", "none"):
133+
return []
134+
if spec.lower() == "all":
135+
plugin_names = [
136+
name
137+
for name, active in enumerate_plugins(category=category)
138+
if active is True
139+
]
140+
else:
141+
plugin_names = []
142+
for clause in spec.split(","):
143+
if clause.count(".") < 1:
144+
plugin_names += [
145+
p
146+
for p, a in enumerate_plugins(category=category)
147+
if p.startswith(f"{category}.{clause}.") and a is True
148+
]
149+
else:
150+
plugin_names += [f"{category}.{clause}"] # spec parsing
5151

6-
basedir = pathlib.Path(__file__).parents[0]
7-
8-
args = None
9-
reportfile = None
10-
hitlogfile = None
11-
seed = None
12-
run_id = None
13-
generator_model = None
14-
generator_name = None
15-
probe_options = {}
16-
generator_options = {}
152+
return plugin_names

garak/_plugins.py

+30-14
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import os
1212
from typing import List
1313

14-
import garak._config
14+
from garak import _config
1515

1616

17-
def enumerate_plugins(category: str = "probes", skip_base_classes=True) -> List[str]:
17+
def enumerate_plugins(
18+
category: str = "probes", skip_base_classes=True
19+
) -> List[tuple[str, bool]]:
1820
"""A function for listing all modules & plugins of the specified kind.
1921
2022
garak's plugins are organised into four packages - probes, detectors, generators
@@ -54,7 +56,7 @@ def enumerate_plugins(category: str = "probes", skip_base_classes=True) -> List[
5456

5557
plugin_class_names = []
5658

57-
for module_filename in sorted(os.listdir(garak._config.basedir / category)):
59+
for module_filename in sorted(os.listdir(_config.transient.basedir / category)):
5860
if not module_filename.endswith(".py"):
5961
continue
6062
if module_filename.startswith("__"):
@@ -82,7 +84,17 @@ def enumerate_plugins(category: str = "probes", skip_base_classes=True) -> List[
8284
return plugin_class_names
8385

8486

85-
def load_plugin(path, break_on_fail=True):
87+
def configure_plugin(plugin_path: str, plugin: object) -> object:
88+
category, module_name, plugin_class_name = plugin_path.split(".")
89+
plugin_name = f"{module_name}.{plugin_class_name}"
90+
plugin_type_config = getattr(_config.plugins, category)
91+
if plugin_name in plugin_type_config:
92+
for k, v in plugin_type_config[plugin_name].items():
93+
setattr(plugin, k, v)
94+
return plugin
95+
96+
97+
def load_plugin(path, break_on_fail=True) -> object:
8698
"""load_plugin takes a path to a plugin class, and attempts to load that class.
8799
If successful, it returns an instance of that class.
88100
@@ -94,41 +106,45 @@ def load_plugin(path, break_on_fail=True):
94106
"""
95107
try:
96108
category, module_name, plugin_class_name = path.split(".")
97-
except ValueError:
109+
except ValueError as ve:
98110
if break_on_fail:
99111
raise ValueError(
100112
f'Expected plugin name in format category.module_name.class_name, got "{path}"'
101-
)
113+
) from ve
102114
else:
103115
return False
104116
module_path = f"garak.{category}.{module_name}"
105117
try:
106118
mod = importlib.import_module(module_path)
107-
except:
108-
logging.warning(f"Exception failed import of {module_path}")
119+
except Exception as e:
120+
logging.warning("Exception failed import of %s", module_path)
109121
if break_on_fail:
110-
raise ValueError("Didn't successfully import " + module_name)
122+
raise ValueError("Didn't successfully import " + module_name) from e
111123
else:
112124
return False
113125

114126
try:
115127
plugin_instance = getattr(mod, plugin_class_name)()
116-
except AttributeError:
128+
except AttributeError as ae:
117129
logging.warning(
118-
f"Exception failed instantiation of {module_path}.{plugin_class_name}"
130+
"Exception failed instantiation of %s.%s", module_path, plugin_class_name
119131
)
120132
if break_on_fail:
121133
raise ValueError(
122134
f"Plugin {plugin_class_name} not found in {category}.{module_name}"
123-
)
135+
) from ae
124136
else:
125137
return False
126138
except Exception as e:
127139
# print("error in: module", mod.__name__, "class", plugin_class_name)
128-
# logging.warning(f"error in: module {mod} class {plugin_class_name}")
140+
logging.warning(
141+
"error instantiating module %s class %s", str(mod), plugin_class_name
142+
)
129143
if break_on_fail:
130-
raise Exception(e)
144+
raise Exception(e) from e
131145
else:
132146
return False
133147

148+
plugin_instance = configure_plugin(path, plugin_instance)
149+
134150
return plugin_instance

0 commit comments

Comments
 (0)