diff --git a/tests/main.py b/tests/main.py index 83886fccd35025e11a0139171bac24281ea0e23f..408fc5147c6b73a25c869cfe9c394cacb25f1d7a 100644 --- a/tests/main.py +++ b/tests/main.py @@ -14,11 +14,23 @@ def main(): """ Test logging output to console. """ - logger.debug('Test DEBUG logging statement.') - logger.info('Test INFO logging statement.') - logger.warning('Test WARNING logging statement.') - logger.error('Test ERROR logging statement.') - logger.testing('Test TEST logging statement.') + print('\n\nTesting logging from within main....\n') + + logger.debug('main() DEBUG logging statement.') + logger.info('main() INFO logging statement.') + logger.warning('main() WARNING logging statement.') + logger.error('main() ERROR logging statement.') + logger.testing('main() TEST logging statement.') + + print('\n\nTesting logging from imported project file...\n') + from src.test_file import import_file_function + import_file_function() + + print('\n\nTesting logging from fake "git submodule" main...\n') + from src.submodule_test.main import submodule_main + submodule_main() + + print('\n') if __name__ == '__main__': diff --git a/tests/src/submodule_test/main.py b/tests/src/submodule_test/main.py new file mode 100644 index 0000000000000000000000000000000000000000..098b1f8d942b0a94e3b93b888f353f318d0c5579 --- /dev/null +++ b/tests/src/submodule_test/main.py @@ -0,0 +1,32 @@ +""" +Test submodule logging usage. +""" + +# System Imports. +import pathlib, sys + +# Handle for "git submodule" path normalization. +submodule_path = str(pathlib.Path(__file__).parent.parent.absolute()) +sys.path.insert(1, submodule_path) + +# User Imports. +from submodule_test.src.logging import init_logging + + +# Import logger. +logger = init_logging(__name__) + + +def submodule_main(): + """ + Test logging output to console. + """ + logger.debug('fake "git submodule" main() DEBUG logging statement.') + logger.info('fake "git submodule" main() INFO logging statement.') + logger.warning('fake "git submodule" main() WARNING logging statement.') + logger.error('fake "git submodule" main() ERROR logging statement.') + logger.testing('fake "git submodule" main() TEST logging statement.') + + print('\n\nTesting logging from fake "git submodule" imported project file...\n') + from submodule_test.src.test_file import import_file_function + import_file_function() diff --git a/tests/src/submodule_test/src/logging.py b/tests/src/submodule_test/src/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..bc82d0f8cb0dffa1f6b2bad0a29b445e5b021fcb --- /dev/null +++ b/tests/src/submodule_test/src/logging.py @@ -0,0 +1,181 @@ +""" +Custom Python logger, defined via a dictionary of logging settings. + +https://git.brandon-rodriguez.com/python/custom_logger +Version 1.0 +""" + + +# System Imports. +import logging.config, pathlib, sys + + +# Logging Variables. +this = sys.modules[__name__] +this.settings = None +project_dir = pathlib.Path().absolute() +logging_directory = project_dir.joinpath('src/logs') +logging_class = 'logging.handlers.RotatingFileHandler' +logging_max_bytes = 1024 * 1024 * 10 # Max log file size of 10 MB. +logging_backup_count = 10 # Keep 10 log files before overwriting. + + +def get_logging_settings(): + """ + Returns an instance of the logging settings dictionary. + """ + return { + 'version': 1, + 'formatters': { + # Minimal logging. Only includes message. + # Generally meant for terminal "end user" interface display. + 'minimal': { + 'format': '%(message)s', + }, + # Simple logging. Includes message type and actual message. + # Generally meant for console logging. + 'simple': { + 'format': '[%(levelname)s] [%(filename)s %(lineno)d]: %(message)s', + }, + # Basic logging. Includes date, message type, file originated, and actual message. + # Generally meant for file logging. + 'standard': { + 'format': '%(asctime)s [%(levelname)s] [%(name)s %(lineno)d]: %(message)s', + }, + # Verbose logging. Includes standard plus the process number and thread id. + # For when you wanna be really verbose. + 'verbose': { + 'format': '%(asctime)s [%(levelname)s] [%(name)s %(lineno)d] || %(process)d %(thread)d || %(message)s' + }, + }, + 'handlers': { + # Sends log message to the void. May be useful for debugging. + 'null': { + 'class': 'logging.NullHandler', + }, + # To console. + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + # Debug Level - To file. + 'file_debug': { + 'level': 'DEBUG', + 'class': logging_class, + 'filename': logging_directory.joinpath('debug.log'), + 'maxBytes': logging_max_bytes, + 'backupCount': logging_backup_count, + 'formatter': 'standard', + }, + # Info Level - To file. + 'file_info': { + 'level': 'INFO', + 'class': logging_class, + 'filename': logging_directory.joinpath('info.log'), + 'maxBytes': logging_max_bytes, + 'backupCount': logging_backup_count, + 'formatter': 'standard', + }, + # Warn Level - To file. + 'file_warn': { + 'level': 'WARNING', + 'class': logging_class, + 'filename': logging_directory.joinpath('warn.log'), + 'maxBytes': logging_max_bytes, + 'backupCount': logging_backup_count, + 'formatter': 'standard', + }, + # Error Level - To file. + 'file_error': { + 'level': 'ERROR', + 'class': logging_class, + 'filename': logging_directory.joinpath('error.log'), + 'maxBytes': logging_max_bytes, + 'backupCount': logging_backup_count, + 'formatter': 'standard', + }, + 'test_level': { + 'level': 'TESTING', + 'class': logging_class, + 'filename': logging_directory.joinpath('test.log'), + 'maxBytes': logging_max_bytes, + 'backupCount': logging_backup_count, + 'formatter': 'standard', + }, + }, + 'loggers': { + # All basic logging. + '': { + 'handlers': ['console', 'file_debug', 'file_info', 'file_warn', 'file_error', 'test_level'], + 'level': 'NOTSET', + 'propagate': False, + } + }, + } + + +def init_logging(caller): + """ + Initializes and returns an instance of the logger. + :param caller: __name__ attribute of calling file. + :param initialize_settings: Boolean defined at top of file. + :return: Instance of logger, associated with calling file's __name__. + """ + if this.settings is None: + # Create logging folder if does not exist. + if not logging_directory.is_dir(): + print('Creating logging folders.') + logging_directory.mkdir() + + # Add new logging levels. + add_logging_level('TESTING', 25) + + # Load dictionary of settings into logger. + this.settings = get_logging_settings() + logging.config.dictConfig(this.settings) + + # Clear setup variable to prevent calling again on each file that imports logging. + initialize_settings = False + + return logging.getLogger(caller) + + +def add_logging_level(level_name, level_num, method_name=None): + """ + Logic to add a new logging level to logger. + + Logic from https://stackoverflow.com/a/35804945 + :param level_name: The name of new log level to create. + :param level_num: The numerical value of new log level to create. + :param method_name: The name of invoke method for new log level. Defaults to lowercase of level_name. + """ + # Get method name if not provided. + if not method_name: + method_name = level_name.lower() + + # Check if values have already been defined in logger. Prevents accidental overriding. + if (hasattr(logging, level_name) and + hasattr(logging, method_name) and + hasattr(logging.getLoggerClass(), method_name)): + # Log level already set with same values. Skip setting. + return None + elif (hasattr(logging, level_name) or + hasattr(logging, method_name) or + hasattr(logging.getLoggerClass(), method_name)): + # Log level partially defined with some values. Raise error. + raise AttributeError('{0} already defined in logging module, but values do not match.'.format(level_name)) + + # Methods to enable logging at new level. + def log_for_level(self, message, *args, **kwargs): + if self.isEnabledFor(level_num): + self._log(level_num, message, args, **kwargs) + + def log_to_root(message, *args, **kwargs): + logging.log(level_num, message, *args, **kwargs) + + # Set logger attributes for new level. + logging.addLevelName(level_num, level_name) + setattr(logging, level_name, level_num) + setattr(logging.getLoggerClass(), method_name, log_for_level) + setattr(logging, method_name, log_to_root) diff --git a/tests/src/submodule_test/src/test_file.py b/tests/src/submodule_test/src/test_file.py new file mode 100644 index 0000000000000000000000000000000000000000..ac825b0db0656dfbb7edf00bd5a1efacfd9b4183 --- /dev/null +++ b/tests/src/submodule_test/src/test_file.py @@ -0,0 +1,21 @@ +""" +Test submodule logging usage from a file within the src folder. +""" + +# User Imports. +from submodule_test.src.logging import init_logging + + +# Import logger. +logger = init_logging(__name__) + + +def import_file_function(): + """ + Test logging output to console. + """ + logger.debug('fake "git submodule" imported file DEBUG logging statement.') + logger.info('fake "git submodule" imported file INFO logging statement.') + logger.warning('fake "git submodule" imported file WARNING logging statement.') + logger.error('fake "git submodule" imported file ERROR logging statement.') + logger.testing('fake "git submodule" imported file TEST logging statement.') diff --git a/tests/src/test_file.py b/tests/src/test_file.py new file mode 100644 index 0000000000000000000000000000000000000000..84b268decf20eef9996ad6acd4fbcd897081f200 --- /dev/null +++ b/tests/src/test_file.py @@ -0,0 +1,21 @@ +""" +Test logging usage from a file within the src folder. +""" + +# User Imports. +from src.logging import init_logging + + +# Import logger. +logger = init_logging(__name__) + + +def import_file_function(): + """ + Test logging output to console. + """ + logger.debug('Imported file DEBUG logging statement.') + logger.info('Imported file INFO logging statement.') + logger.warning('Imported file WARNING logging statement.') + logger.error('Imported file ERROR logging statement.') + logger.testing('Imported file TEST logging statement.')