diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb9d621..57cee7a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,16 +2,16 @@ name: Tests and Coverage on: push: - branches: [ main, develop ] + branches: [ main, dev ] pull_request: - branches: [ main, develop ] + branches: [ main, dev ] jobs: test: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - name: Checkout code @@ -61,11 +61,3 @@ jobs: run: | pytest tests/ -v --cov=veta --cov-append --cov-report=xml --cov-report=term-missing -m "integration" - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false - diff --git a/.gitignore b/.gitignore index c067dd5..8559d60 100644 --- a/.gitignore +++ b/.gitignore @@ -116,5 +116,7 @@ __pycache__ *.!* *pid.txt .coverage* +coverage.xml *_pid.txt -rec_0001* \ No newline at end of file +rec_0001* +*.log \ No newline at end of file diff --git a/examples/logging_example.py b/examples/logging_example.py new file mode 100644 index 0000000..bd4e398 --- /dev/null +++ b/examples/logging_example.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Example script demonstrating the veta logging system. + +This script shows how to initialize and use the comprehensive logging +system throughout the veta package. +""" + +# Import veta components and logging +from veta import setup_logging, get_logger +from veta.survey import Survey +from veta.respondent import Respondent +from veta.item import Item + +def main(): + """Main function demonstrating veta logging capabilities.""" + + # Example 1: Basic logging setup with auto-generated log file + print("=== Example 1: Basic Logging Setup ===") + setup_logging( + level='INFO', + console_level='INFO', + file_level='DEBUG', + auto_generate_file=True + ) + + # Get a logger for this example + logger = get_logger('example') + logger.info("Starting veta logging demonstration") + + # Example 2: Create a survey and demonstrate logging + print("\n=== Example 2: Survey Operations with Logging ===") + survey = Survey() + logger.info("Created new survey") + + # Add some respondents + for i in range(3): + respondent = Respondent(userid=f"user_{i:03d}") + + # Add some items to each respondent + respondent.add_item("I feel happy today", "She seems sad") + respondent.add_item("I am excited", "He looks worried") + + survey.add_respondent(respondent) + + logger.info(f"Survey now has {len(survey.respondents)} respondents") + + # Example 3: Different log levels demonstration + print("\n=== Example 3: Different Log Levels ===") + logger.debug("This is a debug message - detailed information") + logger.info("This is an info message - general information") + logger.warning("This is a warning message - something needs attention") + logger.error("This is an error message - something went wrong") + + # Example 4: Module-specific loggers + print("\n=== Example 4: Module-specific Loggers ===") + item_logger = get_logger('item_processing') + respondent_logger = get_logger('respondent_analysis') + + item_logger.info("Processing individual items") + respondent_logger.info("Analyzing respondent data") + + # Example 5: Demonstrate error handling with logging + print("\n=== Example 5: Error Handling with Logging ===") + try: + # This will create a warning/error in the wordlist loading + item = Item("Test sentence") + # Attempting to score without wordlist should generate logs + logger.warning("Attempting operation that may fail...") + + except Exception as e: + logger.error(f"Operation failed: {str(e)}") + logger.debug("This would contain detailed stack trace information") + + logger.info("Veta logging demonstration completed") + print("\n=== Logging Demonstration Complete ===") + print("Check the generated log file for detailed debug information!") + +def demonstrate_custom_logging(): + """Demonstrate custom logging configuration.""" + print("\n=== Custom Logging Configuration Example ===") + + # Setup logging with custom file and levels + setup_logging( + level='DEBUG', + log_file='./custom_veta_analysis.log', + console_output=True, + file_level='DEBUG', + console_level='WARNING', # Only show warnings and errors in console + auto_generate_file=False + ) + + custom_logger = get_logger('custom_analysis') + + # These will appear in the log file but not console (due to console_level='WARNING') + custom_logger.debug("Detailed debug info - only in file") + custom_logger.info("General info - only in file") + + # This will appear in both console and file + custom_logger.warning("Warning message - appears in both console and file") + + print("Custom logging example complete - check 'custom_veta_analysis.log'") + +if __name__ == "__main__": + main() + demonstrate_custom_logging() diff --git a/requirements.txt b/requirements.txt index ff5f496..4b93d04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ requests>=2.25.0 seaborn>=0.11.0 scikit-learn>=1.0.0 scipy>=1.7.0 +colorama>=0.4.0 # Testing dependencies pytest>=7.0.0 diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..90fefce --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,166 @@ +""" +Test script for the veta logging system. +""" + +import pytest +import logging +import tempfile +import os +from pathlib import Path +from veta.logger import setup_logging, get_logger, VetaLogger + +class TestVetaLogging: + """Test cases for the veta logging system.""" + + def test_basic_logger_setup(self): + """Test basic logger initialization.""" + logger = get_logger('test') + assert isinstance(logger, logging.Logger) + assert logger.name == 'veta.test' + + def test_main_logger(self): + """Test getting the main logger.""" + logger = get_logger() + assert isinstance(logger, logging.Logger) + assert logger.name == 'veta' + + def test_custom_log_file(self): + """Test logging to a custom file.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as f: + log_file = f.name + + try: + setup_logging( + level='DEBUG', + log_file=log_file, + console_output=False, + auto_generate_file=False + ) + + logger = get_logger('test_file') + logger.info("Test message") + + # Check if file was created and contains our message + assert os.path.exists(log_file) + with open(log_file, 'r') as f: + content = f.read() + assert "Test message" in content + assert "test_file" in content + finally: + if os.path.exists(log_file): + os.unlink(log_file) + + def test_auto_generate_log_file(self): + """Test auto-generation of log files.""" + # Setup with auto-generate + setup_logging(auto_generate_file=True, console_output=False) + + logger = get_logger('test_auto') + logger.info("Auto-generated test message") + + # Check that the main veta logger has handlers (child loggers don't have direct handlers) + main_logger = get_logger() + assert len(main_logger.handlers) > 0 + + def test_different_log_levels(self): + """Test different logging levels.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as f: + log_file = f.name + + try: + setup_logging( + level='DEBUG', + log_file=log_file, + console_output=False, + file_level='DEBUG' + ) + + logger = get_logger('test_levels') + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + logger.error("Error message") + + # Check all messages are in the file + with open(log_file, 'r') as f: + content = f.read() + assert "Debug message" in content + assert "Info message" in content + assert "Warning message" in content + assert "Error message" in content + finally: + if os.path.exists(log_file): + os.unlink(log_file) + + def test_singleton_logger(self): + """Test that VetaLogger follows singleton pattern.""" + logger1 = VetaLogger() + logger2 = VetaLogger() + assert logger1 is logger2 + + def test_logger_formatting(self): + """Test that log messages contain expected formatting elements.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as f: + log_file = f.name + + try: + setup_logging( + level='DEBUG', + log_file=log_file, + console_output=False + ) + + logger = get_logger('test_format') + logger.info("Format test message") + + with open(log_file, 'r') as f: + content = f.read() + + # Check for timestamp (YYYY-MM-DD HH:MM:SS format) + assert any(char.isdigit() for char in content) + + # Check for log level + assert "INFO" in content + + # Check for logger name + assert "test_format" in content + + # Check for message + assert "Format test message" in content + finally: + if os.path.exists(log_file): + os.unlink(log_file) + +def test_integration_with_veta_components(): + """Test that veta components can use the logging system.""" + from veta.respondent import Respondent + from veta.item import Item + + # Setup logging + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as f: + log_file = f.name + + try: + setup_logging( + level='DEBUG', + log_file=log_file, + console_output=False + ) + + # Create veta components which should generate logs + respondent = Respondent(userid="test_user") + item = Item("I feel happy", "She looks sad") + respondent.add_item(item) + + # Check that logs were generated + with open(log_file, 'r') as f: + content = f.read() + assert "respondent" in content.lower() + assert "item" in content.lower() + + finally: + if os.path.exists(log_file): + os.unlink(log_file) + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/veta/__init__.py b/veta/__init__.py new file mode 100644 index 0000000..5a48431 --- /dev/null +++ b/veta/__init__.py @@ -0,0 +1,21 @@ +""" +Veta - A Python package for LEAS (Levels of Emotional Awareness Scale) analysis. +""" + +from .logger import get_logger, setup_logging +from .survey import Survey +from .respondent import Respondent +from .item import Item +from .wordlist import Wordlist +from .auto_self_other_item import attempt_auto_self_other + +__version__ = "1.0.0" +__all__ = [ + "Survey", + "Respondent", + "Item", + "Wordlist", + "attempt_auto_self_other", + "get_logger", + "setup_logging" +] diff --git a/veta/auto_self_other_item.py b/veta/auto_self_other_item.py index 03b065a..17e7110 100644 --- a/veta/auto_self_other_item.py +++ b/veta/auto_self_other_item.py @@ -1,4 +1,8 @@ import spacy +from veta.logger import get_logger + +# Initialize logger for this module +logger = get_logger('auto_self_other_item') # Initialize spacy models with defaults nlp_en = None @@ -6,15 +10,20 @@ try: nlp_en = spacy.load("en_core_web_sm") + logger.info("English SpaCy model loaded successfully") except: + logger.warning("English SpaCy not detected: https://spacy.io/usage/models/") print("English SpaCy not detected: https://spacy.io/usage/models/") try: nlp_de = spacy.load("de_core_news_sm") + logger.info("German SpaCy model loaded successfully") except: + logger.warning("German SpaCy not detected: https://spacy.io/usage/models/") print("German SpaCy not detected: https://spacy.io/usage/models/") def attempt_auto_self_other(item, lang = "en") -> None: + logger.debug(f"Attempting auto self/other separation for language: {lang}") self_sentence = '' other_sentence = '' @@ -22,20 +31,26 @@ def attempt_auto_self_other(item, lang = "en") -> None: #Decompose the sentence if lang.lower() == "de" or lang.lower() == "german" or lang.lower() == "deutsch": + logger.debug("Using German language processing") if nlp_de is None: + logger.error("German SpaCy model not available") raise RuntimeError("German SpaCy model not available. Please install with: python -m spacy download de_core_news_sm") doc = nlp_de(item.raw_input) selfidentifiers = ['ich'] verblist = ["würde", "wäre", "fühlt"] subject_identifier = 'sb' else: + logger.debug("Using English language processing") if nlp_en is None: + logger.error("English SpaCy model not available") raise RuntimeError("English SpaCy model not available. Please install with: python -m spacy download en_core_web_sm") doc = nlp_en(item.raw_input) selfidentifiers = ["i"] verblist = ["feel","be","feels","feeling"] subject_identifier = 'nsubj' + logger.debug(f"Processing sentence with {len(doc)} tokens") + #Loop through the sentence components for token in doc: #When we hit a sentence subject @@ -44,6 +59,8 @@ def attempt_auto_self_other(item, lang = "en") -> None: #Then we flip the sentence to be about the other #Otherwise we assume we are discussing the self other_flag = (token.text.lower() not in selfidentifiers) and (token.head.text in verblist) + if other_flag: + logger.debug(f"Switched to 'other' context at token: {token.text}") if other_flag: other_sentence += token.text + ' ' else: @@ -51,4 +68,6 @@ def attempt_auto_self_other(item, lang = "en") -> None: item.self_sentence = item.clean_sentence(self_sentence) item.other_sentence = item.clean_sentence(other_sentence) + + logger.info(f"Auto-separated - Self: '{item.self_sentence[:30]}...', Other: '{item.other_sentence[:30]}...'") return \ No newline at end of file diff --git a/veta/item.py b/veta/item.py index 29ff350..201dc72 100644 --- a/veta/item.py +++ b/veta/item.py @@ -1,7 +1,11 @@ from veta.wordlist import Wordlist from veta.scoring_modules.scoring_module import ScoringModule +from veta.logger import get_logger import inspect +# Initialize logger for this module +logger = get_logger('item') + class Item: """ A class representing a single LEAS survey item. An item is a single response to an LEAS questions. @@ -45,6 +49,8 @@ def __init__(self, self_sentence: str, other_sentence: str = "") -> None: Returns: ''' + logger.debug(f"Initializing Item with self_sentence length: {len(self_sentence)}, other_sentence length: {len(other_sentence)}") + self.raw_input = self_sentence +". " + other_sentence self.full_sentence = self.clean_sentence(self.raw_input) self.self_sentence = self.clean_sentence(self_sentence) @@ -53,6 +59,7 @@ def __init__(self, self_sentence: str, other_sentence: str = "") -> None: self.wordlist = None + logger.info(f"Created Item with full_sentence: '{self.full_sentence[:50]}{'...' if len(self.full_sentence) > 50 else ''}'") return def __str__(self): @@ -80,6 +87,7 @@ def add_additional_info(self, id, info) -> None: Returns: ''' + logger.debug(f"Adding additional info to Item: {id} = {info}") self.scores[id] = info return @@ -109,21 +117,31 @@ def score(self, scoring_module: ScoringModule) -> None: Returns: ''' - if len(inspect.signature(scoring_module.execute).parameters) < 2: - scres = scoring_module.execute(self) - - elif isinstance(self.wordlist, Wordlist): - - scres = scoring_module.execute(self, self.wordlist) - else: - raise Exception("Scoring Error: Item does not have a wordlist") - - if isinstance(scres,tuple): - for i in range(len(scres)): - self.scores[scoring_module.id+str(i+1)] = scres[i] - else: - self.scores[scoring_module.id] = scres + module_id = getattr(scoring_module, 'id', str(scoring_module)) + logger.debug(f"Scoring Item with module: {module_id}") + try: + if len(inspect.signature(scoring_module.execute).parameters) < 2: + scres = scoring_module.execute(self) + logger.debug(f"Executed module {module_id} without wordlist") + elif isinstance(self.wordlist, Wordlist): + scres = scoring_module.execute(self, self.wordlist) + logger.debug(f"Executed module {module_id} with wordlist") + else: + logger.error(f"Scoring Error: Item does not have a wordlist for module {module_id}") + raise Exception("Scoring Error: Item does not have a wordlist") + + if isinstance(scres,tuple): + logger.debug(f"Module {module_id} returned tuple with {len(scres)} values") + for i in range(len(scres)): + self.scores[scoring_module.id+str(i+1)] = scres[i] + else: + logger.debug(f"Module {module_id} returned single value: {scres}") + self.scores[scoring_module.id] = scres + + except Exception as e: + logger.error(f"Error scoring Item with module {module_id}: {str(e)}") + raise return @@ -136,8 +154,9 @@ def add_wordlist(self, wordlist: Wordlist) -> None: Returns: ''' + logger.debug("Adding wordlist to Item") self.wordlist = wordlist return - - + + diff --git a/veta/logger.py b/veta/logger.py new file mode 100644 index 0000000..61c76f2 --- /dev/null +++ b/veta/logger.py @@ -0,0 +1,373 @@ +""" +Comprehensive logging system for veta package. + +This module provides: +1. A centralized logger that can be used throughout the veta package +2. Colored, timestamped logs with different levels (DEBUG, INFO, WARNING, ERROR) +3. Detailed formatting including function name and line number +4. File output with automatic log file generation +5. Easy initialization from any part of the codebase +""" + +import logging +import sys +import os +from datetime import datetime +from pathlib import Path +from typing import Optional, Union +import inspect + +# ANSI color codes for terminal output +class LogColors: + """ANSI color codes for colored terminal output.""" + RESET = '\033[0m' + BOLD = '\033[1m' + + # Regular colors + BLACK = '\033[30m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + + # Bright colors + BRIGHT_BLACK = '\033[90m' + BRIGHT_RED = '\033[91m' + BRIGHT_GREEN = '\033[92m' + BRIGHT_YELLOW = '\033[93m' + BRIGHT_BLUE = '\033[94m' + BRIGHT_MAGENTA = '\033[95m' + BRIGHT_CYAN = '\033[96m' + BRIGHT_WHITE = '\033[97m' + + +class ColoredFormatter(logging.Formatter): + """Custom formatter that adds colors to log levels and maintains detailed formatting.""" + + # Color mapping for different log levels (for the actual message text) + MESSAGE_COLORS = { + 'DEBUG': LogColors.BRIGHT_BLUE, + 'INFO': LogColors.WHITE, # Keep info messages as standard white + 'WARNING': LogColors.BRIGHT_YELLOW, + 'ERROR': LogColors.BRIGHT_RED, + 'CRITICAL': LogColors.BRIGHT_MAGENTA, + } + + # Color mapping for log level labels + LEVEL_COLORS = { + 'DEBUG': LogColors.BLUE, + 'INFO': LogColors.GREEN, + 'WARNING': LogColors.YELLOW, + 'ERROR': LogColors.RED, + 'CRITICAL': LogColors.MAGENTA, + } + + # Fixed colors for different parts + TIMESTAMP_COLOR = LogColors.BRIGHT_GREEN + LOCATION_COLOR = LogColors.CYAN + + def __init__(self, use_colors=True): + self.use_colors = use_colors and sys.stderr.isatty() # Only use colors in terminal + + # Detailed format with timestamp, level, location, and message + detailed_format = ( + "%(asctime)s | %(levelname)-8s | " + "%(name)s:%(funcName)s:%(lineno)d | " + "%(message)s" + ) + + super().__init__( + fmt=detailed_format, + datefmt='%Y-%m-%d %H:%M:%S' + ) + + def format(self, record): + # Get the basic formatted message + formatted = super().format(record) + + # Add colors if enabled and this is a terminal + if self.use_colors: + level_name = record.levelname + + # Split the formatted message into parts + parts = formatted.split(' | ') + if len(parts) >= 4: + timestamp = parts[0] + level = parts[1] + location = parts[2] + message = ' | '.join(parts[3:]) # In case message contains ' | ' + + # Apply colors to each part + colored_timestamp = f"{self.TIMESTAMP_COLOR}{timestamp}{LogColors.RESET}" + colored_level = f"{self.LEVEL_COLORS.get(level_name, '')}{level}{LogColors.RESET}" + colored_location = f"{self.LOCATION_COLOR}{location}{LogColors.RESET}" + colored_message = f"{self.MESSAGE_COLORS.get(level_name, '')}{message}{LogColors.RESET}" + + # Reconstruct the formatted message with colors + formatted = f"{colored_timestamp} | {colored_level} | {colored_location} | {colored_message}" + + return formatted + + +class VetaLogger: + """ + Main logger class for the veta package. + + This class provides a centralized logging system that can be used throughout + the veta package with consistent formatting and output options. + """ + + _instance = None + _initialized = False + + def __new__(cls): + """Singleton pattern to ensure only one logger instance.""" + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """Initialize the logger (only once due to singleton pattern).""" + if not self._initialized: + self.logger = logging.getLogger('veta') + self.logger.setLevel(logging.DEBUG) + self._handlers_added = False + VetaLogger._initialized = True + + def setup(self, + level: Union[str, int] = logging.INFO, + log_file: Optional[str] = None, + console_output: bool = True, + file_level: Union[str, int] = logging.DEBUG, + console_level: Union[str, int] = logging.INFO, + auto_generate_file: bool = True) -> None: + """ + Set up the logger with specified configuration. + + Parameters: + ----------- + level : str or int + Overall logging level (default: INFO) + log_file : str, optional + Path to log file. If None and auto_generate_file is True, + will auto-generate a filename. + console_output : bool + Whether to output logs to console (default: True) + file_level : str or int + Logging level for file output (default: DEBUG) + console_level : str or int + Logging level for console output (default: INFO) + auto_generate_file : bool + Whether to auto-generate log file if none specified (default: True) + """ + + # Clear any existing handlers to avoid duplicates + if self._handlers_added: + self.logger.handlers.clear() + + # Convert string levels to integers if needed + if isinstance(level, str): + level = getattr(logging, level.upper()) + if isinstance(file_level, str): + file_level = getattr(logging, file_level.upper()) + if isinstance(console_level, str): + console_level = getattr(logging, console_level.upper()) + + self.logger.setLevel(level) + + # Set up console handler + if console_output: + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(console_level) + console_handler.setFormatter(ColoredFormatter(use_colors=True)) + self.logger.addHandler(console_handler) + + # Set up file handler + if log_file or auto_generate_file: + if not log_file and auto_generate_file: + log_file = self._generate_log_filename() + + if log_file: + # Ensure log directory exists + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = logging.FileHandler(log_file, mode='a') + file_handler.setLevel(file_level) + # Use plain formatter for file (no colors) + file_formatter = logging.Formatter( + fmt="%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s", + datefmt='%Y-%m-%d %H:%M:%S' + ) + file_handler.setFormatter(file_formatter) + self.logger.addHandler(file_handler) + + self.logger.info(f"Logging to file: {log_file}") + + self._handlers_added = True + self.logger.info("Veta logging system initialized") + + def _generate_log_filename(self) -> str: + """Generate an automatic log filename based on current timestamp.""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Try to create logs in a few different locations + possible_dirs = [ + Path.cwd() / "logs", + Path.cwd(), + Path.home() / "veta_logs", + Path("/tmp") / "veta_logs" + ] + + for log_dir in possible_dirs: + try: + log_dir.mkdir(parents=True, exist_ok=True) + return str(log_dir / f"veta_{timestamp}.log") + except (PermissionError, OSError): + continue + + # Fallback to current directory + return f"veta_{timestamp}.log" + + def get_logger(self, name: Optional[str] = None) -> logging.Logger: + """ + Get a logger instance. + + Parameters: + ----------- + name : str, optional + Name for the logger. If None, returns the main veta logger. + + Returns: + -------- + logging.Logger + Configured logger instance + """ + if name: + return logging.getLogger(f'veta.{name}') + return self.logger + + +# Global logger instance +_veta_logger = VetaLogger() + + +def setup_logging(level: Union[str, int] = logging.INFO, + log_file: Optional[str] = None, + console_output: bool = True, + file_level: Union[str, int] = logging.DEBUG, + console_level: Union[str, int] = logging.INFO, + auto_generate_file: bool = True) -> None: + """ + Set up logging for the veta package. + + This is the main function to initialize logging. Call this once at the start + of your application. + + Parameters: + ----------- + level : str or int + Overall logging level (default: INFO) + log_file : str, optional + Path to log file. If None and auto_generate_file is True, + will auto-generate a filename. + console_output : bool + Whether to output logs to console (default: True) + file_level : str or int + Logging level for file output (default: DEBUG) + console_level : str or int + Logging level for console output (default: INFO) + auto_generate_file : bool + Whether to auto-generate log file if none specified (default: True) + + Examples: + --------- + >>> from veta import setup_logging + >>> setup_logging(level='DEBUG', log_file='my_analysis.log') + + >>> # Auto-generate log file with default settings + >>> setup_logging() + + >>> # Console-only logging + >>> setup_logging(auto_generate_file=False) + """ + _veta_logger.setup( + level=level, + log_file=log_file, + console_output=console_output, + file_level=file_level, + console_level=console_level, + auto_generate_file=auto_generate_file + ) + + +def get_logger(name: Optional[str] = None) -> logging.Logger: + """ + Get a logger for use in veta modules. + + This function automatically sets up basic logging if it hasn't been + configured yet. For full control, call setup_logging() first. + + Parameters: + ----------- + name : str, optional + Name for the logger. If None, returns the main veta logger. + If provided, will create a child logger named 'veta.{name}'. + + Returns: + -------- + logging.Logger + Configured logger instance with proper formatting and handlers. + + Examples: + --------- + >>> from veta import get_logger + >>> logger = get_logger('survey') + >>> logger.info("Processing survey data") + >>> logger.debug("Detailed debug information") + >>> logger.warning("This is a warning") + >>> logger.error("An error occurred") + + >>> # Get the main logger + >>> main_logger = get_logger() + >>> main_logger.info("Main application message") + """ + # Auto-initialize with basic settings if not already done + if not _veta_logger._handlers_added: + _veta_logger.setup() + + return _veta_logger.get_logger(name) + + +# Convenience function to get caller info for manual logging +def get_caller_info(frame_offset: int = 1) -> tuple: + """ + Get information about the calling function. + + Parameters: + ----------- + frame_offset : int + How many frames up the stack to look (default: 1) + + Returns: + -------- + tuple + (function_name, filename, line_number) + """ + frame = inspect.currentframe() + try: + for _ in range(frame_offset + 1): + frame = frame.f_back + if frame is None: + return ("unknown", "unknown", 0) + + return ( + frame.f_code.co_name, + os.path.basename(frame.f_code.co_filename), + frame.f_lineno + ) + finally: + del frame diff --git a/veta/respondent.py b/veta/respondent.py index 7b3349c..34c1706 100644 --- a/veta/respondent.py +++ b/veta/respondent.py @@ -1,7 +1,11 @@ from veta.item import Item from veta.wordlist import Wordlist +from veta.logger import get_logger import numpy as np +# Initialize logger for this module +logger = get_logger('respondent') + total_respondents = 0 class Respondent: @@ -39,13 +43,17 @@ def __init__(self, userid=None, wordlist_file=None) -> None: global total_respondents + logger.debug(f"Initializing new Respondent with userid={userid}, wordlist_file={wordlist_file}") + self.items = [] self.id = total_respondents self.userid = None if isinstance(userid, str): self.userid = userid + logger.debug(f"Set userid to: {userid}") self.wordlist = None if isinstance(wordlist_file, str): + logger.info(f"Loading wordlist from file: {wordlist_file}") wordlist = Wordlist(wordlist_file) self.add_wordlist(wordlist) @@ -54,6 +62,7 @@ def __init__(self, userid=None, wordlist_file=None) -> None: self.totals = {} self.col_names = [] + logger.info(f"Created Respondent {self.id} (userid: {self.userid or 'None'})") return def __str__(self) -> str: @@ -124,11 +133,15 @@ def add_item(self, *sentences) -> Item: Returns: item (Item): The new item that was created. ''' + logger.debug(f"Adding item to Respondent {self.id}. Sentences: {len(sentences)} provided") + if len(sentences) == 1 and isinstance(sentences[0], Item): item = sentences[0] + logger.debug("Using existing Item object") else: item = Item(*sentences) item.add_wordlist(self.wordlist) + logger.debug("Created new Item from sentences") if 'index' not in item.scores.keys(): item.add_additional_info("index", len(self.items)+1) @@ -137,6 +150,7 @@ def add_item(self, *sentences) -> Item: item.add_wordlist(self.wordlist) self.items.append(item) + logger.info(f"Added item {len(self.items)} to Respondent {self.id}") return item @@ -150,8 +164,8 @@ def add_additional_info(self, id, data) -> None: Returns: ''' + logger.debug(f"Adding additional info to Respondent {self.id}: {id} = {data}") self.totals[id] = data - return @@ -166,24 +180,34 @@ def score(self, *modules) -> None: Returns: ''' + logger.info(f"Scoring Respondent {self.id} with {len(modules)} modules") + for module in modules: + logger.debug(f"Applying module: {getattr(module, 'id', str(module))} (type: {getattr(module, 'type', 'unknown')})") + if module.type == "per item": #total = 0 - for item in self.items: + for i, item in enumerate(self.items): + logger.debug(f"Scoring item {i+1} with module {getattr(module, 'id', str(module))}") item.score(module) #total += item.scores[module.id] elif module.type == "per respondent": + logger.debug(f"Applying per-respondent module: {getattr(module, 'id', str(module))}") for item in self.items: item.scores[module.id] = 0 total = module.execute(self.items, self.wordlist) #self.modules_ran.add(module.id) self.totals[module.id] = total + logger.debug(f"Per-respondent module result: {total}") + self.compute_totals() - + logger.info(f"Completed scoring for Respondent {self.id}") return def compute_totals(self): + logger.debug(f"Computing totals for Respondent {self.id}") if len(self.items) < 1: + logger.warning(f"No items found for Respondent {self.id}, cannot compute totals") return for ids in self.items[0].scores.keys(): total = 0 @@ -191,6 +215,7 @@ def compute_totals(self): total += item.scores[ids] if total != 0 or ids not in self.totals.keys(): self.totals[ids] = total + logger.debug(f"Computed totals for {len(self.totals)} scoring methods") def add_wordlist(self, wordlist: Wordlist) -> None: ''' @@ -201,9 +226,11 @@ def add_wordlist(self, wordlist: Wordlist) -> None: Returns: ''' + logger.info(f"Setting wordlist for Respondent {self.id}") self.wordlist = wordlist - for item in self.items: + for i, item in enumerate(self.items): + logger.debug(f"Adding wordlist to item {i+1}") item.add_wordlist(wordlist) + logger.debug(f"Wordlist added to {len(self.items)} items") return - \ No newline at end of file diff --git a/veta/scoring_modules/scoring_module.py b/veta/scoring_modules/scoring_module.py index 8506449..4912231 100644 --- a/veta/scoring_modules/scoring_module.py +++ b/veta/scoring_modules/scoring_module.py @@ -1,8 +1,12 @@ import numpy as np from veta.wordlist import Wordlist +from veta.logger import get_logger import re from collections import defaultdict +# Initialize logger for scoring modules +logger = get_logger('scoring_module') + class ScoringModule: """ The parent class to all of the LEAS scoring modules diff --git a/veta/survey.py b/veta/survey.py index a7a00b9..820b41a 100644 --- a/veta/survey.py +++ b/veta/survey.py @@ -3,6 +3,7 @@ from veta.item import Item from veta.respondent import Respondent from veta.wordlist import Wordlist +from veta.logger import get_logger import numpy as np import pandas as pd import matplotlib.pyplot as plt @@ -12,6 +13,9 @@ import json from scipy.stats import norm +# Initialize logger for this module +logger = get_logger('survey') + class NumpyEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (np.integer, np.int64, np.int32)): @@ -42,9 +46,12 @@ class Survey: """ def __init__(self, wordlist_file=None) -> None: + logger.debug(f"Initializing Survey with wordlist_file: {wordlist_file}") + self.respondents = [] self.wordlist = None if isinstance(wordlist_file,str): + logger.info(f"Loading wordlist from file: {wordlist_file}") wordlist = Wordlist(wordlist_file) self.add_wordlist(wordlist) @@ -52,6 +59,8 @@ def __init__(self, wordlist_file=None) -> None: self.num_item_cols = 0 self.summary = {} self.header = np.array(["ID", "Self", "Other"]) + + logger.info("Survey initialized successfully") return def __str__(self) -> str: @@ -137,27 +146,43 @@ def from_horizontal_layout(self, data): return def from_file(self, filename, layout='vertical'): + logger.info(f"Loading survey data from file: {filename} (layout: {layout})") # Get the file extension file_extension = os.path.splitext(filename)[1] + logger.debug(f"File extension detected: {file_extension}") # Check the file extension and read the file accordingly - if file_extension == '.csv': - data = np.array(pd.read_csv(filename, header=None)) - elif file_extension in ['.xls', '.xlsx']: - data = np.array(pd.read_excel(filename, header=None, engine='openpyxl')) - elif file_extension in ['.json']: - self.from_json(filename) - return - else: - raise ValueError(f"Unsupported file extension: {file_extension}") + try: + if file_extension == '.csv': + logger.debug("Reading CSV file") + data = np.array(pd.read_csv(filename, header=None)) + elif file_extension in ['.xls', '.xlsx']: + logger.debug("Reading Excel file") + data = np.array(pd.read_excel(filename, header=None, engine='openpyxl')) + elif file_extension in ['.json']: + logger.debug("Reading JSON file") + self.from_json(filename) + return + else: + logger.error(f"Unsupported file extension: {file_extension}") + raise ValueError(f"Unsupported file extension: {file_extension}") + + logger.info(f"Successfully loaded data with shape: {data.shape}") + + except Exception as e: + logger.error(f"Error reading file {filename}: {str(e)}") + raise if str(layout).lower() == "vertical": + logger.debug("Processing vertical layout") self.from_vertical_layout(data) elif str(layout).lower() == "horizontal": + logger.debug("Processing horizontal layout") self.from_horizontal_layout(data) self.add_wordlist(self.wordlist) + logger.info(f"Survey loaded with {len(self.respondents)} respondents") return @@ -171,15 +196,22 @@ def configure_columns(self, id_col, self_col, other_col, per_item_cols=[], per_r return def add_respondent(self, respondent): + logger.info(f"Adding respondent {getattr(respondent, 'id', 'unknown')} to survey") self.respondents.append(respondent) if not (self.wordlist is None): + logger.debug("Adding wordlist to new respondent") respondent.add_wordlist(self.wordlist) + logger.debug(f"Survey now has {len(self.respondents)} respondents") return def score(self,*modules): - - for respondent in self.respondents: + logger.info(f"Scoring survey with {len(modules)} modules across {len(self.respondents)} respondents") + + for i, respondent in enumerate(self.respondents): + logger.debug(f"Scoring respondent {i+1}/{len(self.respondents)} (ID: {getattr(respondent, 'id', 'unknown')})") respondent.score(*modules) + + logger.info("Survey scoring completed") def compute_summary(self, percentiles=False): diff --git a/veta/wordlist.py b/veta/wordlist.py index d234ccd..ba61579 100644 --- a/veta/wordlist.py +++ b/veta/wordlist.py @@ -3,7 +3,11 @@ import datetime import re import random -import string +import string +from veta.logger import get_logger + +# Initialize logger for this module +logger = get_logger('wordlist') def is_number(s): """ @@ -46,14 +50,20 @@ def __init__(self, filename: str, creator="veta", name="wordlist", language="en" Returns: ''' + logger.info(f"Initializing Wordlist from file: {filename}") self.unique_id = ''.join(random.sample(string.ascii_uppercase, 26)) self.filename = filename self.creator = creator self.name = name self.language = language + + logger.debug(f"Loading wordlist data from file") self.loadFromFile(filename) + logger.debug("Cleaning wordlist data") self.cleanWordlist() + + logger.info(f"Wordlist initialized with {len(self.words)} words") # w, s, sb = [], [], [] # # @@ -78,20 +88,32 @@ def loadFromFile(self, filename: str) -> np.array: Returns: wordlist (numpy.array): the contents of the wordlist file given as as numpy array ''' - if filename.endswith(".txt"): - self.words, self.scores = self.loadFromTxt(filename) - self.subclasses = np.zeros_like(self.scores) - elif filename.endswith(".xlsx") or filename.endswith(".xls"): - data = np.array(pd.read_excel(filename, engine='openpyxl')) - self.words = data[:,0] - self.scores = data[:,1] - if data.shape[1] > 2: - self.subclasses = data[:,2] - else: + logger.debug(f"Loading wordlist from file: {filename}") + + try: + if filename.endswith(".txt"): + logger.debug("Loading from text file") + self.words, self.scores = self.loadFromTxt(filename) self.subclasses = np.zeros_like(self.scores) - return - else: - raise Exception("File Type not Supported. Please use .txt or .xlsx") + elif filename.endswith(".xlsx") or filename.endswith(".xls"): + logger.debug("Loading from Excel file") + data = np.array(pd.read_excel(filename, engine='openpyxl')) + self.words = data[:,0] + self.scores = data[:,1] + if data.shape[1] > 2: + self.subclasses = data[:,2] + logger.debug("Loaded subclasses from third column") + else: + self.subclasses = np.zeros_like(self.scores) + logger.debug("No subclasses found, using zeros") + return + else: + logger.error(f"Unsupported file type: {filename}") + raise Exception("File Type not Supported. Please use .txt or .xlsx") + + except Exception as e: + logger.error(f"Error loading wordlist from {filename}: {str(e)}") + raise def __str__(self):