Refactor Slice 4: Decompose `Case` god object
- Dominant language
- Python
- Stars
- 174
- Forks
- 225
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 18
Description
Part of #4950. Slice 4 of 5. Estimated 6 weeks. Depends on Slices 1–3B.
## Goal
Decompose the `Case` class (`case.py`, 2600 lines, 63 methods) into focused
subsystems while consolidating the scattered Case-related free functions in
`utils.py`, `build.py`, `status.py`, `locked_files.py`, and `case_run.py`.
End state: a smaller `Case` with clear delegation to independently testable
subsystems.
## Why Case needs refactoring
`Case` is a god object that directly handles XML get/set across env files,
status tracking (CaseStatus), lock management (LockedFiles), build
orchestration, job submission, namelist generation, and clone/setup/test
workflows.
On top of that, many Case operations live **outside** the class as free
functions reaching into private attributes like `case._gitinterface`. Case
responsibility is smeared across ~10 files.
## Two-phase approach
### Phase 1 — Consolidate into Case
Bring scattered free functions back into Case (or into subsystem classes Case
will own). This **temporarily makes Case bigger** but gathers responsibility
in one place so the seams are visible.
- [ ] **Status** (from `status.py`): `append_case_status()`,
`run_and_log_case_status()`. Every caller currently extracts `caseroot`
and `case._gitinterface`.
- [ ] **Locking** (from `locked_files.py`): `check_lockedfiles()`,
`check_lockedfile()`, `diff_lockedfile()`, `check_diff()`.
- [ ] **Case queries** (from `utils.py`): `is_comp_standalone()`,
`find_system_test()`, `get_lids()`, `new_lid()`.
- [ ] **Case run helpers** (from `case/case_run.py`): `_pre_run_check()`,
`_run_model_impl()`, `_post_run_check()`, `_resubmit_check()`.
- [ ] **TestScheduler** (from `test_scheduler.py`): `_get_time_est()`,
`_order_tests_by_runtime()`, `_translate_test_names_for_new_pecount()`.
Module-level `_TIME_CACHE` should be instance state.
### Phase 2 — Extract subsystems
Decompose Case into focused modules it delegates to:
- [ ] `CIME/core/status/` — status tracking (CaseStatus file, `append_status`,
`run_and_log_status`).
- [ ] `CIME/core/locking/` — lock management (LockedFiles, `check_locked`,
`diff_locked`).
- [ ] `CIME/core/xml/` — XML value storage (env get/set/flush, the
`_env_entryid_files` and `_env_generic_files` arrays).
Case keeps its public API but each method delegates:
```python
class Case:
def __init__(self, ...):
self._status = StatusTracker(self._caseroot, self._gitinterface)
self._locks = LockManager(self._caseroot)
self._xml = XmlStore(self._env_files)
def append_status(self, msg, ...):
self._status.append(msg, ...)
def check_lockedfiles(self):
self._locks.check(self._xml)
```
## Other procedural modules to decompose
- [ ] `case/case_st_archive.py` (1395 lines, ~20 free functions taking `case`)
→ consolidate as Case methods or extract into `CIME/core/archive/`.
Resolves #4792.
- [ ] `case/check_input_data.py` (693 lines, ~12 free functions taking `case`)
→ consolidate.
- [ ] `baselines/performance.py` (612 lines, ~12 free functions taking `case`)
→ baseline comparison subsystem.
- [ ] `hist_utils.py` (836 lines, ~7 free functions taking `case`)
→ history file handling subsystem.
- [ ] `TestScheduler` (`test_scheduler.py`, 1340 lines, 28 methods)
→ decompose into test creation / phase mgmt / submission / results.
## Move remaining `utils.py` functions
- [ ] `wait_for_unlocked`, `normalize_case_id`, `parse_test_name`,
`get_full_test_name`, `compute_total_time` → `core/`.
- [ ] `convert_to_seconds`, `convert_to_babylonian_time`, `get_time_in_seconds`,
`format_time` → `CIME/core/time.py`.
- [ ] `convert_to_type`, `convert_to_unknown_type`, `convert_to_string`,
`stringify_bool` → `CIME/core/convert.py`.
By end of this slice, `utils.py` is a thin re-export file with no
implementation code.
## Definition of done
- [ ] Case public API unchanged — external callers work without modification.
- [ ] No free function reaches into `case._gitinterface` or other private
Case state.
- [ ] No module over ~500 lines.
- [ ] `core/status/`, `core/locking/`, `core/xml/` independently testable.
- [ ] `utils.py` is thin re-exports only (was 2700 lines).
- [ ] 80%+ coverage on new subsystems.
- [ ] Resolves #4792.
- [ ] All existing tests pass; E3SM/CESM/NorESM workflows unchanged.
## Related
- Resolves #4792 (config_archive.xml / case_st_archive.py)
- Depends on #4996, #4997, #4998, #4999
Contributor guide
Research direction
Read case.py and the Phase 1 source modules first, then map callers and existing tests before choosing a seam. Done means the public Case API and workflows remain unchanged, free functions no longer reach private Case state, the new status, locking, and XML subsystems are independently testable, utils.py contains only re-exports, and the listed coverage and test requirements pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 20/100