gameboy_worlds.utils.fundamental
1# This file contains all the fundamental utilities that do not rely on any other file. 2import os 3import logging 4import importlib.util 5from typing import Dict 6 7 8class RelativePathFormatter(logging.Formatter): 9 def format(self, record): 10 # record.pathname is the full system path 11 path = record.pathname 12 13 # Look for 'src' and keep everything after it 14 if "src" in path: 15 # Splits at 'src', takes the last part, and removes leading slashes 16 record.custom_path = "gameboy_worlds" + path.split("gameboy_worlds")[-1] 17 else: 18 # Fallback to just the filename if 'src' isn't found 19 record.custom_path = record.filename 20 21 return super().format(record) 22 23 24def get_logger( 25 level: int = logging.INFO, filename: str = None, add_console: bool = True 26) -> logging.Logger: 27 """ 28 Sets up and returns a logger with specified configurations. 29 Args: 30 level (int, optional): Logging level. Defaults to logging.INFO. 31 filename (str, optional): If provided, logs will be written to this file. Defaults to None. 32 add_console (bool, optional): If True, logs will also be printed to the console. Defaults to True. 33 Returns: 34 logging.Logger: Configured logger instance. 35 """ 36 fmt_str = "%(asctime)s, [%(levelname)s, %(custom_path)s:%(lineno)d] %(message)s" 37 # Note: deliberately not passing format=fmt_str here — fmt_str requires 38 # record.custom_path, which only RelativePathFormatter (below) sets. The 39 # root logger's handler uses a plain Formatter, so giving it fmt_str 40 # would raise a KeyError whenever another logger (e.g. asyncio) propagates 41 # a record to root. 42 logging.basicConfig() 43 logger = logging.getLogger("GameBoyWorlds-Server") 44 if add_console: 45 logger.handlers.clear() 46 console_handler = logging.StreamHandler() 47 log_formatter = RelativePathFormatter(fmt_str) 48 console_handler.setFormatter(log_formatter) 49 logger.addHandler(console_handler) 50 if filename is not None: 51 file_handler = logging.FileHandler(filename, mode="a") 52 log_formatter = RelativePathFormatter(fmt_str) 53 file_handler.setFormatter(log_formatter) 54 logger.addHandler(file_handler) 55 if level is not None: 56 logger.setLevel(level) 57 logger.propagate = False 58 return logger 59 60 61def meta_dict_to_str( 62 meta_dict: dict, 63 *, 64 print_mode: bool = False, 65 n_indents: int = 1, 66 skip_write_timestamp: bool = True, 67) -> str: 68 """ 69 Converts a metadata dictionary to a string representation. 70 Args: 71 meta_dict (dict): The metadata dictionary to convert. 72 print_mode (bool, optional): If True, formats the string for printing with indentation. Defaults to False. 73 n_indents (int, optional): Number of indentation levels for print mode. Defaults to 1. 74 skip_write_timestamp (bool, optional): If True, skips the 'write_timestamp' key in non-print mode. Defaults to True. 75 Returns: 76 str: String representation of the metadata dictionary. 77 """ 78 keys = list(meta_dict.keys()) 79 # error out if None is a key 80 if None in keys: 81 raise ValueError("None cannot be a key in meta_dict") 82 keys.sort() 83 meta_str = "" 84 for key in keys: 85 if print_mode: 86 indent = "\t" * n_indents 87 element = meta_dict[key] 88 element_str = None 89 if isinstance(element, dict): 90 element_str = "\n" + meta_dict_to_str( 91 element, print_mode=True, n_indents=n_indents + 1 92 ) 93 else: 94 element_str = str(element) 95 meta_str += f"{indent}{key}: {element_str}\n" 96 else: 97 if skip_write_timestamp and key == "write_timestamp": 98 continue 99 meta_str += f"{key.lower().strip()}_{str(meta_dict[key]).lower().strip()}" 100 return meta_str 101 102 103def logger_print_dict(logger: logging.Logger, meta_dict: dict, n_indents: int = 1): 104 """ 105 Logs the string representation of a metadata dictionary using the provided logger. 106 Args: 107 logger (logging.Logger): The logger to use for logging. 108 meta_dict (dict): The metadata dictionary to log. 109 n_indents (int, optional): Number of indentation levels for formatting. Defaults to 1. 110 """ 111 meta_dict_str = meta_dict_to_str( 112 meta_dict, print_mode=True, n_indents=n_indents, skip_write_timestamp=False 113 ) 114 logger.info(meta_dict_str) 115 116 117def file_makedir(file_path: str): 118 """ 119 Ensures that the directory for the given file path exists. If not, it creates the necessary directories. 120 Args: 121 file_path (str): The file path for which to ensure the directory exists. 122 """ 123 dirname = os.path.dirname(file_path) 124 if dirname != "" and not os.path.exists(dirname): 125 os.makedirs(dirname) 126 return 127 128 129def module_installed(name: str) -> bool: 130 """ 131 Checks if a module with the given name is installed. 132 Args: 133 name (str): The name of the module to check. 134 Returns: 135 bool: True if the module is installed, False otherwise. 136 """ 137 spec = importlib.util.find_spec(name) 138 return spec is not None 139 140 141def check_optional_installs(warn=False) -> Dict[str, bool]: 142 """ 143 Check for installs of optional modules 144 145 Args: 146 warn: whether to log a warning if not found. 147 148 Returns: 149 optionals (dict): a dictionary where keys are optional config modes, and values are whether the packages required for basic imports are installed. 150 This does not check internal requirements (e.g. `einops` may be needed for some models, etc.) 151 """ 152 config_imports = { 153 "vlm": ["transformers", "torch", "accelerate", "openai"], 154 } 155 if warn: 156 logger = get_logger() 157 configs = {} 158 for config in config_imports: 159 not_importable = [] 160 for module in config_imports[config]: 161 if not module_installed(module): 162 not_importable.append(module) 163 if len(not_importable) > 0 and warn: 164 logger.warning( 165 f'Unable to find imports for the following modules of the {config} setting: {not_importable}. Some features will not be enabled.\nTo fix this, run `uv pip install -e ".[{config}]"` in the GameBoyWorlds repo.' 166 ) 167 configs[config] = len(not_importable) == 0 168 return configs
9class RelativePathFormatter(logging.Formatter): 10 def format(self, record): 11 # record.pathname is the full system path 12 path = record.pathname 13 14 # Look for 'src' and keep everything after it 15 if "src" in path: 16 # Splits at 'src', takes the last part, and removes leading slashes 17 record.custom_path = "gameboy_worlds" + path.split("gameboy_worlds")[-1] 18 else: 19 # Fallback to just the filename if 'src' isn't found 20 record.custom_path = record.filename 21 22 return super().format(record)
Formatter instances are used to convert a LogRecord to text.
Formatters need to know how a LogRecord is constructed. They are responsible for converting a LogRecord to (usually) a string which can be interpreted by either a human or an external system. The base Formatter allows a formatting string to be specified. If none is supplied, the style-dependent default value, "%(message)s", "{message}", or "${message}", is used.
The Formatter can be initialized with a format string which makes use of knowledge of the LogRecord attributes - e.g. the default value mentioned above makes use of the fact that the user's message and arguments are pre- formatted into a LogRecord's message attribute. Currently, the useful attributes in a LogRecord are described by:
%(name)s Name of the logger (logging channel) %(levelno)s Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL) %(levelname)s Text logging level for the message ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") %(pathname)s Full pathname of the source file where the logging call was issued (if available) %(filename)s Filename portion of pathname %(module)s Module (name portion of filename) %(lineno)d Source line number where the logging call was issued (if available) %(funcName)s Function name %(created)f Time when the LogRecord was created (time.time() return value) %(asctime)s Textual time when the LogRecord was created %(msecs)d Millisecond portion of the creation time %(relativeCreated)d Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded (typically at application startup time) %(thread)d Thread ID (if available) %(threadName)s Thread name (if available) %(taskName)s Task name (if available) %(process)d Process ID (if available) %(message)s The result of record.getMessage(), computed just as the record is emitted
10 def format(self, record): 11 # record.pathname is the full system path 12 path = record.pathname 13 14 # Look for 'src' and keep everything after it 15 if "src" in path: 16 # Splits at 'src', takes the last part, and removes leading slashes 17 record.custom_path = "gameboy_worlds" + path.split("gameboy_worlds")[-1] 18 else: 19 # Fallback to just the filename if 'src' isn't found 20 record.custom_path = record.filename 21 22 return super().format(record)
Format the specified record as text.
The record's attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message.
25def get_logger( 26 level: int = logging.INFO, filename: str = None, add_console: bool = True 27) -> logging.Logger: 28 """ 29 Sets up and returns a logger with specified configurations. 30 Args: 31 level (int, optional): Logging level. Defaults to logging.INFO. 32 filename (str, optional): If provided, logs will be written to this file. Defaults to None. 33 add_console (bool, optional): If True, logs will also be printed to the console. Defaults to True. 34 Returns: 35 logging.Logger: Configured logger instance. 36 """ 37 fmt_str = "%(asctime)s, [%(levelname)s, %(custom_path)s:%(lineno)d] %(message)s" 38 # Note: deliberately not passing format=fmt_str here — fmt_str requires 39 # record.custom_path, which only RelativePathFormatter (below) sets. The 40 # root logger's handler uses a plain Formatter, so giving it fmt_str 41 # would raise a KeyError whenever another logger (e.g. asyncio) propagates 42 # a record to root. 43 logging.basicConfig() 44 logger = logging.getLogger("GameBoyWorlds-Server") 45 if add_console: 46 logger.handlers.clear() 47 console_handler = logging.StreamHandler() 48 log_formatter = RelativePathFormatter(fmt_str) 49 console_handler.setFormatter(log_formatter) 50 logger.addHandler(console_handler) 51 if filename is not None: 52 file_handler = logging.FileHandler(filename, mode="a") 53 log_formatter = RelativePathFormatter(fmt_str) 54 file_handler.setFormatter(log_formatter) 55 logger.addHandler(file_handler) 56 if level is not None: 57 logger.setLevel(level) 58 logger.propagate = False 59 return logger
Sets up and returns a logger with specified configurations.
Arguments:
- level (int, optional): Logging level. Defaults to logging.INFO.
- filename (str, optional): If provided, logs will be written to this file. Defaults to None.
- add_console (bool, optional): If True, logs will also be printed to the console. Defaults to True.
Returns:
logging.Logger: Configured logger instance.
62def meta_dict_to_str( 63 meta_dict: dict, 64 *, 65 print_mode: bool = False, 66 n_indents: int = 1, 67 skip_write_timestamp: bool = True, 68) -> str: 69 """ 70 Converts a metadata dictionary to a string representation. 71 Args: 72 meta_dict (dict): The metadata dictionary to convert. 73 print_mode (bool, optional): If True, formats the string for printing with indentation. Defaults to False. 74 n_indents (int, optional): Number of indentation levels for print mode. Defaults to 1. 75 skip_write_timestamp (bool, optional): If True, skips the 'write_timestamp' key in non-print mode. Defaults to True. 76 Returns: 77 str: String representation of the metadata dictionary. 78 """ 79 keys = list(meta_dict.keys()) 80 # error out if None is a key 81 if None in keys: 82 raise ValueError("None cannot be a key in meta_dict") 83 keys.sort() 84 meta_str = "" 85 for key in keys: 86 if print_mode: 87 indent = "\t" * n_indents 88 element = meta_dict[key] 89 element_str = None 90 if isinstance(element, dict): 91 element_str = "\n" + meta_dict_to_str( 92 element, print_mode=True, n_indents=n_indents + 1 93 ) 94 else: 95 element_str = str(element) 96 meta_str += f"{indent}{key}: {element_str}\n" 97 else: 98 if skip_write_timestamp and key == "write_timestamp": 99 continue 100 meta_str += f"{key.lower().strip()}_{str(meta_dict[key]).lower().strip()}" 101 return meta_str
Converts a metadata dictionary to a string representation.
Arguments:
- meta_dict (dict): The metadata dictionary to convert.
- print_mode (bool, optional): If True, formats the string for printing with indentation. Defaults to False.
- n_indents (int, optional): Number of indentation levels for print mode. Defaults to 1.
- skip_write_timestamp (bool, optional): If True, skips the 'write_timestamp' key in non-print mode. Defaults to True.
Returns:
str: String representation of the metadata dictionary.
104def logger_print_dict(logger: logging.Logger, meta_dict: dict, n_indents: int = 1): 105 """ 106 Logs the string representation of a metadata dictionary using the provided logger. 107 Args: 108 logger (logging.Logger): The logger to use for logging. 109 meta_dict (dict): The metadata dictionary to log. 110 n_indents (int, optional): Number of indentation levels for formatting. Defaults to 1. 111 """ 112 meta_dict_str = meta_dict_to_str( 113 meta_dict, print_mode=True, n_indents=n_indents, skip_write_timestamp=False 114 ) 115 logger.info(meta_dict_str)
Logs the string representation of a metadata dictionary using the provided logger.
Arguments:
- logger (logging.Logger): The logger to use for logging.
- meta_dict (dict): The metadata dictionary to log.
- n_indents (int, optional): Number of indentation levels for formatting. Defaults to 1.
118def file_makedir(file_path: str): 119 """ 120 Ensures that the directory for the given file path exists. If not, it creates the necessary directories. 121 Args: 122 file_path (str): The file path for which to ensure the directory exists. 123 """ 124 dirname = os.path.dirname(file_path) 125 if dirname != "" and not os.path.exists(dirname): 126 os.makedirs(dirname) 127 return
Ensures that the directory for the given file path exists. If not, it creates the necessary directories.
Arguments:
- file_path (str): The file path for which to ensure the directory exists.
130def module_installed(name: str) -> bool: 131 """ 132 Checks if a module with the given name is installed. 133 Args: 134 name (str): The name of the module to check. 135 Returns: 136 bool: True if the module is installed, False otherwise. 137 """ 138 spec = importlib.util.find_spec(name) 139 return spec is not None
Checks if a module with the given name is installed.
Arguments:
- name (str): The name of the module to check.
Returns:
bool: True if the module is installed, False otherwise.
142def check_optional_installs(warn=False) -> Dict[str, bool]: 143 """ 144 Check for installs of optional modules 145 146 Args: 147 warn: whether to log a warning if not found. 148 149 Returns: 150 optionals (dict): a dictionary where keys are optional config modes, and values are whether the packages required for basic imports are installed. 151 This does not check internal requirements (e.g. `einops` may be needed for some models, etc.) 152 """ 153 config_imports = { 154 "vlm": ["transformers", "torch", "accelerate", "openai"], 155 } 156 if warn: 157 logger = get_logger() 158 configs = {} 159 for config in config_imports: 160 not_importable = [] 161 for module in config_imports[config]: 162 if not module_installed(module): 163 not_importable.append(module) 164 if len(not_importable) > 0 and warn: 165 logger.warning( 166 f'Unable to find imports for the following modules of the {config} setting: {not_importable}. Some features will not be enabled.\nTo fix this, run `uv pip install -e ".[{config}]"` in the GameBoyWorlds repo.' 167 ) 168 configs[config] = len(not_importable) == 0 169 return configs
Check for installs of optional modules
Arguments:
- warn: whether to log a warning if not found.
Returns:
optionals (dict): a dictionary where keys are optional config modes, and values are whether the packages required for basic imports are installed. This does not check internal requirements (e.g.
einopsmay be needed for some models, etc.)