diff --git a/README.md b/README.md index ccc2b16..ca24618 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,7 @@ [![Build Status](https://travis-ci.org/vreuter/logmuse.svg?branch=master)](https://travis-ci.org/vreuter/logmuse) [![Coverage Status](https://coveralls.io/repos/github/vreuter/logmuse/badge.svg?branch=master)](https://coveralls.io/github/vreuter/logmuse?branch=master) -Small logging setup package +Logmuse is a small logging setup package. The point of logmuse is to make it super simple to add CLI-control of logging to your python CLI tool. It just provides a simple interface so that standard arguments can be passed on to the logger. + +It is only useful for CLI tools. diff --git a/docs/autodoc_build/logmuse.md b/docs/autodoc_build/logmuse.md index f325a1e..bcb7cd3 100644 --- a/docs/autodoc_build/logmuse.md +++ b/docs/autodoc_build/logmuse.md @@ -1,93 +1,130 @@ -# Package logmuse Documentation - -## Class AbsentOptionException + + + +
+ +# Package `logmuse` Documentation + +## Class `AbsentOptionException` Exception subtype suggesting that client should add log options. -### add\_logging\_options -Augment a CLI argument parser with this package's logging options. ```python -def add_logging_options(parser) +def init_logger(name='', level=None, stream=None, logfile=None, make_root=None, propagate=False, silent=False, devmode=False, verbosity=None, fmt=None, datefmt=None, plain_format=False, style=None) ``` -**Parameters:** +Establish and configure primary logger. + +This is intended to be called just once per "session", with a "session" +defined as an invocation of the main workflow, a testing session, or an +import of the primary abstractions, e.g. in an interactive iPython session. +#### Parameters: + +- `name` (`str`): name for the logger +- `level` (`int | str`): minimal level of messages to listen for +- `stream` (`str`): standard stream to use as log destination. The defaultbehavior is to write logs to stdout, even if null is passed here. This is to allow a CLI argument as input to stream parameter, where it may be undesirable to require specification of a default value in the client application in order to prevent passing None if no CLI option value is given. To disable standard stream logging, set 'silent' to True or pass a path to a file to which to write logs, which gets priority over a standard stream as the destination for log messages. +- `logfile` (`str | FileIO[str]`): path to filesystem location to use aslogs destination. if provided, this mutes standard stream logging. +- `make_root` (`bool`): whether to use returned logger as root logger. Thismeans the name will be 'root' and that messages will not propagate. +- `propagate` (`bool`): whether to allow messages from this logger to reachparent logger(s). +- `silent` (`bool`): whether to silence logging; this is only guaranteed formessages from this logger and for those from loggers beneath this one in the runtime hierarchy without no separate handling. Propagation must also be turned off separately--if this is not the root logger--in order to ensure that messages are not handled and emitted from a potential parent to the logger built here. +- `devmode` (`bool`): whether to log in development mode; possibly amongother behavioral changes to logs handling, use a more information-rich message format template. +- `verbosity` (`int | str`): alternate mode of expression for logging levelthat better accords with intuition about how to convey this. It's positively associated with message volume rather than negatively so, as logging level is. This takes precedence over 'level' if both are present. +- `fmt` (`str`): message format/template. +- `datefmt` (`str`): format/template for time component of a log record. +- `plain_format` (`bool`): force use of plain message format, even ifin development mode (debug level) +- `style` (`str`): string indicating message formatting strategy; refer tohttps://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles; only valid in Python3.2+ -- `parser` -- `argparse.ArgumentParser`: CLI options and argument parser toaugment with logging options. +#### Returns: -**Returns:** +- `logging.Logger`: configured Logger instance + + +#### Raises: + +- `ValueError`: if attempting to name explicitly non-root logger witha root name, or if both level and verbosity are specified + + + + +```python +def setup_logger(name='', level=None, stream=None, logfile=None, make_root=None, propagate=False, silent=False, devmode=False, verbosity=None, fmt=None, datefmt=None, plain_format=False, style=None) +``` -`argparse.ArgumentParser`: the input argument, supplemented with thispackage's logging options. +Old alias for init_logger for backwards compatibility +```python +def logger_via_cli(opts, strict=True, **kwargs) +``` -### logger\_via\_cli Convenience function creating a logger. This module provides the ability to augment a CLI parser with logging-related options/arguments so that client applications do not need intimate knowledge of the implementation. This function completes that lack of burden, parsing values for the options supplied herein. -```python -def logger_via_cli(opts, **kwargs) -``` - -**Parameters:** +#### Parameters: -- `opts` -- `argparse.Namespace`: command-line options/arguments. +- `opts` (`argparse.Namespace`): command-line options/arguments. +- `strict` (`bool`): whether to raise an exception -**Returns:** +#### Returns: -`logging.Logger`: configured logger instance. +- `logging.Logger`: configured logger instance. -**Raises:** +#### Raises: -- `pararead.logs.AbsentOptionException`: if one of the expected optionsisn't available in the given Namespace. Such a case suggests that a client application didn't use this module to add the expected logging options to a parser. +- `pararead.logs.AbsentOptionException`: if one of the expected optionsisn't available in the given Namespace, and the argument to the strict parameter is True. Such a case suggests that a client application didn't use this module to add the expected logging options to a parser. -### setup\_logger -Establish and configure primary logger. - -This is intended to be called just once per "session", with a "session" -defined as an invocation of the main workflow, a testing session, or an -import of the primary abstractions, e.g. in an interactive iPython session. ```python -def setup_logger(name='', level=None, stream=None, logfile=None, make_root=None, propagate=False, silent=False, devmode=False, verbosity=None, fmt=None, datefmt=None, plain_format=False, style=None) +def add_logging_options(parser) ``` -**Parameters:** - -- `name` -- `str`: name for the logger -- `level` -- `int | str`: minimal level of messages to listen for -- `stream` -- `str`: standard stream to use as log destination. The defaultbehavior is to write logs to stdout, even if null is passed here. This is to allow a CLI argument as input to stream parameter, where it may be undesirable to require specification of a default value in the client application in order to prevent passing None if no CLI option value is given. To disable standard stream logging, set 'silent' to True or pass a path to a file to which to write logs, which gets priority over a standard stream as the destination for log messages. -- `logfile` -- `str | FileIO[str]`: path to filesystem location to use aslogs destination. if provided, this mutes standard stream logging. -- `make_root` -- `bool`: whether to use returned logger as root logger. Thismeans the name will be 'root' and that messages will not propagate. -- `propagate` -- `bool`: whether to allow messages from this logger to reachparent logger(s). -- `silent` -- `bool`: whether to silence logging; this is only guaranteed formessages from this logger and for those from loggers beneath this one in the runtime hierarchy without no separate handling. Propagation must also be turned off separately--if this is not the root logger--in order to ensure that messages are not handled and emitted from a potential parent to the logger built here. -- `devmode` -- `bool`: whether to log in development mode; possibly amongother behavioral changes to logs handling, use a more information-rich message format template. -- `verbosity` -- `int | str`: alternate mode of expression for logging levelthat better accords with intuition about how to convey this. It's positively associated with message volume rather than negatively so, as logging level is. This takes precedence over 'level' if both are present. -- `fmt` -- `str`: message format/template. -- `datefmt` -- `str`: format/template for time component of a log record. -- `plain_format` -- `bool`: force use of plain message format, even ifin development mode (debug level) -- `style` -- `str`: string indicating message formatting strategy; refer tohttps://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles; only valid in Python3.2+ - +Augment a CLI argument parser with this package's logging options. +#### Parameters: -**Returns:** +- `parser` (`argparse.ArgumentParser`): CLI options and argument parser toaugment with logging options. -`logging.Logger`: configured Logger instance +#### Returns: -**Raises:** +- `argparse.ArgumentParser`: the input argument, supplemented with thispackage's logging options. -- `ValueError`: if attempting to name explicitly non-root logger witha root name, or if both level and verbosity are specified +
-**Version Information**: `logmuse` v0.1, generated by `lucidoc` v0.3.1 \ No newline at end of file +*Version Information: `logmuse` v0.2.1, generated by `lucidoc` v0.4.0* \ No newline at end of file diff --git a/docs/interactive.md b/docs/interactive.md new file mode 100644 index 0000000..7cc60a1 --- /dev/null +++ b/docs/interactive.md @@ -0,0 +1,15 @@ +# Using logmuse interactively + + +To set logmuse to DEBUG while in an interactive session, use: + +``` +logmuse.init_logger(PACKAGE, "DEBUG", devmode=True) +``` + +For example, for package divvy, which uses logmuse, run this in your interactive session: + +``` +logmuse.init_logger("divvy", "DEBUG", devmode=True) +``` + diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..7a16f84 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,100 @@ +# Tutorial + +You are producing a CLI package that will use logmuse. Logmuse will provide command-line options to control logging, such as `--verbosity` and `--silent`. Here's how to set it up. + +## Imported packages + +For any packages that are imported by your CLI package, all you have to do is follow the normal usage of the built-in `logging` module. **No need to use logmuse in imported packages**. Just type, for example, something like: + +``` +_LOGGER = logging.getLogger(__name__) +``` + +At the top of each module, and then use `_LOGGER.debug` or whatever in the module. That's it. The command line arguments passed via your CLI will control the imported packages as well. Make sure your package isn't making a root logger or something silly like that. + + +## Your CLI package + +Now, to use logmuse in your CLI package, *including* all imported loggers, all you have to do is: + +## 1 Initialize + +Just add to the `__init__.py` file: + +``` +import logmuse +logmuse.init_logger(PACKAGE) +``` + +Where `PACKAGE` is the name of your package. Now it will be set up with default parameters for your within-python-use. Remember, **this is only for the CLI package.** Do not add this code to client packages that do not implement CLIs. + + +## 2 Add CLI args + +When you build your argparser, add the logmuse CLI options with this: + + +``` +parser = logmuse.add_logging_options(parser) +``` + +This will give you: + +- `--verbosity` +- `--silent` +- `--logdev` + +And your logger will automatically respond to these command-line arguments. (PS, [pypiper](http://pypiper.databio.org) uses logmuse to add these; so if you're using pypiper to add args, don't repeat). + + +## 3 Activate logmuse + +At the top of your module file, say: + +``` +import logmuse +``` + +In your `main` function say: + +``` +global _LOGGER +_LOGGER = logmuse.logger_via_cli(args, make_root=True) +``` + +Here, `args` is the result of argparse.parse_args(). + + + + +## Old way + +No need to read further. This is how it *used to* work, before it was awesome. + +``` +# Set the logging level. +if args.dbg: + # Debug mode takes precedence and will listen for all messages. + level = args.logging_level or logging.DEBUG +elif args.verbosity is not None: + # Verbosity-framed specification trumps logging_level. + level = _LEVEL_BY_VERBOSITY[args.verbosity] +else: + # Normally, we're not in debug mode, and there's no verbosity. + level = LOGGING_LEVEL + +# Establish the project-root logger and attach one for this module. + +logger_kwargs = {"level": level, "logfile": args.logfile, "devmode": args.dbg} +init_logger(name="peppy", **logger_kwargs) +init_logger(name="divvy", **logger_kwargs) +global _LOGGER +_LOGGER = init_logger(name=_PKGNAME, **logger_kwargs) + + +logger_kwargs = {"level": level, "logfile": args.logfile, "devmode": args.dbg} +init_logger(name="peppy", **logger_kwargs) +init_logger(name="divvy", **logger_kwargs) +``` + +We should move the logging level stuff into logmuse diff --git a/logmuse/__init__.py b/logmuse/__init__.py index ba41a5d..137a773 100644 --- a/logmuse/__init__.py +++ b/logmuse/__init__.py @@ -1,2 +1,3 @@ from .est import * from ._version import __version__ +from .est import LEVEL_BY_VERBOSITY, DEV_LOGGING_FMT diff --git a/logmuse/_version.py b/logmuse/_version.py index 3ced358..b5fdc75 100644 --- a/logmuse/_version.py +++ b/logmuse/_version.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/logmuse/est.py b/logmuse/est.py index 2af411f..d5d825d 100644 --- a/logmuse/est.py +++ b/logmuse/est.py @@ -17,11 +17,12 @@ __email__ = "vreuter@virginia.edu" __all__ = ["add_logging_options", "logger_via_cli", "init_logger", - "setup_logger", "AbsentOptionException"] + "setup_logger", "AbsentOptionException", "LOGGING_CLI_OPTDATA"] BASIC_LOGGING_FORMAT = "%(message)s" -DEV_LOGGING_FMT = "[%(asctime)s] {%(name)s:%(lineno)d} (%(funcName)s) [%(levelname)s] > %(message)s " +DEV_LOGGING_FMT = "%(levelname).4s %(asctime)s | %(name)s:%(module)s:%(lineno)d > %(message)s " +DEFAULT_DATE_FMT = "%H:%M:%S" PACKAGE_NAME = "logmuse" STREAMS = {"OUT": sys.stdout, "ERR": sys.stderr} DEFAULT_STREAM = STREAMS["ERR"] @@ -30,9 +31,9 @@ TRACE_LEVEL_VALUE = 5 TRACE_LEVEL_NAME = "TRACE" CUSTOM_LEVELS = {TRACE_LEVEL_NAME: TRACE_LEVEL_VALUE} -SILENCE_LOGS_OPTNAME = "--silent" -VERBOSITY_OPTNAME = "--verbosity" -DEVMODE_OPTNAME = "--logdev" +SILENCE_LOGS_OPTNAME = "silent" +VERBOSITY_OPTNAME = "verbosity" +DEVMODE_OPTNAME = "logdev" PARAM_BY_OPTNAME = {DEVMODE_OPTNAME: "devmode"} # Translation of verbosity into logging level. @@ -70,7 +71,7 @@ def add_logging_options(parser): package's logging options. """ for optname, optdata in LOGGING_CLI_OPTDATA.items(): - parser.add_argument("{}".format(optname), **optdata) + parser.add_argument("--{}".format(optname), **optdata) return parser @@ -115,7 +116,7 @@ def logger_via_cli(opts, strict=True, **kwargs): def init_logger( name="", level=None, stream=None, logfile=None, make_root=None, propagate=False, silent=False, devmode=False, - verbosity=None, fmt=None, datefmt=None, plain_format=False, style=None): + verbosity=None, fmt=None, datefmt=DEFAULT_DATE_FMT, plain_format=False, style=None): """ Establish and configure primary logger. @@ -317,3 +318,18 @@ def __init__(self, missing_optname): format(missing_optname, "{}.{}".format( __name__, add_logging_options.__name__)) super(AbsentOptionException, self).__init__(likely_reason) + + + + +# Stolen from peppy. Probably need to make peppy/looper rely on this. +def get_logger(name): + """ + Return a logger with given name, equipped with custom method. + + :param str name: name for the logger to get/create. + :return logging.Logger: named, custom logger instance. + """ + l = logging.getLogger(name) + l.whisper = lambda msg, *args, **kwargs: l.log(5, msg, *args, **kwargs) + return l diff --git a/mkdocs.yml b/mkdocs.yml index e2bb9df..ee4cf89 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,8 @@ pypi_name: logmuse nav: - Introduction: - Home: README.md + - Tutorial: tutorial.md + - Interactive logmuse: interactive.md - Reference: - API: autodoc_build/logmuse.md - Support: support.md diff --git a/tests/test_add_logging_options.py b/tests/test_add_logging_options.py index 0eedf4a..bc4d759 100644 --- a/tests/test_add_logging_options.py +++ b/tests/test_add_logging_options.py @@ -14,7 +14,7 @@ def pytest_generate_tests(metafunc): """ Generation and parameterization of tests in this module. """ if "opt" in metafunc.fixturenames: - metafunc.parametrize("opt", list(LOGGING_CLI_OPTDATA.keys())) + metafunc.parametrize("opt", list(["--" + x for x in LOGGING_CLI_OPTDATA.keys()])) def test_all_options_are_added(parser, opt): @@ -24,7 +24,7 @@ def test_all_options_are_added(parser, opt): assert opt in _get_optnames(parser) -def test_each_option_is_functional(parser, opt): +def test_each_option_gis_functional(parser, opt): """ Each added CLI opt can be used as expected. """ add_logging_options(parser) for a in parser._actions: @@ -67,7 +67,7 @@ def _build_action_usage(act_kind): def get_general_use(act): name = _get_opt_first_name(act) arg = random.choice(_VERBOSITY_CHOICES) \ - if name == VERBOSITY_OPTNAME else _random_chars_option() + if name == "--" + VERBOSITY_OPTNAME else _random_chars_option() return [name, arg] strategies = [ ((argparse._StoreTrueAction, argparse._StoreFalseAction), diff --git a/tests/test_logger_via_cli.py b/tests/test_logger_via_cli.py index aeae67f..d0924d9 100644 --- a/tests/test_logger_via_cli.py +++ b/tests/test_logger_via_cli.py @@ -15,6 +15,8 @@ __email__ = "vreuter@virginia.edu" +VERBOSITY_OPTNAME = "--" + VERBOSITY_OPTNAME + @pytest.fixture def parser(): """ Update empty argument parser with standard logging options. """ @@ -54,7 +56,7 @@ def test_opts_added_none_used(parser): @pytest.mark.parametrize( ["cmdl", "flag", "hdlr_type"], - [([SILENCE_LOGS_OPTNAME], True, logging.NullHandler), + [(["--" + SILENCE_LOGS_OPTNAME], True, logging.NullHandler), ([], False, logging.StreamHandler)]) def test_silence(parser, cmdl, flag, hdlr_type): """ Log silencing generates a null handler. """