
# zdbg
# Copyright (c) Stacy Prowell (sprowell@gmail.com)
# All rights reserved.
# This software is licensed under the MIT License. See LICENSE for details.

"""
zdbg: A lightweight (nearly zero cost) debugging helper.

Public API:
    DEBUG: set[str]
    debug(msg: Any) -> None
    error(msg: Any) -> None
    configure(options: Iterable[str] | None, ...) -> None

    
Overview
========
This module provides two functions: `error` and `debug`. The error function
writes an error message in all cases, while the debug function writes a
debugging message only when debugging is enabled, and is explicitly a no-op
otherwise. This is configured at import time by reading the `DEBUG`
environment variable (or another; see Environment Variable, below), which
should be a comma-separated list of options.

The following options are supported.

    debug .......... Enable debug output. (1 is a synonym.)
    context ........ Include file:line for each debug message.
    nocolor ........ Disable ANSI colors.
    timestamp ...... Include an ISO-like timestamp.
    debugout ....... Specify a file path for debugging output
    noflush_debug .. Do not flush debug output after each message.
    noflush_error .. Do not flush error output after each message.
    check .......... Configure debugging, write configuration, and exit.

By default, color is used when the corresponding output stream is a TTY. Any
other options can be passed, and these are available at runtime by checking
the `DEBUG` set.

When color is enabled, error messages are written in bold and tagged in red,
and debug messages are tagged in green.

By default, error messages are written to standard error, and debug messages
are written to standard output. This can be changed by passing different
output streams to the `configure()` function.

The debug output can be re-directed to a file using the `debugout` option.
Follow this with an equal sign and the path to the desired output file. For
example, to write debug output to `debug.log`, you would set the environment
variable as follows.

    export DEBUG="debug,debugout=debug.log,noflushdebug,context"

You will probably want to include `noflush_debug` as well, to avoid flushing
the output stream after every debug message, and enable context (as is done
in the above example).

By default the output streams are flushed after every message. If you do not
want that, you can disable it with the `noflush_debug` and `noflush_error`
options.


General Usage
=============
To use this module, import it.

    import zdbg

To write an error message, use `zdbg.error("message")`. Likewise, to write a
debug message use `zdbg.debug("message")`. The debug message will only be
written if debugging is enabled, but if the message construction is costly,
you have two choices. First, you can check for the `debug` option in the
`DEBUG` set before calling `zdbg.debug()`.

    if 'debug' in zdbg.DEBUG:
        zdbg.debug(expensive_function())

Second, you can use the `debug_lazy()` function, which takes a callable that
returns the message. This callable is only invoked if debugging is enabled.

Both methods avoid the cost of calling `expensive_function()` when debugging
is not enabled.


Runtime Configuration
=====================
You can use the `configure()` function to change the debugging configuration
at runtime. For example, to enable debugging with context information, you
can do the following.

    import zdbg

    zdbg.configure(["debug", "context"])
    zdbg.debug("This will be printed with context")

    
Runtime Checks
==============
You can use the `DEBUG` set to check for other options at runtime. For example,
if you have a special debugging option called `fred`, you can check for it as
follows.

    if 'fred' in zdbg.DEBUG:
        zdbg.debug("Fred debugging is enabled.")

From the command line you might invoke the program (with this option) as follows.

    DEBUG=debug,fred python3 myprogram.py

    
Configuration
=============
To configure debugging at runtime, call the `configure()` function with
a list of options (as above). To re-parse from the environment variable, pass
`None` to the function. Note that the configuration is global, so this will
impact all uses of the `debug` and `error` functions.


Command Line Usage
==================
You can set the `DEBUG` environment variable from the command line in BASH and
ZSH as follows.

    export DEBUG="debug,context,timestamp"

To set it for just the next command, include the setting in front of the
command as follows.

    DEBUG=debug,context,timestamp python3 myprogram.py

    
Environment Variable
====================
If you don't want to use `DEBUG` as the environment variable, you can
configure from a different one using `configure()`. For example, to use
`ZDBG_DEBUG` instead of `DEBUG`, you can do the following at the start of your
program.

    import zdbg
    zdbg.configure(None, envvar="ZDBG_DEBUG")

    
Checking Options
================
If your output isn't what you expect, you can check the current options by adding
`check` to the options list. This will print the current configuration to standard
output, along with an example debug and example error message, and then exit.

NB: This only works when options are set from an environment variable. If you are
setting options at runtime directly, and not via an environment variable, this
will have no effect, and will be discarded from the options list. This is to
prevent the program from crashing at runtime because of a stray 'check' option.

    DEBUG=debug,check,debugout=out,noflush_debug python3 examples/perftest.py 
    zdbg configuration check:
    Configured from environment variable 'DEBUG'
    DEBUG = {'debugout=out', 'debug', 'noflush_debug', 'check'}
    - debugging .............. selected
    - color debug messages ... disabled
    - color error messages ... selected
    - include context ........ disabled
    - include timestamp ...... disabled
    - flush debug stream ..... disabled
    - flush error stream ..... selected
    - debug written to ....... out
    - error written to ....... <stderr>
    Example debug/error output, given configuration:
    Error: This is an error message

Note that in the above, an example debug message is not printed to standard
output, but is logged in the file `out`, as requested.
    
Performance
===========
When debugging is disabled, calls to `zdbg.debug()` are effectively no-ops, but
in order to support runtime configuration, there is additional overhead associated
with them. You can avoid this overhead by instead using `debug_root()`, which
avoids the runtime configuration check. However, this means that you cannot change
the debugging configuration at runtime for calls to `debug_root()`; you get what
the configuration was at import time.

The `perftest.py` script in `examples` shows how these different approaches
impact the overhead associated with debugging, comparing the cost of a debugging
statement to the baseline of ten floating point operations. This small baseline
was chosen to amplify the results, especially when debugging is disabled.

"""

from .core import DEBUG, configure, debug, error, debug_root, debug_lazy

__all__ = ["DEBUG", "configure", "debug", "error", "debug_root", "debug_lazy"]

# Optional: package metadata
__version__ = "1.1.0"
__author__ = "Stacy Prowell"
__license__ = "MIT"
