Skip to content

Commit 29bec07

Browse files
authored
Merge pull request #44 from LOAMRI/develop
Develop
2 parents 2a94ac8 + 70d1d34 commit 29bec07

21 files changed

+1508
-56
lines changed

asltk/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
BIDS_IMAGE_FORMATS = ('.nii', '.nii.gz')
22
AVAILABLE_IMAGE_FORMATS = ('.nii', '.nii.gz', '.mha', '.nrrd')
3+
4+
# Import logging functionality for easy access
5+
from .logging_config import configure_for_scripts, get_logger, setup_logging
6+
7+
# Set up default logging (INFO level, console only)
8+
setup_logging()
9+
10+
__all__ = ['setup_logging', 'get_logger', 'configure_for_scripts']

asltk/asldata.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import numpy as np
44
import SimpleITK as sitk
55

6+
from asltk.logging_config import get_logger, log_data_info, log_function_call
67
from asltk.utils import load_image
78

89

@@ -60,11 +61,22 @@ def __init__(
6061
'dw': None,
6162
}
6263

64+
logger = get_logger('asldata')
65+
logger.info('Creating ASLData object')
66+
6367
if kwargs.get('pcasl') is not None:
64-
self._asl_image = load_image(kwargs.get('pcasl'))
68+
pcasl_path = kwargs.get('pcasl')
69+
logger.info(f'Loading ASL image from: {pcasl_path}')
70+
self._asl_image = load_image(pcasl_path)
71+
if self._asl_image is not None:
72+
log_data_info('ASL image', self._asl_image.shape, pcasl_path)
6573

6674
if kwargs.get('m0') is not None:
67-
self._m0_image = load_image(kwargs.get('m0'))
75+
m0_path = kwargs.get('m0')
76+
logger.info(f'Loading M0 image from: {m0_path}')
77+
self._m0_image = load_image(m0_path)
78+
if self._m0_image is not None:
79+
log_data_info('M0 image', self._m0_image.shape, m0_path)
6880

6981
self._parameters['ld'] = (
7082
[] if kwargs.get('ld_values') is None else kwargs.get('ld_values')
@@ -74,13 +86,26 @@ def __init__(
7486
if kwargs.get('pld_values') is None
7587
else kwargs.get('pld_values')
7688
)
89+
90+
if self._parameters['ld'] or self._parameters['pld']:
91+
logger.info(
92+
f"ASL timing parameters - LD: {self._parameters['ld']}, PLD: {self._parameters['pld']}"
93+
)
94+
7795
self._check_ld_pld_sizes(
7896
self._parameters['ld'], self._parameters['pld']
7997
)
8098
if kwargs.get('te_values'):
81-
self._parameters['te'] = kwargs.get('te_values')
99+
te_values = kwargs.get('te_values')
100+
self._parameters['te'] = te_values
101+
logger.info(f'Multi-TE parameters set: {te_values}')
102+
82103
if kwargs.get('dw_values'):
83-
self._parameters['dw'] = kwargs.get('dw_values')
104+
dw_values = kwargs.get('dw_values')
105+
self._parameters['dw'] = dw_values
106+
logger.info(f'Diffusion-weighted parameters set: {dw_values}')
107+
108+
logger.debug('ASLData object created successfully')
84109

85110
def set_image(self, image, spec: str):
86111
"""Insert a image necessary to define de ASL data processing.
@@ -222,7 +247,12 @@ def _check_input_parameter(self, values, param_type):
222247
)
223248

224249
def _check_ld_pld_sizes(self, ld, pld):
250+
logger = get_logger('asldata')
225251
if len(ld) != len(pld):
226-
raise ValueError(
227-
f'LD and PLD must have the same array size. LD size is {len(ld)} and PLD size is {len(pld)}'
252+
error_msg = f'LD and PLD must have the same array size. LD size is {len(ld)} and PLD size is {len(pld)}'
253+
logger.error(error_msg)
254+
raise ValueError(error_msg)
255+
else:
256+
logger.debug(
257+
f'LD and PLD size validation passed: {len(ld)} elements each'
228258
)

asltk/logging_config.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""
2+
Centralized logging configuration for the ASLTK library.
3+
4+
This module provides a unified logging system with configurable levels,
5+
output formats, and destinations (console and file).
6+
"""
7+
8+
import logging
9+
import logging.handlers
10+
import os
11+
from typing import Optional, Union
12+
13+
# Default log format
14+
DEFAULT_LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15+
DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
16+
17+
# Package logger name
18+
PACKAGE_LOGGER_NAME = 'asltk'
19+
20+
21+
def get_logger(name: Optional[str] = None) -> logging.Logger:
22+
"""
23+
Get a logger instance for the ASLTK package.
24+
25+
Args:
26+
name: Logger name suffix. If None, returns the root package logger.
27+
28+
Returns:
29+
Logger instance configured for ASLTK.
30+
"""
31+
if name is None:
32+
logger_name = PACKAGE_LOGGER_NAME
33+
else:
34+
logger_name = f'{PACKAGE_LOGGER_NAME}.{name}'
35+
36+
return logging.getLogger(logger_name)
37+
38+
39+
def setup_logging(
40+
level: Union[str, int] = logging.INFO,
41+
console_output: bool = True,
42+
file_output: Optional[str] = None,
43+
log_format: Optional[str] = None,
44+
date_format: Optional[str] = None,
45+
) -> None:
46+
"""
47+
Configure logging for the ASLTK package.
48+
49+
Args:
50+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL or numeric value)
51+
console_output: Whether to output logs to console
52+
file_output: Path to log file. If None, no file logging is configured
53+
log_format: Custom log format string
54+
date_format: Custom date format string
55+
56+
Examples:
57+
Basic setup with INFO level to console:
58+
>>> setup_logging()
59+
60+
Setup with DEBUG level to both console and file:
61+
>>> setup_logging(level='DEBUG', file_output='asltk.log')
62+
63+
Setup with custom format:
64+
>>> setup_logging(log_format='%(levelname)s: %(message)s')
65+
"""
66+
# Convert string level to numeric if needed
67+
if isinstance(level, str):
68+
level = getattr(logging, level.upper())
69+
70+
# Use default formats if not provided
71+
if log_format is None:
72+
log_format = DEFAULT_LOG_FORMAT
73+
if date_format is None:
74+
date_format = DEFAULT_DATE_FORMAT
75+
76+
# Create formatter
77+
formatter = logging.Formatter(log_format, date_format)
78+
79+
# Get the root logger for the package
80+
logger = logging.getLogger(PACKAGE_LOGGER_NAME)
81+
logger.setLevel(level)
82+
83+
# Clear any existing handlers to avoid duplicates
84+
logger.handlers.clear()
85+
86+
# Add console handler if requested
87+
if console_output:
88+
console_handler = logging.StreamHandler()
89+
console_handler.setLevel(level)
90+
console_handler.setFormatter(formatter)
91+
logger.addHandler(console_handler)
92+
93+
# Add file handler if requested
94+
if file_output:
95+
# Ensure directory exists
96+
log_dir = os.path.dirname(file_output)
97+
if log_dir and not os.path.exists(log_dir):
98+
os.makedirs(log_dir, exist_ok=True)
99+
100+
file_handler = logging.FileHandler(file_output, mode='a')
101+
file_handler.setLevel(level)
102+
file_handler.setFormatter(formatter)
103+
logger.addHandler(file_handler)
104+
105+
# Prevent propagation to avoid duplicate messages (but allow for testing)
106+
if os.environ.get('PYTEST_CURRENT_TEST'):
107+
logger.propagate = True
108+
else:
109+
logger.propagate = False
110+
111+
112+
def configure_for_scripts(
113+
verbose: bool = False, log_file: Optional[str] = None
114+
) -> None:
115+
"""
116+
Convenience function to configure logging for command-line scripts.
117+
118+
Args:
119+
verbose: If True, sets DEBUG level; otherwise INFO level
120+
log_file: Optional log file path
121+
122+
Examples:
123+
Configure for verbose script execution:
124+
>>> configure_for_scripts(verbose=True)
125+
126+
Configure with log file:
127+
>>> configure_for_scripts(log_file='processing.log')
128+
"""
129+
level = logging.DEBUG if verbose else logging.INFO
130+
setup_logging(level=level, console_output=True, file_output=log_file)
131+
132+
133+
def log_function_call(func_name: str, **kwargs) -> None:
134+
"""
135+
Log a function call with its parameters.
136+
137+
Args:
138+
func_name: Name of the function being called
139+
**kwargs: Function parameters to log
140+
"""
141+
logger = get_logger()
142+
params = ', '.join(f'{k}={v}' for k, v in kwargs.items())
143+
logger.debug(f'Calling {func_name}({params})')
144+
145+
146+
def log_processing_step(step_name: str, details: Optional[str] = None) -> None:
147+
"""
148+
Log a processing step at INFO level.
149+
150+
Args:
151+
step_name: Name of the processing step
152+
details: Optional additional details
153+
"""
154+
logger = get_logger()
155+
message = f'Processing step: {step_name}'
156+
if details:
157+
message += f' - {details}'
158+
logger.info(message)
159+
160+
161+
def log_data_info(
162+
data_type: str, shape: tuple, path: Optional[str] = None
163+
) -> None:
164+
"""
165+
Log information about loaded data.
166+
167+
Args:
168+
data_type: Type of data (e.g., 'ASL image', 'M0 image', 'mask')
169+
shape: Shape of the data array
170+
path: Optional file path
171+
"""
172+
logger = get_logger()
173+
message = f'Loaded {data_type}: shape={shape}'
174+
if path:
175+
message += f', path={path}'
176+
logger.info(message)
177+
178+
179+
def log_warning_with_context(
180+
message: str, context: Optional[str] = None
181+
) -> None:
182+
"""
183+
Log a warning with optional context information.
184+
185+
Args:
186+
message: Warning message
187+
context: Optional context information
188+
"""
189+
logger = get_logger()
190+
full_message = message
191+
if context:
192+
full_message += f' (Context: {context})'
193+
logger.warning(full_message)
194+
195+
196+
def log_error_with_traceback(message: str, exc_info: bool = True) -> None:
197+
"""
198+
Log an error with traceback information.
199+
200+
Args:
201+
message: Error message
202+
exc_info: Whether to include exception traceback
203+
"""
204+
logger = get_logger()
205+
logger.error(message, exc_info=exc_info)

0 commit comments

Comments
 (0)