Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: --add-timestamp #323

Merged
merged 3 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion nox/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def main() -> None:
print(metadata.version("nox"), file=sys.stderr)
return

setup_logging(color=args.color, verbose=args.verbose)
setup_logging(
color=args.color, verbose=args.verbose, add_timestamp=args.add_timestamp
)

# Execute the appropriate tasks.
exit_code = workflow.execute(
Expand Down
9 changes: 9 additions & 0 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ def _session_completer(
help="Logs the output of all commands run including commands marked silent.",
noxfile=True,
),
_option_set.Option(
"add_timestamp",
"-ts",
"--add-timestamp",
group=options.groups["secondary"],
action="store_true",
help="Adds a timestamp to logged output.",
noxfile=True,
),
_option_set.Option(
"default_venv_backend",
"-db",
Expand Down
87 changes: 66 additions & 21 deletions nox/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,53 @@
OUTPUT = logging.DEBUG - 1


class NoxFormatter(ColoredFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super(NoxFormatter, self).__init__(*args, **kwargs)
def _get_format(colorlog: bool, add_timestamp: bool) -> str:
if colorlog:
if add_timestamp:
return "%(cyan)s%(name)s > [%(asctime)s] %(log_color)s%(message)s"
else:
return "%(cyan)s%(name)s > %(log_color)s%(message)s"
else:
if add_timestamp:
return "%(name)s > [%(asctime)s] %(message)s"
else:
return "%(name)s > %(message)s"


class NoxFormatter(logging.Formatter):
def __init__(self, add_timestamp: bool = False) -> None:
super().__init__(fmt=_get_format(colorlog=False, add_timestamp=add_timestamp))
self._simple_fmt = logging.Formatter("%(message)s")

def format(self, record: Any) -> str:
if record.levelname == "OUTPUT":
return self._simple_fmt.format(record)
return super(NoxFormatter, self).format(record)
return super().format(record)


class NoxColoredFormatter(ColoredFormatter):
def __init__(
self,
datefmt: Any = None,
style: Any = None,
log_colors: Any = None,
reset: bool = True,
secondary_log_colors: Any = None,
add_timestamp: bool = False,
) -> None:
super().__init__(
fmt=_get_format(colorlog=True, add_timestamp=add_timestamp),
datefmt=datefmt,
style=style,
log_colors=log_colors,
reset=reset,
secondary_log_colors=secondary_log_colors,
)

def format(self, record: Any) -> str:
if record.levelname == "OUTPUT":
return self._simple_fmt.format(record)
return super().format(record)


class LoggerWithSuccessAndOutput(logging.getLoggerClass()): # type: ignore
Expand All @@ -55,23 +93,9 @@ def output(self, msg: str, *args: Any, **kwargs: Any) -> None:
logger = cast(LoggerWithSuccessAndOutput, logging.getLogger("nox"))


def setup_logging(color: bool, verbose: bool = False) -> None: # pragma: no cover
"""Setup logging.

Args:
color (bool): If true, the output will be colored using
colorlog. Otherwise, it will be plaintext.
"""
root_logger = logging.getLogger()
if verbose:
root_logger.setLevel(OUTPUT)
else:
root_logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()

def _get_formatter(color: bool, add_timestamp: bool) -> logging.Formatter:
if color is True:
formatter = NoxFormatter(
"%(cyan)s%(name)s > %(log_color)s%(message)s",
return NoxColoredFormatter(
reset=True,
log_colors={
"DEBUG": "cyan",
Expand All @@ -81,10 +105,31 @@ def setup_logging(color: bool, verbose: bool = False) -> None: # pragma: no cov
"CRITICAL": "red,bg_white",
"SUCCESS": "green",
},
style="%",
secondary_log_colors=None,
add_timestamp=add_timestamp,
)
else:
return NoxFormatter(add_timestamp=add_timestamp)


handler.setFormatter(formatter)
def setup_logging(
color: bool, verbose: bool = False, add_timestamp: bool = False
) -> None: # pragma: no cover
"""Setup logging.

Args:
color (bool): If true, the output will be colored using
colorlog. Otherwise, it will be plaintext.
"""
root_logger = logging.getLogger()
if verbose:
root_logger.setLevel(OUTPUT)
else:
root_logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()

handler.setFormatter(_get_formatter(color, add_timestamp))
root_logger.addHandler(handler)

# Silence noisy loggers
Expand Down
38 changes: 38 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import logging
from unittest import mock

import pytest

from nox import logger


Expand All @@ -39,6 +41,7 @@ def test_formatter(caplog):

logs = [rec for rec in caplog.records if rec.levelname in ("INFO", "OUTPUT")]
assert len(logs) == 1
assert not hasattr(logs[0], "asctime")

caplog.clear()
with caplog.at_level(logger.OUTPUT):
Expand All @@ -52,3 +55,38 @@ def test_formatter(caplog):
assert len(logs) == 1
# Make sure output level log records are not nox prefixed
assert "nox" not in logs[0].message


@pytest.mark.parametrize(
"color",
[
# This currently fails due to some incompatibility between caplog and colorlog
# that causes caplog to not collect the asctime from colorlog.
pytest.param(True, id="color", marks=pytest.mark.xfail),
pytest.param(False, id="no-color"),
],
)
def test_no_color_timestamp(caplog, color):
logger.setup_logging(color=color, add_timestamp=True)
caplog.clear()
with caplog.at_level(logging.DEBUG):
logger.logger.info("bar")
logger.logger.output("foo")

logs = [rec for rec in caplog.records if rec.levelname in ("INFO", "OUTPUT")]
assert len(logs) == 1
assert hasattr(logs[0], "asctime")

caplog.clear()
with caplog.at_level(logger.OUTPUT):
logger.logger.info("bar")
logger.logger.output("foo")

logs = [rec for rec in caplog.records if rec.levelname != "OUTPUT"]
assert len(logs) == 1
assert hasattr(logs[0], "asctime")

logs = [rec for rec in caplog.records if rec.levelname == "OUTPUT"]
assert len(logs) == 1
# no timestamp for output
assert not hasattr(logs[0], "asctime")