Skip to content

Refactor project structure and implement fire forest model with Hydra and tracking#135

Merged
SongshGeo merged 6 commits intomasterfrom
dev
Jan 6, 2026
Merged

Refactor project structure and implement fire forest model with Hydra and tracking#135
SongshGeo merged 6 commits intomasterfrom
dev

Conversation

@SongshGeo
Copy link
Copy Markdown
Collaborator

@SongshGeo SongshGeo commented Jan 6, 2026

Summary

This PR refactors the project structure and adds a new fire forest model example with comprehensive configuration management and experiment tracking.

Key Changes

  • Project refactoring: Migrate to uv for dependency management and restructure project layout
  • Fire forest model: Implement forest fire spread simulation with ABSESpy, featuring:
    • Spatial modeling using PatchCell with Von Neumann neighborhood
    • Hydra integration for configuration management
    • Experiment tracking support (Aim)
    • Solara-based visualization
  • Rainfall model: Fix bug and make it compatible with ABSESpy
  • Model compatibility: Update examples to work with ABSESpy v0.7.0+

Technical Details

  • Uses Hydra for flexible configuration management
  • Implements batch experiments with parameter sweeps
  • Demonstrates ABSESpy features: PatchCell, raster attributes, and ActorsList
  • Adds visualization support via Solara and Mesa Geo

Summary by CodeRabbit

  • New Features

    • Experiment-level logging with configurable modes (once, separate, merge) and per-run log handling.
    • Analysis utilities for Hydra multirun results (per-run and aggregated views).
    • Public API expanded to expose core experiment/model classes and analysis utilities.
  • Bug Fixes

    • More accurate model hyperparameter tracking in trackers.
  • Documentation

    • Added Analysis API docs and navigation entry.
  • Chores

    • Switched to stdlib logging and removed legacy logging dependency; version bumped to 0.10.0.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit deletes the .cursorrules file, which contained guidelines and rules for Python development practices. The removal of this file helps streamline the project structure by eliminating unnecessary documentation that may no longer be relevant.
…ment results

This commit introduces the `ResultAnalyzer` and `ExpAnalyzer` classes in the new `analysis.py` module, providing tools for analyzing and aggregating results from Hydra multirun experiments. It also updates the `mkdocs.yml` to include a new navigation entry for analysis documentation and adds corresponding markdown files for API documentation. Additionally, comprehensive tests for the analysis utilities are included to ensure functionality and reliability.
… logging

This commit removes the `loguru` dependency from the project, replacing it with Python's built-in `logging` module for improved consistency and maintainability. The logging configuration is updated to support experiment-level and model-level logging, with new utilities for setting up loggers and handling different logging modes. Additionally, a new YAML configuration file is introduced for logging settings, and comprehensive tests are added to ensure the logging functionality works as expected.
…tox.ini

This commit enhances the test-all and test-tox targets in the makefile by adding informative messages regarding the use of system Python interpreters for tox. It also includes a new step to install documentation dependencies for notebook tests. Additionally, the tox.ini file is updated with comments to clarify the requirement for system Python interpreters, improving the overall clarity and usability of the testing setup.
This commit updates the project version from 0.9.0 to 0.10.0 in both the pyproject.toml and uv.lock files. Additionally, it modifies the fallback version in abses/__init__.py to reflect the new version, ensuring consistency across the project.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Experiment- and run-level logging were reworked: loguru removed in favor of Python logging; new logging utilities parse config (Hydra-compatible), determine per-run log paths/modes, and configure separate experiment logger. Analysis utilities for Hydra multiruns (ExpAnalyzer, ResultAnalyzer) and public API exports were added.

Changes

Cohort / File(s) Summary
Package init & public API
abses/__init__.py, abses/utils/__init__.py
Expanded exports: Experiment, MainModel, time_condition, BaseHuman, PatchCell, raster_attribute; re-exported ExpAnalyzer and ResultAnalyzer.
Logging migration (modules)
abses/agents/container.py, abses/human/links.py, abses/space/patch.py, abses/utils/datacollector.py
Replaced loguru imports with standard logging and added module-level logger = logging.getLogger(__name__).
Logging core utilities
abses/utils/log_config.py, abses/utils/log_parser.py, abses/utils/logging.py
Centralized formatting/level defaults; new path-determination API; expanded setup_integrated_logging and setup_abses_logger signatures; added get_* parsers and log_repeat_separator; setup_model_logger signature extended for console/file/mesa params.
Experiment-level logging helpers
abses/utils/exp_logging.py, abses/utils/hydra_logging.py
New experiment logger setup (EXP_LOGGER_NAME, setup_exp_logger); Hydra-compatible job logging generator (generate_hydra_job_logging).
Core integration (Experiment / Model)
abses/core/experiment.py, abses/core/model.py
Experiment now detects logging mode, computes per-repeat log paths, logs experiment metadata via experiment logger; Model _setup_logger refactored to consume new stdout/file/mesa configs and handle merge/separate modes (including repeat separators and banner deferral).
Analysis utilities
abses/utils/analysis.py, docs/api/analysis.md, mkdocs.yml
Added ResultAnalyzer and ExpAnalyzer for Hydra run/multirun analysis; docs and nav entry added.
Config & packaging
abses/conf/absespy.yaml, pyproject.toml
Added Hydra YAML with detailed logging/exp settings; bumped version 0.9.0 → 0.10.0 and removed loguru dependency.
Tests & test infra
tests/utils/test_analysis.py, tests/utils/test_logging.py, makefile, tox.ini, .pre-commit-config.yaml
New tests for analysis and logging flows; updated Makefile/tox notes and pre-commit mypy args to run on abses/.
Tracker tweak
abses/utils/tracker/factory.py
Tracker now records model.settings instead of model.params when creating run hyperparameters.
Removed
test_colormap.py
Deleted obsolete test script.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant User
participant Experiment
participant ExpLogger as "Experiment Logger"
participant Model
participant ModelLogger as "Model Logger / File"
participant FS as "Filesystem"

Note over Experiment,ModelLogger: New flow: experiment-level logger coordinates run logs

User->>Experiment: start multirun (cfg, repeats)
Experiment->>ExpLogger: setup_exp_logger(cfg) [determine mode, outpath]
Experiment->>Experiment: for each repeat compute log_path (determine_log_file_path)
alt logging_mode == "separate"
    Experiment->>FS: create per-repeat log file path
    Experiment->>User: print log file path
end
Experiment->>Model: launch run(repeat_id, log_path)
Model->>ModelLogger: setup_model_logger(cfg, log_file_path=log_path, logging_mode)
Model->>ModelLogger: emit run logs (console/file/mesa)
ModelLogger->>FS: write/run log file
Note over ExpLogger,ModelLogger: Experiment logger and Model logger are separate, handlers configured per mode

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops through configs, I bound and dig—

Experiment logs now sing, not brig.
Models, files, banners in a row,
Hydra seeds help output grow.
Nibbles of code — a carrot-glow.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: refactoring project structure, implementing a fire forest model, and integrating Hydra and tracking capabilities.
Docstring Coverage ✅ Passed Docstring coverage is 97.17% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
abses/utils/datacollector.py (1)

216-218: Fix typo in warning message: missing space.

There's a missing space between "defined" and "returning" in the warning message.

🔎 Proposed fix
         if not self.model_reporters:
             logger.warning(
-                "No model reporters have been definedreturning empty DataFrame."
+                "No model reporters have been defined, returning empty DataFrame."
             )
pyproject.toml (1)

50-57: Update constraint to exclude the vulnerable version or explicitly pin to a patched release.

CVE-2025-5321 is confirmed to affect aim versions up to and including 3.29.1 (sandbox-escape/code-injection vulnerability in RestrictedPythonQuery). The current constraint aim>=3.29.1 allows the vulnerable version. While 3.30.0 stable does not exist, development versions (3.30.0.dev*) are available but unsuitable for production use. Stable 3.9.4 and 4.x releases exist; either specify a known patched version explicitly or wait for an official fix release and update the constraint to aim>=<patched-version>.

abses/utils/log_config.py (2)

214-222: file_datefmt parameter is not passed to handler.

Similarly, file_datefmt is accepted (line 175) but not passed to create_file_handler (lines 215-221).

🔎 Proposed fix
     if file_path:
         handler = create_file_handler(
             filepath=file_path,
             level=file_level or level,
             fmt=file_format or DEFAULT_FORMAT,
+            datefmt=file_datefmt or DEFAULT_DATEFMT,
             rotation=rotation,
             retention=retention,
         )

297-308: Create new handlers for Mesa loggers instead of modifying shared references.

Lines 300-301 modify formatter on handler objects that are the same references attached to abses_logger (passed via list(abses_logger.handlers) at line 422). This overwrites the original ABSES logger's formatter with the Mesa format, causing unintended side effects. Either copy handlers before modification or create fresh handlers for Mesa loggers.

🤖 Fix all issues with AI Agents
In @abses/core/experiment.py:
- Around line 160-172: The code mutates the input cfg when it's a plain dict
(cfg["outpath"] = ...), causing asymmetry with the DictConfig branch; to fix,
create a copy of the dict (e.g., cfg_copy = dict(cfg) or use copy.deepcopy) and
set cfg_copy["outpath"] = str(self.outpath) then call setup_exp_logger(cfg_copy)
instead of modifying cfg directly; keep the DictConfig branch unchanged
(OmegaConf.to_container/OmegaConf.create) and ensure you reference cfg and
setup_exp_logger in this change.
- Around line 421-446: The method _log_experiment_info currently prints a
hardcoded "Logging mode: separate"; instead read the actual logging mode from
the configuration passed in (cfg) and log that value (e.g., use cfg.logging.mode
or derive it from cfg.exp_file/related fields) so the output reflects the real
mode; if you prefer explicitness, add a logging_mode parameter to
_log_experiment_info and update all callers (places that call
_log_experiment_info) to pass the real mode so the logged line uses that
parameter rather than the hardcoded "separate".

In @abses/core/model.py:
- Around line 359-366: The isinstance check on exp_cfg prevents reading repeats
when self.settings returns an Omega DictConfig; update the check in the block
using logging_mode, self.run_id and exp_cfg so it accepts both dict and
DictConfig (e.g. isinstance(exp_cfg, (dict, DictConfig))) or remove the
isinstance guard entirely and directly call exp_cfg.get("repeats", 1), then pass
total_repeats into log_repeat_separator; ensure you import or reference
DictConfig if you add it to the isinstance tuple.

In @abses/utils/analysis.py:
- Around line 427-439: The results property yields a generator that gets
exhausted after one iteration; change it to build and cache a list (e.g.,
self._results_list) by iterating self.subdir and appending
ResultAnalyzer(subdir) while catching FileNotFoundError/ValueError and logging
as currently done, then return an iterator over that cached list (or expose the
list). Update diff_runs and agg_data to iterate over self._results_list (or call
list(self.results) once and reuse it) instead of iterating the generator
multiple times; also ensure nested loops in diff_runs use the cached list so
subsequent keys see the full set.
- Around line 162-170: The __init__ method in ResultAnalyzer contains a
duplicated docstring; remove the second misplaced docstring block that appears
after the call to super().__init__ so only the proper docstring (the one
directly under def __init__) remains and is the one parsed as documentation;
adjust surrounding whitespace/comments if needed to keep PEP8 formatting and
ensure ResultAnalyzer.__init__ still documents args and raises correctly.

In @abses/utils/log_config.py:
- Around line 206-211: The console_datefmt parameter accepted by the log
configuration is never forwarded to the console handler; update the call to
create_console_handler inside the block that checks `if console:` to pass the
`console_datefmt` argument (alongside level and fmt) so the handler uses the
user-provided date format; ensure the symbol names `create_console_handler`,
`console_datefmt`, and `logger.addHandler(handler)` are used to locate and
update the code.

In @abses/utils/log_parser.py:
- Around line 105-106: The current check on old_log is unsafe because subsequent
code calls old_log.get(); change the guard so old_log is guaranteed to be a dict
before using .get() — e.g., replace the conditional with a strict type check
that returns or normalizes non-dict values (for example: if not
isinstance(old_log, dict): return {} or set old_log = {}), ensuring any truthy
non-dict (like a string) cannot reach the .get() calls that follow.
- Around line 30-74: get_log_mode incorrectly treats an explicit log.mode="once"
the same as a missing value because OmegaConf.select(cfg, "log.mode",
default="once") cannot distinguish default vs explicit; update the DictConfig
branch to call OmegaConf.select(cfg, "log.mode") with no default and only if
that returns None set mode="once" and then consult OmegaConf.select(cfg,
"exp.logging.mode", default=None) for backward-compat; similarly update the
plain dict branch to detect explicit setting by checking if "log" in cfg and
"mode" in cfg["log"] (use that value if present, otherwise set mode="once" and
then inspect cfg.get("exp", {}).get("logging") for old_mode) and return the
explicit mode immediately without falling back to exp.logging.mode.

In @tests/utils/test_logging.py:
- Around line 52-136: All test methods in TestLogParser (e.g.,
test_get_log_mode_default, test_get_log_mode_from_config,
test_get_stdout_config_enabled, test_get_stdout_config_disabled,
test_get_file_config_enabled, test_get_file_config_defaults,
test_get_mesa_config, test_get_mesa_config_defaults) lack explicit return type
annotations; update each method signature to include the explicit return type
"-> None" (for example: def test_get_log_mode_default(self) -> None:) and apply
the same change to every other test method in the file.
- Around line 19-27: Add a TYPE_CHECKING import and make pytest-related imports
type-only: add "from typing import TYPE_CHECKING" at the top, and move or wrap
the "import pytest" (and any test fixture/plugin type imports) inside an "if
TYPE_CHECKING:" block so those imports are only for type checking; leave any
needed runtime imports (if decorators or runtime pytest APIs are used)
unchanged. Ensure you reference the existing import line "import pytest" and
wrap any plugin/fixture type imports similarly (e.g., mocker/fixture types)
under the TYPE_CHECKING guard.
🧹 Nitpick comments (15)
abses/utils/hydra_logging.py (2)

22-23: Remove unnecessary empty TYPE_CHECKING block.

The TYPE_CHECKING block contains only pass and serves no purpose. It should be removed to reduce code clutter.

🔎 Proposed fix
-if TYPE_CHECKING:
-    pass

38-49: Consider simplifying default value handling.

Since get_stdout_config already applies defaults (or returns an empty dict), the conditional logic at lines 42-49 could be simplified. The function currently defines local defaults and applies them conditionally, which duplicates the default handling already performed by get_stdout_config.

💡 Suggested simplification
-    # Default configuration
-    default_format = "[%(asctime)s][%(name)s][%(levelname)s] - %(message)s"
-    default_datefmt = "%H:%M:%S"
-    default_level = "WARNING"
-
-    if hydra_stdout:
-        format_str = hydra_stdout.get("format", default_format)
-        datefmt = hydra_stdout.get("datefmt", default_datefmt)
-        level = hydra_stdout.get("level", default_level)
-    else:
-        format_str = default_format
-        datefmt = default_datefmt
-        level = default_level
+    # Use defaults from get_stdout_config or fallback to reasonable defaults
+    format_str = hydra_stdout.get("format", "[%(asctime)s][%(name)s][%(levelname)s] - %(message)s")
+    datefmt = hydra_stdout.get("datefmt", "%H:%M:%S")
+    level = hydra_stdout.get("level", "WARNING")

This relies on get_stdout_config to provide configured values (with its own defaults already applied) and only uses the .get() fallbacks when the returned dict is empty.

tests/utils/test_analysis.py (1)

21-22: Import pytest fixture types in the TYPE_CHECKING block.

According to the coding guidelines, test files should conditionally import pytest fixtures and plugins under TYPE_CHECKING. Consider importing the fixture types used in this file:

🔎 Proposed improvement
 if TYPE_CHECKING:
-    pass
+    from _pytest.fixtures import FixtureRequest
+    from _pytest.tmpdir import TempPathFactory
abses/conf/absespy.yaml (1)

70-76: Document placeholder values and their runtime behavior.

The configuration includes several null placeholders with example values in comments (rotation, retention, mesa level/format). Consider documenting:

  1. What happens when these values are null (e.g., no rotation, inherit from parent level)
  2. Validation of the example formats (e.g., "1 day", "100 MB" for rotation)
  3. Whether these can be dynamically configured at runtime

This will help users understand the configuration options better.

Based on coding guidelines, ensuring configuration is well-documented improves maintainability.

abses/utils/logging.py (1)

102-120: Consider refactoring to reduce parameter count.

The setup_model_logger function now has 20+ parameters, which makes the signature complex and potentially error-prone to call. Consider refactoring options:

  1. Configuration object approach: Group related parameters into configuration objects (e.g., ConsoleConfig, FileConfig, MesaConfig)
  2. Builder pattern: Use a builder class to construct logging configuration incrementally
  3. Kwargs dict: Accept structured configuration dictionaries for console/file/mesa settings

Example:

from dataclasses import dataclass

@dataclass
class ConsoleConfig:
    enabled: bool = True
    level: Optional[str] = None
    format: Optional[str] = None
    datefmt: Optional[str] = None

def setup_model_logger(
    name: str = "model",
    level: str = "INFO",
    outpath: Optional[Path] = None,
    console: Union[bool, ConsoleConfig] = True,
    file: Optional[FileConfig] = None,
    mesa: Optional[MesaConfig] = None,
    ...
) -> tuple[logging.Logger, logging.Logger, logging.Logger]:

This would improve readability and make it easier to extend configuration options in the future.

abses/utils/exp_logging.py (3)

35-36: Remove empty TYPE_CHECKING block.

The TYPE_CHECKING block with only pass serves no purpose and can be removed.

🔎 Proposed fix
-if TYPE_CHECKING:
-    pass
-

117-132: Extract duplicated outpath resolution logic into a helper function.

The outpath resolution logic (handling dict vs DictConfig, None fallback, string/Path conversion) appears twice in this function (lines 117-132 and 154-169). This violates DRY and increases maintenance burden.

🔎 Proposed refactor

Add a helper function and use it in both places:

def _resolve_outpath(cfg: DictConfig | dict) -> Path:
    """Resolve output path from configuration.
    
    Args:
        cfg: Configuration dictionary.
        
    Returns:
        Resolved output path.
    """
    if isinstance(cfg, dict):
        outpath = cfg.get("outpath")
    else:
        try:
            outpath = OmegaConf.select(cfg, "outpath", default=None)
        except Exception:
            outpath = None

    if outpath is None:
        return Path.cwd()
    elif isinstance(outpath, str):
        return Path(outpath)
    elif not isinstance(outpath, Path):
        return Path(str(outpath))
    return outpath

Also applies to: 154-169


96-99: Bare except Exception swallows useful debugging info.

The broad exception handling with just exp_file_cfg_raw = {} makes it hard to debug configuration issues. Consider logging a warning or being more specific about expected exceptions.

🔎 Proposed improvement
         else:
             try:
                 exp_file_cfg_raw = OmegaConf.select(cfg, "log.exp.file", default={})
-            except Exception:
+            except (KeyError, TypeError) as e:
+                import logging
+                logging.getLogger(__name__).debug(f"Could not select log.exp.file: {e}")
                 exp_file_cfg_raw = {}
abses/core/model.py (2)

371-377: Remove empty conditional block with only pass.

The if logging_mode == "separate" block contains only a pass statement and comments. This adds no functionality and reduces readability. Consider restructuring to only execute setup_logger_info when needed.

🔎 Proposed refactor
         # Display startup info
-        # In separate mode, setup_logger_info should only go to experiment log
-        # For model run logs, only log model-specific info
-        if logging_mode == "separate":
-            # In separate mode, don't log framework banner to model run log
-            # It will be logged to experiment log file instead
-            pass
-        else:
-            # In once/merge mode, log to model run log
+        # In separate mode, framework banner goes to experiment log only
+        # In once/merge mode, log to model run log
+        if logging_mode != "separate":
             setup_logger_info(self.exp)
-        # Always log model-specific info to model run log
+
         self._logging_begin()

293-357: Consider extracting logging config extraction into a helper method.

The _setup_logger method has grown to handle many responsibilities: extracting stdout config, file config, mesa config, determining log mode, setting up handlers, and handling mode-specific behavior. Consider extracting the configuration extraction (lines 299-337) into a separate method like _get_logging_config() for better maintainability.

abses/core/experiment.py (2)

163-169: Simplify nested type check.

The if isinstance(cfg_dict, dict) check on line 166 is redundant since OmegaConf.to_container with resolve=True always returns a dict when the input is a DictConfig.

🔎 Proposed simplification
         if isinstance(cfg, DictConfig):
             # Create a copy to avoid modifying original
             cfg_dict = OmegaConf.to_container(cfg, resolve=True)
-            if isinstance(cfg_dict, dict):
-                cfg_dict["outpath"] = str(self.outpath)  # Convert Path to string
-                cfg_copy = OmegaConf.create(cfg_dict)
-                setup_exp_logger(cfg_copy)
+            cfg_dict["outpath"] = str(self.outpath)  # Convert Path to string
+            cfg_copy = OmegaConf.create(cfg_dict)
+            setup_exp_logger(cfg_copy)

490-493: Remove empty conditional block.

The if logging_mode == "merge" block with only a pass statement and comment adds no value and reduces readability.

🔎 Proposed fix
-                # Log separator for merge mode
-                if logging_mode == "merge" and repeat_id > 1:
-                    # Note: Separator will be logged in model setup
-                    pass
-
                 # Get log file path for this repeat
abses/utils/log_parser.py (1)

21-22: Remove empty TYPE_CHECKING block.

The TYPE_CHECKING block with only pass serves no purpose.

🔎 Proposed fix
-if TYPE_CHECKING:
-    pass
-
abses/utils/analysis.py (2)

31-32: Remove empty TYPE_CHECKING block.

The TYPE_CHECKING block with only pass serves no purpose.

🔎 Proposed fix
-if TYPE_CHECKING:
-    pass
-

318-331: lru_cache on get_data is ineffective.

The @lru_cache(maxsize=1) decorator caches the return value, but the method returns self.data.copy(), meaning each call creates a new DataFrame copy. The cache stores a reference to the first copy, but subsequent calls still create new copies before the cache check.

More importantly, lru_cache doesn't work correctly on instance methods because self is part of the cache key.

🔎 Proposed fix - Use cached_property or remove cache
-    @lru_cache(maxsize=1)
-    def get_data(self, **kwargs: Any) -> pd.DataFrame:
+    def get_data(self, **kwargs: Any) -> pd.DataFrame:
         """Get processed data with optional transformations.

         This method can be overridden or extended to support different
         aggregation levels or data transformations.

         Args:
             **kwargs: Additional arguments for data processing.

         Returns:
             Processed DataFrame.
         """
         return self.data.copy()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da9c90e and b38b438.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .cursor/rules/.cursorrules
  • abses/__init__.py
  • abses/agents/container.py
  • abses/conf/absespy.yaml
  • abses/core/experiment.py
  • abses/core/model.py
  • abses/human/links.py
  • abses/space/patch.py
  • abses/utils/__init__.py
  • abses/utils/analysis.py
  • abses/utils/datacollector.py
  • abses/utils/exp_logging.py
  • abses/utils/hydra_logging.py
  • abses/utils/log_config.py
  • abses/utils/log_parser.py
  • abses/utils/logging.py
  • abses/utils/tracker/factory.py
  • docs/api/analysis.md
  • makefile
  • mkdocs.yml
  • pyproject.toml
  • test_colormap.py
  • tests/utils/test_analysis.py
  • tests/utils/test_logging.py
  • tox.ini
💤 Files with no reviewable changes (1)
  • test_colormap.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Write detailed documentation using docstrings in all Python functions and classes, following PEP 257 conventions
Add typing annotations to each function or class in Python files, including explicit return types (including None where appropriate)

Files:

  • abses/agents/container.py
  • abses/utils/hydra_logging.py
  • abses/utils/log_parser.py
  • tests/utils/test_logging.py
  • abses/utils/__init__.py
  • abses/utils/exp_logging.py
  • abses/space/patch.py
  • abses/utils/logging.py
  • abses/utils/tracker/factory.py
  • abses/human/links.py
  • tests/utils/test_analysis.py
  • abses/utils/analysis.py
  • abses/core/experiment.py
  • abses/utils/datacollector.py
  • abses/core/model.py
  • abses/utils/log_config.py
  • abses/__init__.py
tests/**/@(test_*.py|*_test.py)

📄 CodeRabbit inference engine (.cursorrules)

tests/**/@(test_*.py|*_test.py): Use pytest for comprehensive testing in Python test files
In test files, import TYPE_CHECKING and conditionally import pytest fixtures and plugins (_pytest.capture.CaptureFixture, _pytest.fixtures.FixtureRequest, _pytest.logging.LogCaptureFixture, _pytest.monkeypatch.MonkeyPatch, pytest_mock.plugin.MockerFixture)
All tests should have typing annotations and contain docstrings

Files:

  • tests/utils/test_logging.py
  • tests/utils/test_analysis.py
tests/**/*.py

📄 CodeRabbit inference engine (.cursorrules)

Place all tests under ./tests directory and use pytest or pytest plugins exclusively

Files:

  • tests/utils/test_logging.py
  • tests/utils/test_analysis.py
🧠 Learnings (2)
📚 Learning: 2025-12-12T14:30:01.950Z
Learnt from: CR
Repo: SongshGeoLab/ABSESpy PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-12T14:30:01.950Z
Learning: Applies to tests/**/@(test_*.py|*_test.py) : Use pytest for comprehensive testing in Python test files

Applied to files:

  • makefile
📚 Learning: 2025-12-12T14:30:01.950Z
Learnt from: CR
Repo: SongshGeoLab/ABSESpy PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-12T14:30:01.950Z
Learning: Implement robust error handling and logging with context capture in Python files

Applied to files:

  • abses/human/links.py
🧬 Code graph analysis (11)
abses/utils/hydra_logging.py (1)
abses/utils/log_parser.py (1)
  • get_stdout_config (149-174)
abses/utils/log_parser.py (4)
abses/core/experiment.py (2)
  • cfg (180-182)
  • cfg (185-198)
abses/space/patch.py (1)
  • select (595-637)
abses/utils/analysis.py (2)
  • select (117-129)
  • select (333-342)
abses/human/links.py (2)
  • get (453-481)
  • get (704-740)
tests/utils/test_logging.py (8)
abses/core/model.py (6)
  • MainModel (73-566)
  • outpath (169-178)
  • name (159-166)
  • setup (534-535)
  • step (537-541)
  • exp (407-409)
abses/core/experiment.py (6)
  • Experiment (143-601)
  • cfg (180-182)
  • cfg (185-198)
  • outpath (243-247)
  • new (207-226)
  • batch_run (552-585)
abses/utils/exp_logging.py (1)
  • setup_exp_logger (42-183)
abses/utils/log_config.py (3)
  • create_console_handler (79-100)
  • create_file_handler (103-162)
  • determine_log_file_path (227-268)
abses/utils/log_parser.py (4)
  • get_file_config (177-207)
  • get_log_mode (30-74)
  • get_mesa_config (210-230)
  • get_stdout_config (149-174)
abses/utils/logging.py (2)
  • setup_model_logger (102-179)
  • formatter (40-51)
abses/core/time_driver.py (1)
  • fmt (187-192)
abses/core/job_manager.py (1)
  • ExperimentManager (20-150)
abses/utils/__init__.py (1)
abses/utils/analysis.py (2)
  • ExpAnalyzer (345-541)
  • ResultAnalyzer (132-342)
abses/utils/exp_logging.py (2)
abses/utils/log_config.py (2)
  • create_console_handler (79-100)
  • create_file_handler (103-162)
abses/utils/log_parser.py (3)
  • get_file_config (177-207)
  • get_log_mode (30-74)
  • get_stdout_config (149-174)
abses/utils/logging.py (1)
abses/utils/log_config.py (2)
  • determine_log_file_path (227-268)
  • setup_integrated_logging (339-426)
abses/utils/tracker/factory.py (1)
abses/core/model.py (1)
  • settings (420-432)
abses/utils/analysis.py (1)
abses/core/experiment.py (3)
  • _load_hydra_cfg (309-337)
  • overrides (250-259)
  • overrides (262-266)
abses/core/experiment.py (4)
abses/utils/exp_logging.py (1)
  • setup_exp_logger (42-183)
abses/utils/log_parser.py (2)
  • get_file_config (177-207)
  • get_log_mode (30-74)
abses/utils/log_config.py (2)
  • determine_log_file_path (227-268)
  • info (474-476)
abses/utils/logging.py (1)
  • setup_logger_info (71-86)
abses/core/model.py (2)
abses/utils/log_parser.py (4)
  • get_file_config (177-207)
  • get_log_mode (30-74)
  • get_mesa_config (210-230)
  • get_stdout_config (149-174)
abses/utils/logging.py (3)
  • log_repeat_separator (89-99)
  • setup_model_logger (102-179)
  • setup_logger_info (71-86)
abses/__init__.py (2)
abses/core/model.py (1)
  • version (181-187)
abses/core/protocols.py (1)
  • version (240-240)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Tests 3.13 on macos-latest
  • GitHub Check: Tests 3.11 on macos-latest
  • GitHub Check: Tests 3.11 on windows-latest
  • GitHub Check: Tests 3.13 on windows-latest
  • GitHub Check: Tests 3.11 on ubuntu-latest
  • GitHub Check: Tests 3.12 on windows-latest
  • GitHub Check: Tests 3.12 on macos-latest
  • GitHub Check: Tests 3.13 on ubuntu-latest
  • GitHub Check: Tests 3.12 on ubuntu-latest
  • GitHub Check: nbmake
🔇 Additional comments (26)
pyproject.toml (1)

12-12: LGTM - Version bump to 0.10.0.

The version increment appropriately reflects the scope of changes in this PR, including the logging system migration and new analysis utilities.

abses/human/links.py (1)

13-13: LGTM - Standard logging migration.

The migration from loguru to Python's standard logging follows idiomatic patterns: importing logging and creating a module-level logger with logging.getLogger(__name__). This ensures proper logger hierarchy and compatibility with the centralized logging configuration introduced in this PR.

Also applies to: 45-46

abses/agents/container.py (1)

14-14: LGTM - Logging migration correctly implemented.

The standard logging setup follows the project-wide pattern. The debug log at line 294 appropriately tracks agent creation without being too verbose.

Also applies to: 46-48

abses/space/patch.py (1)

13-13: LGTM - Consistent logging pattern applied.

The logging migration follows the same pattern as other modules. The logger is appropriately used for initialization info (line 277) and warning messages in the apply_agents method for exception handling.

Also applies to: 58-59

tox.ini (1)

5-6: LGTM - Helpful documentation for interpreter selection.

The comments clarify the expected Python interpreter source, which helps developers understand the tox configuration and avoid path issues with uv-managed environments.

abses/utils/datacollector.py (1)

12-12: LGTM - Logging migration correctly implemented.

The standard logging pattern is correctly applied with the module-level logger positioned appropriately after type alias declarations.

Also applies to: 45-46

makefile (2)

191-198: LGTM - Improved test-all robustness.

The changes add appropriate fallback handling for docs dependency installation and provide clear warnings about potential tox/uv compatibility issues. The direct tox -p auto invocation aligns with the guidance in tox.ini about using system Python interpreters.


203-204: LGTM - Consistent tox invocation pattern.

The test-tox target now matches the approach used in test-all, with clear documentation about system Python requirements.

mkdocs.yml (1)

67-67: LGTM! Documentation navigation updated appropriately.

The new Analysis API documentation is correctly added to the navigation structure under the "Workflow Control" section, which is an appropriate placement for experiment analysis utilities.

docs/api/analysis.md (1)

1-10: LGTM! Documentation structure is clean and follows conventions.

The documentation file correctly references the new analysis utilities using mkdocstrings directives, and the YAML front matter is properly formatted.

abses/utils/tracker/factory.py (1)

228-228: LGTM! Parameter source correctly updated to model.settings.

The change from model.params to model.settings aligns with the broader refactoring toward centralized configuration management using Hydra. The type compatibility is maintained as model.settings returns a DictConfig, which matches the expected parameter type.

abses/utils/__init__.py (1)

5-5: LGTM! New analysis utilities properly exposed in public API.

The ExpAnalyzer and ResultAnalyzer classes are correctly imported and added to __all__. Based on the provided code snippets, both classes follow the coding guidelines with comprehensive docstrings (PEP 257) and complete type annotations.

Also applies to: 16-17

abses/utils/hydra_logging.py (1)

26-34: LGTM! Function documentation and type annotations comply with guidelines.

The generate_hydra_job_logging function has proper docstrings following PEP 257 conventions and complete type annotations including explicit return types, meeting the project's coding guidelines.

tests/utils/test_analysis.py (1)

25-321: Excellent test coverage for analysis utilities.

The test suite comprehensively covers:

  • Fixture setup with realistic Hydra multirun directory structures
  • Base analyzer functionality (path, config, select, subdir)
  • ResultAnalyzer initialization, data reading, and reporter extraction
  • ExpAnalyzer overrides parsing, results generation, diff_runs, agg_data, and apply methods
  • Edge cases like invalid paths and empty directories

The tests are well-structured with clear docstrings and proper type annotations.

abses/__init__.py (1)

35-42: Improved fallback version handling with warnings.

The changes improve version handling by:

  1. Using Python's built-in warnings module instead of environment variables
  2. Updating the fallback version to v0.10.0-dev (aligns with PR objectives to bump from 0.9.0 to 0.10.0)
  3. Providing clear user feedback when package metadata is unavailable

This is a cleaner approach than the previous environment-variable based initialization.

abses/utils/logging.py (1)

89-100: Well-implemented separator logging function.

The log_repeat_separator function properly:

  • Has clear docstring and type annotations
  • Uses the LoggerAdapter's no_format binding for clean output
  • Provides useful visual separation for merge-mode runs
tests/utils/test_logging.py (1)

385-557: Comprehensive integration testing with good coverage.

The TestLoggingIntegration class provides excellent end-to-end testing:

  • Tests experiment and model logging flows
  • Validates file creation and handler configuration
  • Covers all logging modes (once, separate, merge)
  • Includes proper cleanup with ExperimentManager singleton reset

The integration tests effectively validate the complete logging system behavior.

abses/utils/exp_logging.py (1)

42-57: LGTM - Well-documented function signature.

The function has proper type annotations, a clear docstring following PEP 257, and appropriate parameter documentation. As per coding guidelines, this follows the required documentation patterns.

abses/core/model.py (1)

46-58: LGTM - Clean import organization.

The new imports for logging utilities are well-organized and follow the project's import structure. The addition of log_repeat_separator and parser functions aligns with the new logging subsystem.

abses/core/experiment.py (1)

391-419: LGTM - Well-documented helper methods.

The new helper methods _get_logging_mode and _get_log_file_path have proper docstrings, type annotations, and follow PEP 257 conventions. The late import in _get_log_file_path (line 412) is acceptable to avoid circular imports.

abses/utils/log_parser.py (2)

149-174: LGTM - Clean stdout configuration parser.

The function correctly handles enabled/disabled states, applies sensible defaults, and has proper type annotations and docstring per coding guidelines.


177-207: LGTM - Well-structured file configuration parser.

The function correctly defaults enabled to True for file logging (different from stdout's False default), provides appropriate fallback names based on level, and includes all necessary configuration keys.

abses/utils/analysis.py (1)

39-129: LGTM - Well-designed base analyzer class.

_BaseAnalyzer provides a clean foundation with proper property setters for validation, good use of cached_property for subdir, and appropriate docstrings following PEP 257.

abses/utils/log_config.py (3)

227-268: LGTM - Well-implemented log file path determination.

The determine_log_file_path function correctly handles all three logging modes (once, separate, merge) with appropriate fallback behavior. The docstring clearly documents the expected behavior for each mode.


41-42: Good use of sentinel pattern for optional parameter.

Using a sentinel object (_LOG_FILE_PATH_DEFAULT) to distinguish between "not provided" and "explicitly None" is a clean pattern that avoids the common Python pitfall of using mutable defaults.


410-415: No action needed. The code correctly isolates experiment-level logging without interference.

Lines 410-415 set propagate=False on parent loggers without clearing handlers, which is the correct approach. The experiment logger (EXP_LOGGER_NAME = "abses.core.experiment") independently configures itself in exp_logging.py by clearing handlers and setting up its own, so the redundant propagate=False setting here is harmless. Tests confirm experiment logging works as expected.

Comment thread abses/core/experiment.py Outdated
Comment thread abses/core/experiment.py Outdated
Comment thread abses/core/model.py
Comment thread abses/utils/analysis.py Outdated
Comment thread abses/utils/analysis.py
Comment thread abses/utils/log_config.py
Comment thread abses/utils/log_parser.py
Comment thread abses/utils/log_parser.py Outdated
Comment on lines +52 to +136
def test_get_log_mode_default(self):
"""测试获取默认日志模式"""
cfg = {}
assert get_log_mode(cfg) == "once"

def test_get_log_mode_from_config(self):
"""测试从配置中获取日志模式"""
cfg = {"log": {"mode": "separate"}}
assert get_log_mode(cfg) == "separate"

cfg = {"log": {"mode": "merge"}}
assert get_log_mode(cfg) == "merge"

def test_get_stdout_config_enabled(self):
"""测试获取启用的 stdout 配置"""
cfg = {
"log": {
"run": {
"stdout": {
"enabled": True,
"level": "DEBUG",
"format": "[%(levelname)s] %(message)s",
"datefmt": "%Y-%m-%d",
}
}
}
}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg["enabled"] is True
assert stdout_cfg["level"] == "DEBUG"
assert stdout_cfg["format"] == "[%(levelname)s] %(message)s"
assert stdout_cfg["datefmt"] == "%Y-%m-%d"

def test_get_stdout_config_disabled(self):
"""测试获取禁用的 stdout 配置"""
cfg = {"log": {"run": {"stdout": {"enabled": False}}}}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg == {}

def test_get_file_config_enabled(self):
"""测试获取启用的文件配置"""
cfg = {
"log": {
"run": {
"file": {
"enabled": True,
"name": "test_model",
"level": "WARNING",
"format": "[%(levelname)s] %(message)s",
"rotation": "1 day",
"retention": "10 days",
}
}
}
}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["enabled"] is True
assert file_cfg["name"] == "test_model"
assert file_cfg["level"] == "WARNING"
assert file_cfg["rotation"] == "1 day"
assert file_cfg["retention"] == "10 days"

def test_get_file_config_defaults(self):
"""测试文件配置的默认值"""
cfg = {"log": {"run": {"file": {"enabled": True}}}}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["name"] == "model" # Default name for run
assert file_cfg["level"] == DEFAULT_LEVEL
assert file_cfg["format"] == DEFAULT_FORMAT
assert file_cfg["datefmt"] == DEFAULT_DATEFMT

def test_get_mesa_config(self):
"""测试获取 Mesa 配置"""
cfg = {"log": {"run": {"mesa": {"level": "DEBUG", "format": "%(message)s"}}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] == "DEBUG"
assert mesa_cfg["format"] == "%(message)s"

def test_get_mesa_config_defaults(self):
"""测试 Mesa 配置的默认值"""
cfg = {"log": {"run": {}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] is None
assert mesa_cfg["format"] is None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add return type annotations to all test methods.

According to the coding guidelines, all functions and methods in Python files must have explicit return types, including None for test methods. Currently, test methods in TestLogParser are missing return type annotations.

🔎 Proposed improvement
-    def test_get_log_mode_default(self):
+    def test_get_log_mode_default(self) -> None:
         """测试获取默认日志模式"""
         cfg = {}
         assert get_log_mode(cfg) == "once"
 
-    def test_get_log_mode_from_config(self):
+    def test_get_log_mode_from_config(self) -> None:
         """测试从配置中获取日志模式"""
         cfg = {"log": {"mode": "separate"}}
         assert get_log_mode(cfg) == "separate"

Apply this pattern to all test methods in the file (lines 52-557).

As per coding guidelines, explicit return types are required for all functions and methods.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_get_log_mode_default(self):
"""测试获取默认日志模式"""
cfg = {}
assert get_log_mode(cfg) == "once"
def test_get_log_mode_from_config(self):
"""测试从配置中获取日志模式"""
cfg = {"log": {"mode": "separate"}}
assert get_log_mode(cfg) == "separate"
cfg = {"log": {"mode": "merge"}}
assert get_log_mode(cfg) == "merge"
def test_get_stdout_config_enabled(self):
"""测试获取启用的 stdout 配置"""
cfg = {
"log": {
"run": {
"stdout": {
"enabled": True,
"level": "DEBUG",
"format": "[%(levelname)s] %(message)s",
"datefmt": "%Y-%m-%d",
}
}
}
}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg["enabled"] is True
assert stdout_cfg["level"] == "DEBUG"
assert stdout_cfg["format"] == "[%(levelname)s] %(message)s"
assert stdout_cfg["datefmt"] == "%Y-%m-%d"
def test_get_stdout_config_disabled(self):
"""测试获取禁用的 stdout 配置"""
cfg = {"log": {"run": {"stdout": {"enabled": False}}}}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg == {}
def test_get_file_config_enabled(self):
"""测试获取启用的文件配置"""
cfg = {
"log": {
"run": {
"file": {
"enabled": True,
"name": "test_model",
"level": "WARNING",
"format": "[%(levelname)s] %(message)s",
"rotation": "1 day",
"retention": "10 days",
}
}
}
}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["enabled"] is True
assert file_cfg["name"] == "test_model"
assert file_cfg["level"] == "WARNING"
assert file_cfg["rotation"] == "1 day"
assert file_cfg["retention"] == "10 days"
def test_get_file_config_defaults(self):
"""测试文件配置的默认值"""
cfg = {"log": {"run": {"file": {"enabled": True}}}}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["name"] == "model" # Default name for run
assert file_cfg["level"] == DEFAULT_LEVEL
assert file_cfg["format"] == DEFAULT_FORMAT
assert file_cfg["datefmt"] == DEFAULT_DATEFMT
def test_get_mesa_config(self):
"""测试获取 Mesa 配置"""
cfg = {"log": {"run": {"mesa": {"level": "DEBUG", "format": "%(message)s"}}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] == "DEBUG"
assert mesa_cfg["format"] == "%(message)s"
def test_get_mesa_config_defaults(self):
"""测试 Mesa 配置的默认值"""
cfg = {"log": {"run": {}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] is None
assert mesa_cfg["format"] is None
def test_get_log_mode_default(self) -> None:
"""测试获取默认日志模式"""
cfg = {}
assert get_log_mode(cfg) == "once"
def test_get_log_mode_from_config(self) -> None:
"""测试从配置中获取日志模式"""
cfg = {"log": {"mode": "separate"}}
assert get_log_mode(cfg) == "separate"
cfg = {"log": {"mode": "merge"}}
assert get_log_mode(cfg) == "merge"
def test_get_stdout_config_enabled(self) -> None:
"""测试获取启用的 stdout 配置"""
cfg = {
"log": {
"run": {
"stdout": {
"enabled": True,
"level": "DEBUG",
"format": "[%(levelname)s] %(message)s",
"datefmt": "%Y-%m-%d",
}
}
}
}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg["enabled"] is True
assert stdout_cfg["level"] == "DEBUG"
assert stdout_cfg["format"] == "[%(levelname)s] %(message)s"
assert stdout_cfg["datefmt"] == "%Y-%m-%d"
def test_get_stdout_config_disabled(self) -> None:
"""测试获取禁用的 stdout 配置"""
cfg = {"log": {"run": {"stdout": {"enabled": False}}}}
stdout_cfg = get_stdout_config(cfg, "run")
assert stdout_cfg == {}
def test_get_file_config_enabled(self) -> None:
"""测试获取启用的文件配置"""
cfg = {
"log": {
"run": {
"file": {
"enabled": True,
"name": "test_model",
"level": "WARNING",
"format": "[%(levelname)s] %(message)s",
"rotation": "1 day",
"retention": "10 days",
}
}
}
}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["enabled"] is True
assert file_cfg["name"] == "test_model"
assert file_cfg["level"] == "WARNING"
assert file_cfg["rotation"] == "1 day"
assert file_cfg["retention"] == "10 days"
def test_get_file_config_defaults(self) -> None:
"""测试文件配置的默认值"""
cfg = {"log": {"run": {"file": {"enabled": True}}}}
file_cfg = get_file_config(cfg, "run")
assert file_cfg["name"] == "model" # Default name for run
assert file_cfg["level"] == DEFAULT_LEVEL
assert file_cfg["format"] == DEFAULT_FORMAT
assert file_cfg["datefmt"] == DEFAULT_DATEFMT
def test_get_mesa_config(self) -> None:
"""测试获取 Mesa 配置"""
cfg = {"log": {"run": {"mesa": {"level": "DEBUG", "format": "%(message)s"}}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] == "DEBUG"
assert mesa_cfg["format"] == "%(message)s"
def test_get_mesa_config_defaults(self) -> None:
"""测试 Mesa 配置的默认值"""
cfg = {"log": {"run": {}}}
mesa_cfg = get_mesa_config(cfg, "run")
assert mesa_cfg["level"] is None
assert mesa_cfg["format"] is None
🤖 Prompt for AI Agents
In @tests/utils/test_logging.py around lines 52 - 136, All test methods in
TestLogParser (e.g., test_get_log_mode_default, test_get_log_mode_from_config,
test_get_stdout_config_enabled, test_get_stdout_config_disabled,
test_get_file_config_enabled, test_get_file_config_defaults,
test_get_mesa_config, test_get_mesa_config_defaults) lack explicit return type
annotations; update each method signature to include the explicit return type
"-> None" (for example: def test_get_log_mode_default(self) -> None:) and apply
the same change to every other test method in the file.

This commit modifies the pre-commit configuration to check the entire `abses/` directory, aligning local checks with CI behavior. Additionally, it specifies the Python interpreter path for the `py312` test environment in `tox.ini` to avoid conflicts with local installations. Changes in the `experiment.py` file enhance configuration handling by creating a copy of the input dictionary before modification, ensuring the original configuration remains unchanged. Other updates include improvements to logging and analysis utilities for better functionality and compatibility.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
abses/utils/log_config.py (2)

214-223: Missing file_datefmt parameter in file handler creation.

The function accepts file_datefmt parameter (line 175) but doesn't pass it to create_file_handler. This is the same issue that was previously fixed for console_datefmt.

🔎 Proposed fix
     # Add file handler
     if file_path:
         handler = create_file_handler(
             filepath=file_path,
             level=file_level or level,
             fmt=file_format or DEFAULT_FORMAT,
+            datefmt=file_datefmt or DEFAULT_DATEFMT,
             rotation=rotation,
             retention=retention,
         )
         logger.addHandler(handler)

298-309: Handler formatters are being replaced, not cloned.

When handlers are provided, the code modifies the formatter on each handler directly (lines 300-302). This mutates the original handlers, which could cause unexpected side effects if those handlers are shared with other loggers.

🔎 Proposed fix

Consider creating new handlers with the desired format instead of mutating existing ones, or document that handlers are modified in-place:

     if handlers:
-        # Apply format to existing handlers
+        # Note: This modifies handlers in-place. Callers should pass copies if needed.
         formatter = logging.Formatter(format_str, datefmt=DATE_FORMAT)
         for handler in handlers:
             handler.setFormatter(formatter)

Or clone handlers:

     if handlers:
         formatter = logging.Formatter(format_str, datefmt=DATE_FORMAT)
         # Clone handlers to avoid mutating originals
         handlers = [copy.copy(h) for h in handlers]
         for handler in handlers:
             handler.setFormatter(formatter)
🤖 Fix all issues with AI Agents
In @abses/utils/analysis.py:
- Around line 181-191: The data property getter currently returns self._data
directly which can raise a vague AttributeError if _data hasn't been set; update
the data property (the @property def data) to check for the presence of
self._data (e.g., using hasattr or try/except AttributeError) and raise a clear
AttributeError with a descriptive message like "data has not been loaded; call
load_data(...) first" before returning the pd.DataFrame to match the docstring.

In @tests/utils/test_logging.py:
- Around line 49-56: All test methods (e.g.,
TestLogParser.test_get_log_mode_default) are missing explicit return type
annotations; update every test method in TestLogParser, TestLogConfig,
TestExpLogging, TestModelLogging, and TestLoggingIntegration to include the
return type -> None on their definitions so every function has an explicit
typing annotation per guidelines.
- Around line 19-27: Add a static-only import for TYPE_CHECKING and move any
test-fixture-specific imports into a conditional block so they are only imported
for type checking; specifically, add "from typing import TYPE_CHECKING" to the
top of tests/utils/test_logging.py and wrap pytest fixture imports (e.g., any
uses of pytest fixtures or fixture types) inside "if TYPE_CHECKING:" so runtime
import overhead is avoided while keeping type hints for tools.
🧹 Nitpick comments (6)
tests/utils/test_logging.py (1)

543-548: Add docstrings to inner TestModel class.

The TestModel class defined inside the test lacks a docstring. While this is a minimal test fixture, adding a brief docstring improves clarity.

🔎 Proposed fix
             class TestModel(MainModel):
+                """Minimal test model for logging integration tests."""
+
                 def setup(self):
+                    """Setup method (no-op for testing)."""
                     pass
 
                 def step(self):
+                    """Step method (no-op for testing)."""
                     pass
abses/utils/log_parser.py (1)

21-22: Empty TYPE_CHECKING block is unused.

The TYPE_CHECKING import and block are present but empty. If no type-only imports are needed, this can be removed to reduce noise.

🔎 Proposed fix
-from typing import TYPE_CHECKING, Any, Dict
+from typing import Any, Dict
 
 from omegaconf import DictConfig, OmegaConf
-
-if TYPE_CHECKING:
-    pass
abses/utils/analysis.py (1)

31-32: Empty TYPE_CHECKING block is unused.

Similar to log_parser.py, the TYPE_CHECKING import and block are present but empty.

🔎 Proposed fix
-from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List
+from typing import Any, Callable, Dict, Generator, List
...
-if TYPE_CHECKING:
-    pass
abses/utils/log_config.py (1)

22-23: Empty TYPE_CHECKING block is unused.

The TYPE_CHECKING import and block are present but empty across multiple files.

abses/core/experiment.py (2)

483-498: Empty pass blocks suggest incomplete implementation.

Lines 483-485 and 495-498 contain empty pass blocks with comments like "Note: This will be logged in the model's logger setup". These appear to be placeholders. Consider removing them or adding TODO comments if future work is planned.

🔎 Proposed fix
-        # For merge mode, log separator before first repeat
-        if logging_mode == "merge" and repeats > 1:
-            # Note: This will be logged in the model's logger setup
-            pass
+        # For merge mode, separators are logged in the model's logger setup

And similarly for lines 495-498:

-                # Log separator for merge mode
-                if logging_mode == "merge" and repeat_id > 1:
-                    # Note: Separator will be logged in model setup
-                    pass

401-421: Move determine_log_file_path import to module level.

The determine_log_file_path function is imported inside _get_log_file_path (line 414), adding runtime overhead on each call. There is no circular dependency, so this import can be safely moved to the module level.

🔎 Proposed fix
 from abses.utils.exp_logging import EXP_LOGGER_NAME, setup_exp_logger
+from abses.utils.log_config import determine_log_file_path
 from abses.utils.log_parser import get_file_config, get_log_mode
 ...
     def _get_log_file_path(
         self, log_name: str, repeat_id: int, logging_mode: str
     ) -> Optional[Path]:
         ...
-        from abses.utils.log_config import determine_log_file_path
-
         return determine_log_file_path(
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b38b438 and 8e115f6.

📒 Files selected for processing (8)
  • .pre-commit-config.yaml
  • abses/core/experiment.py
  • abses/core/model.py
  • abses/utils/analysis.py
  • abses/utils/log_config.py
  • abses/utils/log_parser.py
  • tests/utils/test_logging.py
  • tox.ini
🚧 Files skipped from review as they are similar to previous changes (2)
  • abses/core/model.py
  • tox.ini
🧰 Additional context used
📓 Path-based instructions (3)
tests/**/@(test_*.py|*_test.py)

📄 CodeRabbit inference engine (.cursorrules)

tests/**/@(test_*.py|*_test.py): Use pytest for comprehensive testing in Python test files
In test files, import TYPE_CHECKING and conditionally import pytest fixtures and plugins (_pytest.capture.CaptureFixture, _pytest.fixtures.FixtureRequest, _pytest.logging.LogCaptureFixture, _pytest.monkeypatch.MonkeyPatch, pytest_mock.plugin.MockerFixture)
All tests should have typing annotations and contain docstrings

Files:

  • tests/utils/test_logging.py
**/*.py

📄 CodeRabbit inference engine (.cursorrules)

**/*.py: Write detailed documentation using docstrings in all Python functions and classes, following PEP 257 conventions
Add typing annotations to each function or class in Python files, including explicit return types (including None where appropriate)

Files:

  • tests/utils/test_logging.py
  • abses/core/experiment.py
  • abses/utils/log_parser.py
  • abses/utils/analysis.py
  • abses/utils/log_config.py
tests/**/*.py

📄 CodeRabbit inference engine (.cursorrules)

Place all tests under ./tests directory and use pytest or pytest plugins exclusively

Files:

  • tests/utils/test_logging.py
🧠 Learnings (3)
📚 Learning: 2025-12-12T14:30:01.950Z
Learnt from: CR
Repo: SongshGeoLab/ABSESpy PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-12T14:30:01.950Z
Learning: Applies to tests/**/@(test_*.py|*_test.py) : In test files, import TYPE_CHECKING and conditionally import pytest fixtures and plugins (_pytest.capture.CaptureFixture, _pytest.fixtures.FixtureRequest, _pytest.logging.LogCaptureFixture, _pytest.monkeypatch.MonkeyPatch, pytest_mock.plugin.MockerFixture)

Applied to files:

  • tests/utils/test_logging.py
📚 Learning: 2025-12-12T14:30:01.950Z
Learnt from: CR
Repo: SongshGeoLab/ABSESpy PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-12T14:30:01.950Z
Learning: Applies to tests/**/@(test_*.py|*_test.py) : All tests should have typing annotations and contain docstrings

Applied to files:

  • tests/utils/test_logging.py
📚 Learning: 2025-12-12T14:30:01.950Z
Learnt from: CR
Repo: SongshGeoLab/ABSESpy PR: 0
File: .cursorrules:0-0
Timestamp: 2025-12-12T14:30:01.950Z
Learning: Applies to **/*.py : Add typing annotations to each function or class in Python files, including explicit return types (including None where appropriate)

Applied to files:

  • tests/utils/test_logging.py
🧬 Code graph analysis (4)
tests/utils/test_logging.py (8)
abses/core/model.py (5)
  • MainModel (73-567)
  • outpath (169-178)
  • name (159-166)
  • setup (535-536)
  • exp (408-410)
abses/core/experiment.py (4)
  • Experiment (143-606)
  • cfg (182-184)
  • cfg (187-200)
  • outpath (245-249)
abses/utils/exp_logging.py (1)
  • setup_exp_logger (42-183)
abses/utils/log_config.py (4)
  • create_console_handler (79-100)
  • create_file_handler (103-162)
  • determine_log_file_path (228-269)
  • info (475-477)
abses/utils/log_parser.py (4)
  • get_file_config (187-217)
  • get_log_mode (30-84)
  • get_mesa_config (220-240)
  • get_stdout_config (159-184)
abses/utils/logging.py (2)
  • setup_model_logger (102-179)
  • formatter (40-51)
abses/core/time_driver.py (1)
  • fmt (187-192)
abses/core/job_manager.py (1)
  • ExperimentManager (20-150)
abses/core/experiment.py (3)
abses/utils/exp_logging.py (1)
  • setup_exp_logger (42-183)
abses/utils/log_parser.py (2)
  • get_file_config (187-217)
  • get_log_mode (30-84)
abses/utils/log_config.py (2)
  • determine_log_file_path (228-269)
  • info (475-477)
abses/utils/log_parser.py (2)
abses/core/experiment.py (2)
  • cfg (182-184)
  • cfg (187-200)
abses/utils/analysis.py (2)
  • select (117-129)
  • select (325-334)
abses/utils/analysis.py (1)
abses/core/experiment.py (3)
  • _load_hydra_cfg (311-339)
  • overrides (252-261)
  • overrides (264-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Tests 3.13 on ubuntu-latest
  • GitHub Check: Tests 3.12 on macos-latest
  • GitHub Check: Tests 3.11 on macos-latest
  • GitHub Check: Tests 3.13 on windows-latest
  • GitHub Check: Tests 3.12 on windows-latest
  • GitHub Check: Tests 3.12 on ubuntu-latest
  • GitHub Check: Tests 3.13 on macos-latest
  • GitHub Check: Tests 3.11 on windows-latest
  • GitHub Check: Tests 3.11 on ubuntu-latest
  • GitHub Check: nbmake
  • GitHub Check: nbmake
🔇 Additional comments (10)
.pre-commit-config.yaml (1)

29-31: LGTM! Mypy configuration now matches CI behavior.

The change to check the entire abses/ directory ensures consistency between local pre-commit hooks and CI, which is valuable for catching type errors early. Note that this will run mypy on all files in abses/ on every commit rather than just changed files, trading incremental speed for consistency.

tests/utils/test_logging.py (1)

515-576: LGTM: Comprehensive integration test with proper cleanup.

The test_experiment_with_logging test properly:

  • Saves and restores the singleton instance to avoid test pollution
  • Uses try/finally to ensure handler cleanup (important for Windows)
  • Verifies log file creation with helpful error messages

The singleton manipulation (lines 521-522) is acceptable for test isolation purposes.

abses/utils/log_parser.py (2)

30-84: LGTM: Backward compatibility logic correctly distinguishes explicit vs default values.

The implementation now properly:

  1. Uses OmegaConf.select(cfg, "log.mode") without a default to detect if the key exists (line 42)
  2. Only falls back to exp.logging.mode when log.mode is not explicitly set (lines 43-51)
  3. Applies the same logic to plain dicts by checking if "mode" in log_section (line 59)

This addresses the previously flagged issue about being unable to distinguish explicit log.mode="once" from a missing value.


107-147: LGTM: Fixed old_log type check.

Line 115 now correctly guards against non-dict truthy values with if not old_log or not isinstance(old_log, dict):. This prevents AttributeError when old_log is a truthy non-dict (e.g., a string).

abses/utils/analysis.py (2)

433-440: LGTM: Generator exhaustion bug fixed with cached property.

The _results_list cached property correctly caches the generator results, preventing the exhaustion issue when diff_runs and agg_data need to iterate multiple times. This addresses the previously flagged critical bug.


304-308: IndexError is a legitimate exception for pd.read_csv with index_col and the exception handling is appropriate.

pd.read_csv(path, index_col=0) can raise IndexError when the specified column index is out of range or when CSV parsing issues result in fewer columns than expected. The current exception handling pattern—attempting to read with an index column and falling back to reading without one—is a reasonable defensive approach for this scenario.

abses/utils/log_config.py (1)

206-212: LGTM: console_datefmt is now passed to handler.

The previously flagged issue about console_datefmt not being passed to create_console_handler has been fixed. Line 210 now correctly passes datefmt=console_datefmt or DEFAULT_DATEFMT.

abses/core/experiment.py (3)

160-174: LGTM: Input dict mutation issue fixed.

The previously flagged issue about mutating the input cfg dict is now fixed. Line 172 creates a shallow copy with cfg_copy = cfg.copy() before modifying outpath, ensuring the original input is not mutated.


423-451: LGTM: Logging mode is now dynamic.

The previously hardcoded "Logging mode: separate" has been replaced with f"Logging mode: {logging_mode}" on line 449, correctly reflecting the actual configuration.


505-511: Intentional use of print() for stdout-only output.

The use of print() instead of logger on line 511 is documented as intentional to avoid writing to model run log files. This is a valid design choice for user-facing progress information.

Comment thread abses/utils/analysis.py
Comment on lines +181 to +191
@property
def data(self) -> pd.DataFrame:
"""Raw data loaded from CSV or datacollector output.

Returns:
DataFrame containing the raw data.

Raises:
AttributeError: If data has not been loaded yet.
"""
return self._data
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing AttributeError handling in data property getter.

The docstring mentions AttributeError: If data has not been loaded yet, but the getter simply returns self._data without handling the case where _data doesn't exist. This would raise an AttributeError but with an unclear message.

🔎 Proposed fix
     @property
     def data(self) -> pd.DataFrame:
         """Raw data loaded from CSV or datacollector output.

         Returns:
             DataFrame containing the raw data.

         Raises:
             AttributeError: If data has not been loaded yet.
         """
+        if not hasattr(self, "_data"):
+            raise AttributeError("Data has not been loaded yet. Call read_data() first.")
         return self._data
🤖 Prompt for AI Agents
In @abses/utils/analysis.py around lines 181 - 191, The data property getter
currently returns self._data directly which can raise a vague AttributeError if
_data hasn't been set; update the data property (the @property def data) to
check for the presence of self._data (e.g., using hasattr or try/except
AttributeError) and raise a clear AttributeError with a descriptive message like
"data has not been loaded; call load_data(...) first" before returning the
pd.DataFrame to match the docstring.

Comment on lines +19 to +27
from __future__ import annotations

import logging
import tempfile
from pathlib import Path

import pytest
from omegaconf import OmegaConf

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add TYPE_CHECKING import and conditional imports for pytest fixtures.

As per coding guidelines, test files should import TYPE_CHECKING and conditionally import pytest fixtures. This pattern improves type checking without runtime overhead.

🔎 Proposed fix
 from __future__ import annotations
 
 import logging
 import tempfile
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 import pytest
 from omegaconf import OmegaConf
+
+if TYPE_CHECKING:
+    from _pytest.tmpdir import TempPathFactory
🤖 Prompt for AI Agents
In @tests/utils/test_logging.py around lines 19 - 27, Add a static-only import
for TYPE_CHECKING and move any test-fixture-specific imports into a conditional
block so they are only imported for type checking; specifically, add "from
typing import TYPE_CHECKING" to the top of tests/utils/test_logging.py and wrap
pytest fixture imports (e.g., any uses of pytest fixtures or fixture types)
inside "if TYPE_CHECKING:" so runtime import overhead is avoided while keeping
type hints for tools.

Comment on lines +49 to +56
class TestLogParser:
"""测试配置解析功能"""

def test_get_log_mode_default(self):
"""测试获取默认日志模式"""
cfg = {}
assert get_log_mode(cfg) == "once"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add return type annotations to all test methods.

All test methods in the file are missing explicit -> None return type annotations. As per coding guidelines, all functions must have typing annotations including explicit return types.

🔎 Proposed fix pattern

Apply this pattern to all test methods in the file:

-    def test_get_log_mode_default(self):
+    def test_get_log_mode_default(self) -> None:
         """测试获取默认日志模式"""

This applies to all ~30 test methods across TestLogParser, TestLogConfig, TestExpLogging, TestModelLogging, and TestLoggingIntegration classes.

🤖 Prompt for AI Agents
In @tests/utils/test_logging.py around lines 49 - 56, All test methods (e.g.,
TestLogParser.test_get_log_mode_default) are missing explicit return type
annotations; update every test method in TestLogParser, TestLogConfig,
TestExpLogging, TestModelLogging, and TestLoggingIntegration to include the
return type -> None on their definitions so every function has an explicit
typing annotation per guidelines.

@SongshGeo SongshGeo merged commit fa6de88 into master Jan 6, 2026
13 of 14 checks passed
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.

1 participant