Skip to content

Fix AttributeError: 'dict' object has no attribute 'lower' in compass setup#5

Draft
Copilot wants to merge 3 commits into
copilot/replace-restart-member-classfrom
copilot/fix-compass-setup-crash
Draft

Fix AttributeError: 'dict' object has no attribute 'lower' in compass setup#5
Copilot wants to merge 3 commits into
copilot/replace-restart-member-classfrom
copilot/fix-compass-setup-crash

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 31, 2026

compass setup crashed immediately because configure() called config.get('restart_ensemble', {}) — treating MpasConfigParser (a configparser.ConfigParser subclass) as a dict. configparser.get(section, option) interprets the second arg as the option name and calls .lower() on it, blowing up on a {}.

Changes

  • test_case.py: Rewrote configure() to use the proper configparser API throughout:
    • config.get('restart_ensemble', 'spinup_work_dir') instead of config.get('restart_ensemble', {}).get('spinup_work_dir')
    • config.getint/getfloat/getboolean('restart_ensemble', option) with try/except (configparser.NoSectionError, configparser.NoOptionError) for optional keys with defaults
    • Added import configparser for the specific exception types
# Before (broken)
section = config.get('restart_ensemble', {})          # AttributeError
max_restarts = section.getint('max_consecutive_restarts', 3)

# After (correct)
spinup_work_dir = config.get('restart_ensemble', 'spinup_work_dir')
try:
    max_restarts = config.getint('restart_ensemble', 'max_consecutive_restarts')
except (configparser.NoSectionError, configparser.NoOptionError):
    max_restarts = 3

Checklist

  • User's Guide has been updated
  • Developer's Guide has been updated
  • API documentation in the Developer's Guide (api.rst) has any new or modified class, method and/or functions listed
  • Documentation has been built locally and changes look as expected
  • The E3SM-Project submodule has been updated with relevant E3SM changes
  • The MALI-Dev submodule has been updated with relevant MALI changes
  • Document (in a comment titled Testing in this PR) any testing that was used to verify the changes
  • New tests have been added to a test suite
Original prompt

Bug

compass setup crashes with:

AttributeError: 'dict' object has no attribute 'lower'

at compass/landice/tests/ensemble_generator/sgh_restart_ensemble/test_case.py, line 60:

section = config.get('restart_ensemble', {})

Root Cause

config is an MpasConfigParser (from mpas_tools), which inherits from configparser.ConfigParser. Its .get() signature is get(section, option) — not get(section, default) like a dict. Passing {} as the second argument causes configparser to call {}.lower(), which raises AttributeError: 'dict' object has no attribute 'lower'.

The code then goes on to use section as if it were a config section proxy (section.get(...), section.getint(...), section.getboolean(...)), which is not how MpasConfigParser/configparser works.

Fix

In compass/landice/tests/ensemble_generator/sgh_restart_ensemble/test_case.py, replace the configure() method body so that each config value is read directly via config.get(section_name, option_name) (and config.getint, config.getfloat, config.getboolean) rather than through a fake section proxy.

The corrected configure() should look like:

def configure(self):
    config = self.config

    try:
        spinup_work_dir = config.get('restart_ensemble', 'spinup_work_dir')
    except Exception:
        raise ValueError(
            "restart_ensemble config must specify spinup_work_dir\n"
            "Add to config file:\n"
            "[restart_ensemble]\n"
            "spinup_work_dir = /path/to/spinup/ensemble"
        )

    if not spinup_work_dir:
        raise ValueError(
            "restart_ensemble config must specify spinup_work_dir\n"
            "Add to config file:\n"
            "[restart_ensemble]\n"
            "spinup_work_dir = /path/to/spinup/ensemble"
        )

    if not os.path.exists(spinup_work_dir):
        raise ValueError(f"spinup_work_dir not found: {spinup_work_dir}")

    # Get restart configuration using proper configparser API
    try:
        max_consecutive_restarts = config.getint(
            'restart_ensemble', 'max_consecutive_restarts')
    except Exception:
        max_consecutive_restarts = 3

    try:
        min_simulation_years = config.getfloat(
            'restart_ensemble', 'min_simulation_years_before_restart')
    except Exception:
        min_simulation_years = 50.0

    try:
        auto_restart = config.getboolean(
            'restart_ensemble', 'auto_restart_incomplete')
    except Exception:
        auto_restart = True

    # Scan for existing run directories
    run_dirs = sorted(glob.glob(os.path.join(spinup_work_dir, 'run*')))

    restart_runs = []
    skipped_runs = []

    for run_dir in run_dirs:
        run_name = os.path.basename(run_dir)
        try:
            run_num = int(run_name.replace('run', ''))
        except ValueError:
            continue

        should_restart, reason = self._should_restart_run(
            run_dir=run_dir,
            run_num=run_num,
            min_years=min_simulation_years,
            max_restarts=max_consecutive_restarts,
            auto_restart=auto_restart
        )

        if should_restart:
            restart_runs.append(run_num)
            print(f"Scheduling restart for {run_name}")

            self.add_step(InPlaceRestartMember(
                test_case=self,
                run_num=run_num,
                spinup_work_dir=spinup_work_dir
            ))
        else:
            if reason:
                skipped_runs.append((run_num, reason))

    if skipped_runs:
        print("\nSkipped runs:")
        for run_num, reason in skipped_runs:
            print(f"  run{run_num:03}: {reason}")

    self.restart_run_numbers = restart_runs

    # Only run ensemble_manager; it submits individual restart jobs
    self.steps_to_run = ['ensemble_manager']

File to change

compass/landice/tests/ensemble_generator/sgh_restart_ensemble/test_case.py on branch copilot/replace-restart-member-class

Current BlobSha: 16388c1dd0557165679ffb8e573a5c6f49678677

All other files (restart_member.py, __init__.py, restart_scheduler.py) do not need changes.

The following is the prior conversation context from the user's chat exploration (may be truncated):

Assistant: The file content is truncated. Let me get the full files:

Assistant: I can see the current state. The commit 291d6794 is on the sgh_ensemble_generator_template branch (the original, before the Copilot PR). The restart_member.py there still has the old RestartMember class. Let me also look at what's on the Copilot PR branch now:

Assistant: The files are being returned truncated. Let me look at the current state of the branch that has your job_script.sh snippet to understand exactly what's there now:

Assistant: Good — I now have enough context. Let me look at the current state of the base branch to understand exactly what's there now:

Assistant: Now I have a clear picture of the cu...

This pull request was created from Copilot chat.

Copilot AI and others added 2 commits March 31, 2026 22:41
Copilot AI changed the title [WIP] Fix crash in compass setup due to configparser issue Fix AttributeError: 'dict' object has no attribute 'lower' in compass setup Mar 31, 2026
Copilot AI requested a review from alexolinhager March 31, 2026 22:44
Copilot AI added a commit that referenced this pull request Apr 1, 2026
Bug #1: config.get('restart_ensemble', {}) crashes because MpasConfigParser.get()
expects (section, option) positional args, not a dict fallback.
Fixed: config['restart_ensemble'] returns a SectionProxy with proper
.get()/.getint()/.getfloat()/.getboolean() methods.

Bug #2: _should_restart_run() looked for per-run analysis_results.json files
that are never written.  AnalysisStep writes analysis_summary.json to its
own work dir containing an individual_results dict for all runs.
Fixed: add analysis_summary_file config option; configure() loads the file
and passes per-run dicts to _should_restart_run() via a new run_results param.
RestartScheduler.create_config_file() now includes analysis_summary_file in
generated configs.

Bug #3: restart_attempt_N/ tracking dirs were never created by
InPlaceRestartMember.setup(), so max_consecutive_restarts was effectively
disabled and all attempt counters read 0.
Fixed: setup() now creates restart_attempt_N/ dirs using a single os.listdir()
call to find the highest existing attempt number.

Bug #5: restart_scheduler.py docstring Examples section referenced a
non-existent module path. Fixed to the correct path.

Agent-Logs-Url: https://github.com/alexolinhager/compass/sessions/ca2d29bf-1246-415c-bf2c-9de7521fa55f

Co-authored-by: alexolinhager <131483939+alexolinhager@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants