Files
beta_shape_pnpi/beta_spectrum/logging_utils.py
T
unzhakov 3749220efc Fix pyright type errors and correct paceENSDF Q-value extraction
- nuclear_data.py: Extract Q-value from ground state branch state[7] instead of
  halflife_data (was reading half-life seconds as Q-value)
- nuclear_data.py: Use ground state branch for forbiddenness code, with fallback
- nuclear_data.py: Add None guards for beta_data and pairs
- spectrum.py: Pass Z=config.Z_daughter, A=config.A_number to RadiativeCorrection
- phase_space.py: Add __init__ debug logging (W0, transition_type, m_e, m_nu)
- phase_space.py: Add output range logging in __call__
- phase_space.py: Fix possibly unbound forbid_factor variable
- logging_utils.py: Fix broken get_git_short_hash indentation
- cli.py: Add explicit SpectrumConfig annotation to fix pyright type identity
- cw_extractor.py: Fix T_to_W ndarray→float conversions (6 locations)
- cw_extractor.py: Fix FermiFunction() calls — was passing Z as __call__ arg
- fitter.py: Add explicit dict type annotation for least_squares kwargs
2026-05-05 12:43:43 +03:00

165 lines
4.5 KiB
Python

"""
Logging infrastructure for beta-spectrum package.
Provides a centralized logging setup with:
- Configurable verbosity levels (WARNING, INFO, DEBUG)
- Optional file output
- Consistent formatting across all modules
- Opt-in design: logging is disabled by default for library use,
enabled via setup_logging() for CLI/workflow use
Usage (library):
# No logging by default — silent, zero overhead
Usage (CLI):
from beta_spectrum.logging_utils import setup_logging, get_logger
setup_logging(level="INFO", log_file="calc.log")
logger = get_logger("spectrum")
logger.info("Starting calculation...")
"""
from __future__ import annotations
import logging
import sys
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class LoggingConfig:
"""Configuration for logging setup."""
level: str = "WARNING"
"""Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'."""
log_file: Optional[str] = None
"""Path to log file. If None, only stdout is used."""
log_format: str = "[%(asctime)s] [%(levelname)-8s] [%(name)s] %(message)s"
"""Log message format string."""
date_format: str = "%H:%M:%S"
"""Date/time format for timestamps."""
_handlers_configured: bool = field(default=False, repr=False)
"""Internal flag to prevent double configuration."""
def setup_logging(config: Optional[LoggingConfig] = None) -> logging.Logger:
"""
Configure the root logger for the beta_spectrum package.
This is a one-shot setup — calling it multiple times with different
configs will not reconfigure existing handlers. Call setup_logging()
once at the start of your application.
Parameters
----------
config : LoggingConfig, optional
Logging configuration. Uses defaults if None.
Returns
-------
logging.Logger
The root logger for the beta_spectrum package.
"""
if config is None:
config = LoggingConfig()
logger = logging.getLogger("beta_spectrum")
# Prevent double configuration
if config._handlers_configured:
return logger
logger.setLevel(getattr(logging, config.level.upper(), logging.WARNING))
# Remove any existing handlers to avoid duplicates on re-init
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close()
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, config.level.upper(), logging.WARNING))
console_handler.setFormatter(
logging.Formatter(config.log_format, config.date_format)
)
logger.addHandler(console_handler)
# File handler (optional)
if config.log_file:
file_handler = logging.FileHandler(config.log_file, mode="a")
file_handler.setLevel(logging.DEBUG) # Always log everything to file
file_handler.setFormatter(
logging.Formatter(config.log_format, config.date_format)
)
logger.addHandler(file_handler)
config._handlers_configured = True
return logger
def get_logger(name: str) -> logging.Logger:
"""
Get a named logger for the beta_spectrum package.
Parameters
----------
name : str
Logger name (e.g., "spectrum", "fitter", "nuclear_data").
Returns
-------
logging.Logger
A logger named "beta_spectrum.{name}".
Examples
--------
>>> logger = get_logger("spectrum")
>>> logger.info("Creating spectrum components")
"""
return logging.getLogger(f"beta_spectrum.{name}")
def get_git_short_hash(length: int = 7) -> str:
"""
Get the short git commit hash.
Parameters
----------
length : int
Number of characters to return (default: 7).
Returns
-------
str
Short git hash, or "unknown" if not in a git repo.
"""
import subprocess
try:
result = subprocess.run(
["git", "rev-parse", f"--short={length}", "HEAD"],
capture_output=True,
text=True,
check=True,
cwd=_find_git_root(),
)
return result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return "unknown"
def _find_git_root() -> str:
"""Find the root directory of the git repository."""
from pathlib import Path
current = Path(__file__).resolve()
while current != current.parent:
if (current / ".git").exists():
return str(current)
current = current.parent
return str(current)