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

Python logging can be configured using a json file #89

Merged
merged 1 commit into from
Jan 6, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ not use `-` when using this option.
hab --save-prefs dump project_a/Seq001/S0010
```

User prefs are stored in the `%LOCALAPPDATA%` folder on Windows and in the user's
home directory on other platforms.

## Installing

Hab is installed using pip. It requires python 3.6 or above. It's recommended
Expand Down Expand Up @@ -1054,6 +1057,15 @@ will be removed. Alternatively if you use the `--dump-scripts` flag on hab comma
that write scripts, to make it print the contents of every file to the shell
instead of writing them to disk.

## Logging configuration

Hab uses python's logging module to output a ton of debugging information. For the
most part you can control the output using the `hab -v ...` verbosity option.
However if you need more fine grained control you can create a `.hab_logging_prefs.json`
file next to your user [user prefs](#user-prefs) file. The cli also supports passing
the path to a configuration file using `hab --logging-config [path/to/file.json]`
that is used instead of the default file if pased.

# Caveats

* Using `hab activate` in the command prompt is disabled. Batch doesn't have a function
Expand Down
30 changes: 22 additions & 8 deletions hab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import click
from colorama import Fore

from . import Resolver, Site, __version__
from . import Resolver, Site, __version__, utils
from .parsers.unfrozen_config import UnfrozenConfig
from .utils import decode_freeze, dumps_json, encode_freeze, json, verbosity_filter

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -110,15 +109,15 @@ def type_cast_value(self, ctx, value):
return uri_check.uri
# User passed a frozen hab string
if re.match(r"^v\d+:", value):
return decode_freeze(value)
return utils.decode_freeze(value)

# If its not a string, convert to a Path object, if the path exists,
# return the extracted dictionary assuming it was a json file.
try:
cpath = click.Path(path_type=Path, file_okay=True, resolve_path=True)
data = cpath.convert(value, None, ctx=ctx)
if data.exists():
return json.load(data.open())
return utils.json.load(data.open())
except ValueError:
self.fail(f"{value!r} is not a valid frozen file path.", ctx)

Expand Down Expand Up @@ -267,7 +266,7 @@ def set_ctx_instance(cls, ctx, key, value):
logger.warning("[Optimization warning]: Resetting ctx resolver.")
ctx.obj._resolver = None

# Ensure verbosity is properly respected
# Ensure logging settings are properly respected
if key == "verbosity":
global _verbose_errors

Expand All @@ -277,6 +276,10 @@ def set_ctx_instance(cls, ctx, key, value):

level = [logging.WARNING, logging.INFO, logging.DEBUG][value]
logging.basicConfig(level=level)
elif key == "logging_config":
if value is not None:
value = Path(value)
utils.Platform.configure_logging(value)

return value

Expand Down Expand Up @@ -375,6 +378,17 @@ def get_command(self, ctx, name):
help="Increase the verbosity of the output. Can be used up to 3 times. "
"This also enables showing a full traceback if an exception is raised.",
)
@click.option(
"--logging-config",
callback=SharedSettings.set_ctx_instance,
# Note: Using eager makes it so logging is configured as early as possible
# based on this argument.
is_eager=True,
type=click.Path(file_okay=True, resolve_path=True),
help="Path to json file defining a logging configuration based on "
"logging.config.dictConfig. If not specified uses .hab_logging_prefs.json "
"next to user prefs file if it exists.",
)
@click.option(
"--script-dir",
callback=SharedSettings.set_ctx_instance,
Expand Down Expand Up @@ -548,7 +562,7 @@ def echo_line(line):
if report_type in ("uris", "forest"):
click.echo(f'{Fore.YELLOW}{" URIs ".center(50, "-")}{Fore.RESET}')
# Filter out any URI's hidden by the requested verbosity level
with verbosity_filter(resolver, verbosity):
with utils.verbosity_filter(resolver, verbosity):
for line in resolver.dump_forest(resolver.configs):
echo_line(line)
if report_type in ("versions", "forest"):
Expand Down Expand Up @@ -579,9 +593,9 @@ def echo_line(line):
# This is a seperate set of if/elif/else statements than from above.
# I became confused while reading so decided to add this reminder.
if format_type == "freeze":
ret = encode_freeze(ret.freeze(), site=resolver.site)
ret = utils.encode_freeze(ret.freeze(), site=resolver.site)
elif format_type == "json":
ret = dumps_json(ret.freeze(), indent=2)
ret = utils.dumps_json(ret.freeze(), indent=2)
elif format_type == "versions":
ret = "\n".join([v.name for v in ret.versions])
else:
Expand Down
36 changes: 32 additions & 4 deletions hab/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import errno
import json as _json
import logging.config
import ntpath
import os
import re
Expand Down Expand Up @@ -403,6 +404,33 @@ class BasePlatform(ABC):
_name = None
_sep = ":"

@classmethod
def configure_logging(cls, filename=None):
"""Update the logging configuration with the contents of this json file
if exists.

See https://docs.python.org/3/library/logging.config.html#dictionary-schema-details
for details on how to construct this file.

You will most likely want to enable incremental so it doesn't fully reset
the logging basicConfig.

Example:
{"incremental": True,
"loggers": {"": {"level": 30}, "hab.parsers": {"level": 10}},
"version": 1}
"""
if filename is None:
filename = cls.user_prefs_filename(".hab_logging_prefs.json")

if not filename.exists():
return False

with filename.open() as fle:
cfg = json.load(fle)
logging.config.dictConfig(cfg)
return True

@classmethod
@abstractmethod
def check_name(cls, name):
Expand Down Expand Up @@ -496,9 +524,9 @@ def system(cls):
return "linux"

@classmethod
def user_prefs_filename(cls):
def user_prefs_filename(cls, filename=".hab_user_prefs.json"):
"""Returns the filename that contains the hab user preferences."""
return Path.home() / ".hab_user_prefs.json"
return Path.home() / filename


class WinPlatform(BasePlatform):
Expand Down Expand Up @@ -545,9 +573,9 @@ def pathsep(cls, ext=None, key=None):
return cls._sep

@classmethod
def user_prefs_filename(cls):
def user_prefs_filename(cls, filename=".hab_user_prefs.json"):
"""Returns the filename that contains the hab user preferences."""
return Path(os.path.expandvars("$LOCALAPPDATA")) / ".hab_user_prefs.json"
return Path(os.path.expandvars("$LOCALAPPDATA")) / filename


class LinuxPlatform(BasePlatform):
Expand Down
48 changes: 48 additions & 0 deletions tests/test_user_prefs.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
import datetime
import json
import logging

import pytest

from hab import user_prefs, utils


def test_user_prefs_filename():
"""Check that user_prefs_filename generates the expected file paths."""
# Check the default for filename
path = utils.Platform.user_prefs_filename()
assert path.name == ".hab_user_prefs.json"

# Check overriding filename
new = utils.Platform.user_prefs_filename(filename="test.json")
assert new == path.parent / "test.json"


def test_configure_logging(monkeypatch, tmpdir):
# Ensure we can read/write logging prefs, but using the test dir.
monkeypatch.setenv("HOME", str(tmpdir))
monkeypatch.setenv("LOCALAPPDATA", str(tmpdir))

logger = logging.getLogger("hab.test")
default = utils.Platform.user_prefs_filename(".hab_logging_prefs.json")
custom = tmpdir / "test.json"

logging_cfg = {
"version": 1,
"incremental": True,
"loggers": {"hab.test": {"level": 10}},
}

# The default file doesn't exist yet, no configuration is loaded
assert not utils.Platform.configure_logging()
# The logger's level is not configured yet
assert logger.level == 0

# Create the configuration and ensure it gets loaded
with default.open("w") as fle:
json.dump(logging_cfg, fle)
assert utils.Platform.configure_logging()
# The logger had its level set to 10 by the config
assert logger.level == 10

# Check that passing a filename is respected
logging_cfg["loggers"]["hab.test"]["level"] = 30
with custom.open("w") as fle:
json.dump(logging_cfg, fle)
assert utils.Platform.configure_logging(filename=custom)
# The logger had its level set to 10 by the config
assert logger.level == 30


def test_filename(resolver, tmpdir):
prefs = resolver.user_prefs(load=True)
# Defaults to Platform path
Expand Down