From d6d8e343856cc594fa1854a2ec56066997ebb7dc Mon Sep 17 00:00:00 2001 From: Aaron R Hall Date: Tue, 24 Dec 2024 12:33:18 -0700 Subject: [PATCH] Make beeflow config new non-interactive by default --- beeflow/common/config_driver.py | 49 ++++++++++++++--------- beeflow/tests/test_config_driver.py | 61 ++++++++++++++++++++++++++++- docs/sphinx/installation.rst | 13 +++--- 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/beeflow/common/config_driver.py b/beeflow/common/config_driver.py index 5735e3c0..8e23e515 100644 --- a/beeflow/common/config_driver.py +++ b/beeflow/common/config_driver.py @@ -105,7 +105,7 @@ def init(cls, userconfig=None, **_kwargs): break except FileNotFoundError: print("Configuration file is missing! Generating new config file.") - new(USERCONFIG_FILE) + new(USERCONFIG_FILE, interactive=False) # remove default keys from the other sections cls.CONFIG = config_utils.filter_and_validate(config, VALIDATOR) @@ -256,7 +256,7 @@ def filepath_completion_input(*pargs, **kwargs): default=7777, validator=int) VALIDATOR.option('DEFAULT', 'workload_scheduler', choices=('Slurm', 'LSF', 'Flux', 'Simple'), - info='backend workload scheduler to interact with ') + default='Slurm', info='backend workload scheduler to interact with ') VALIDATOR.option('DEFAULT', 'delete_completed_workflow_dirs', validator=validation.bool_, default=True, info='delete workflow directory for completed jobs') @@ -295,11 +295,11 @@ def filepath_completion_input(*pargs, **kwargs): # General job requirements VALIDATOR.section('job', info='General job requirements.') VALIDATOR.option('job', 'default_account', validator=lambda val: val.strip(), - info='default account to launch jobs with (leave blank if none)') + default='', info='default account to launch jobs with (leave blank if none)') VALIDATOR.option('job', 'default_time_limit', validator=validation.time_limit, - info='default account time limit (leave blank if none)') + default='', info='default account time limit (leave blank if none)') VALIDATOR.option('job', 'default_partition', validator=lambda val: val.strip(), - info='default partition to run jobs on (leave blank if none)') + default='', info='default partition to run jobs on (leave blank if none)') def validate_chrun_opts(opts): @@ -373,7 +373,7 @@ def __init__(self, fname, validator): self.validator = validator self.sections = {} - def choose_values(self): + def choose_values(self, interactive=False, flux=False): """Choose configuration values based on user input.""" dirname = os.path.dirname(self.fname) if dirname: @@ -401,10 +401,12 @@ def choose_values(self): print() printed = True + this_default = option.default + if flux is True and opt_name == 'workload_scheduler': + this_default = "Flux" # Check for a default value - if option.default is not None: - default = option.default - value = option.validate(default) + if not interactive and this_default is not None: + value = option.validate(this_default) print(f'Setting option "{opt_name}" to default value "{value}".') print() self.sections[sec_name][opt_name] = value @@ -427,7 +429,8 @@ def _input_loop(self, opt_name, option): print_wrap(f'{opt_name} - {option.info}') if option.choices is not None: print(f'(allowed values: {",".join(option.choices)})') - value = input_fn(f'{opt_name}: ') + value = input_fn(f'Enter selection for {opt_name} or\n' + + f'leave blank for default ({option.default}): ') or option.default # Validate the input try: option.validate(value) @@ -436,7 +439,7 @@ def _input_loop(self, opt_name, option): value = None return value - def save(self): + def save(self, interactive=False): """Save the config to a file.""" print() print('The following configuration options were chosen:') @@ -446,10 +449,11 @@ def save(self): for opt_name in section: print(f'{opt_name} = {section[opt_name]}') print() - ans = input('Would you like to save this config? [y/n] ') - if ans.lower() != 'y': - print('Quitting without saving') - return + if interactive: + ans = input('Would you like to save this config? [y/n] ') + if ans.lower() != 'y': + print('Quitting without saving') + return config_utils.write_config(self.fname, self.sections) print(70 * '#') print('Before running BEE, check defaults in the configuration file:', @@ -568,12 +572,21 @@ def info(): @app.command() def new(path: str = typer.Argument(default=USERCONFIG_FILE, - help='Path to new config file')): + help='Path to new config file'), + interactive: bool = typer.Option(False, '--interactive', '-i', + help='Whether or not to be prompted' + + ' during config generation'), + flux: bool = typer.Option(False, '--flux', '-f', + help='Changes default scheduler to Flux')): """Create a new config file.""" if os.path.exists(path): - if check_yes(f'Path "{path}" already exists.\nWould you like to save a copy of it?'): + if not interactive or check_yes(f'Path "{path}" already exists.\n' + + 'Would you like to save a copy of it?'): config_utils.backup(path) - ConfigGenerator(path, VALIDATOR).choose_values().save() + ConfigGenerator(path, VALIDATOR).choose_values( + flux=flux, + interactive=interactive + ).save(interactive=interactive) @app.command() diff --git a/beeflow/tests/test_config_driver.py b/beeflow/tests/test_config_driver.py index bfbc199c..d44773af 100644 --- a/beeflow/tests/test_config_driver.py +++ b/beeflow/tests/test_config_driver.py @@ -1,7 +1,9 @@ """Unit tests for the config driver.""" +import os import pytest -from beeflow.common.config_driver import AlterConfig +from beeflow.common.config_driver import AlterConfig, ConfigGenerator, new +from beeflow.common.config_validator import ConfigValidator # AlterConfig tests @@ -103,3 +105,60 @@ def test_change_value_multiple_times(mocker): alter_config.change_value("DEFAULT", "bee_workdir", "/path/two") assert alter_config.config["DEFAULT"]["bee_workdir"] == "/path/two" assert alter_config.changes == {"DEFAULT": {"bee_workdir": "/path/two"}} + + +@pytest.mark.parametrize("interactive, flux, expected", + [ + (True, True, 'input'), + (True, False, 'input'), + (False, True, 'Flux'), + (False, False, 'default'), + ], + ) +def test_choose_values(interactive, flux, expected): + """Test running choose_values with different flags.""" + validator = ConfigValidator('') + validator.section('sec', info='') + validator.option('sec', 'workload_scheduler', info='', default='default', + validator=lambda _: _, input_fn=lambda _: 'input') + config_generator = ConfigGenerator('', validator) + config = config_generator.choose_values(interactive=interactive, + flux=flux).sections + assert config['sec']['workload_scheduler'] == expected + + +@pytest.mark.parametrize("interactive, input_val, expected", + [ + (True, 'y', 'bee.conf'), + (True, 'n', 'Quitting'), + (False, '', 'bee.conf'), + ], + ) +def test_config_generator_save(mocker, tmpdir, capsys, interactive, input_val, expected): + """Test the ConfigGenerator save function with different flags/input options.""" + mocker.patch('builtins.input', return_value=input_val) + alter_config = ConfigGenerator('bee.conf', lambda _: _) + with tmpdir.as_cwd(): + alter_config.save(interactive=interactive) + captured = capsys.readouterr() + assert expected in captured.out + + +@pytest.mark.parametrize("interactive, input_val, expected", + [ + (True, 'y', True), + (True, 'n', False), + (False, '', True), + ], + ) +def test_config_new(mocker, tmpdir, interactive, input_val, expected): + """Test creation of new config with different settings.""" + filename = 'bee.conf' + mocker.patch('builtins.input', return_value=input_val) + mocker.patch('beeflow.common.config_driver.ConfigGenerator.choose_values') + mocker.patch('beeflow.common.config_driver.ConfigGenerator.choose_values.save') + with tmpdir.as_cwd(): + with open(filename, 'w', encoding='utf-8'): + pass + new("bee.conf", interactive=interactive) + assert os.path.exists('bee.conf.1') == expected diff --git a/docs/sphinx/installation.rst b/docs/sphinx/installation.rst index 2bdc8f1d..8be041ba 100644 --- a/docs/sphinx/installation.rst +++ b/docs/sphinx/installation.rst @@ -53,17 +53,20 @@ You will need to setup the bee configuration file that will be located in: macOS: ``~/Library/Application Support/beeflow/bee.conf`` -Before creating a bee.conf file you will need to know the path to the two required Charliecloud containers, one for Neo4j (``neo4j_image``) and Redis (``redis_image``). See `Requirements:`_ above for pulling these containers. Depending on the system, you may also need to know system-specific information, such as account information. You can leave some options blank if these are unnecessary. +Using ``beeflow config new`` will create the configuration file using the default values or you can use the ``--interactive`` flag to be prompted for each setting. -Once you are ready type ``beeflow config new``. - -The bee.conf configuration file is a text file and you can edit it for your -needs. +Following generation of the bee.conf configuration file you can edit the settings using a text editor. **Caution: The default for container_archive is in the home directory. Some systems have small quotas for home directories and containers can be large files.** +It is important to ensure the path to the two required Charliecloud containers is correct. There is a setting for the Neo4j container (``neo4j_image``) and the Redis container (``redis_image``). See `Requirements:`_ above for pulling these containers. + +Depending on the system, you may also need to enter the system-specific information, such as account information. + +Options can be left blank if they are unnecessary. + **beeflow config** has other options including a configuration validator. For more information or help run: ``beeflow config info`` or ``beeflow config --help``.