Skip to content

Commit 8a830a6

Browse files
authored
Merge pull request #15 from dwhswenson/logging
Add --log (logging conf) option to main command
2 parents 63e65bf + 107b1ac commit 8a830a6

File tree

3 files changed

+113
-14
lines changed

3 files changed

+113
-14
lines changed

paths_cli/cli.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55
# builds off the example of MultiCommand in click's docs
66
import collections
7+
import logging
8+
import logging.config
79
import os
810

911
import click
@@ -36,16 +38,30 @@ def __init__(self, *args, **kwargs):
3638
self.plugin_folders.append(folder)
3739

3840
plugin_files = self._list_plugin_files(self.plugin_folders)
39-
self.plugins = self._load_plugin_files(plugin_files)
41+
plugins = self._load_plugin_files(plugin_files)
4042

4143
self._get_command = {}
4244
self._sections = collections.defaultdict(list)
43-
for plugin in self.plugins:
44-
self._get_command[plugin.name] = plugin.func
45-
self._sections[plugin.section].append(plugin.name)
45+
self.plugins = []
46+
for plugin in plugins:
47+
self._register_plugin(plugin)
4648

4749
super(OpenPathSamplingCLI, self).__init__(*args, **kwargs)
4850

51+
def _register_plugin(self, plugin):
52+
self.plugins.append(plugin)
53+
self._get_command[plugin.name] = plugin.func
54+
self._sections[plugin.section].append(plugin.name)
55+
56+
def _deregister_plugin(self, plugin):
57+
# mainly used in testing
58+
self.plugins.remove(plugin)
59+
del self._get_command[plugin.name]
60+
self._sections[plugin.section].remove(plugin.name)
61+
62+
def plugin_for_command(self, command_name):
63+
return {p.name: p for p in self.plugins}[name]
64+
4965
@staticmethod
5066
def _list_plugin_files(plugin_folders):
5167
def is_plugin(filename):
@@ -118,16 +134,17 @@ def format_commands(self, ctx, formatter):
118134
openpathsampling strip-snapshots --help
119135
"""
120136

121-
OPS_CLI = OpenPathSamplingCLI(
122-
name="openpathsampling",
123-
help=_MAIN_HELP,
124-
context_settings=CONTEXT_SETTINGS
125-
)
126-
127-
def main(): # no-cov
128-
OPS_CLI()
137+
@click.command(cls=OpenPathSamplingCLI, name="openpathsampling",
138+
help=_MAIN_HELP, context_settings=CONTEXT_SETTINGS)
139+
@click.option('--log', type=click.Path(exists=True, readable=True),
140+
help="logging configuration file")
141+
def main(log):
142+
if log:
143+
logging.config.fileConfig(log, disable_existing_loggers=False)
144+
# TODO: if log not given, check for logging.conf in .openpathsampling/
129145

146+
logger = logging.getLogger(__name__)
147+
logger.debug("About to run command") # TODO: maybe log invocation?
130148

131149
if __name__ == '__main__': # no-cov
132-
main()
133-
# print("list commands:", cli.list_commands())
150+
cli()

paths_cli/tests/null_command.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import logging
2+
import click
3+
@click.command(
4+
'null-command',
5+
short_help="Do nothing (testing)"
6+
)
7+
def null_command():
8+
logger = logging.getLogger(__name__)
9+
logger.info("Running null command")
10+
11+
CLI = null_command
12+
SECTION = "Workflow"
13+
14+
class NullCommandContext(object):
15+
"""Context that registers/deregisters the null command (for tests)"""
16+
def __init__(self, cli):
17+
self.plugin = cli._load_plugin_files([__file__])[0]
18+
self.cli = cli
19+
20+
def __enter__(self):
21+
self.cli._register_plugin(self.plugin)
22+
23+
def __exit__(self, type, value, traceback):
24+
self.cli._deregister_plugin(self.plugin)

paths_cli/tests/test_cli.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import pytest
2+
from click.testing import CliRunner
3+
4+
import logging
5+
6+
from paths_cli.cli import *
7+
from .null_command import NullCommandContext
8+
9+
class TestOpenPathSamplingCLI(object):
10+
def setup(self):
11+
# TODO: patch out the directory to fake the plugins
12+
self.cli = OpenPathSamplingCLI()
13+
14+
def test_plugins(self):
15+
pytest.skip()
16+
pass
17+
18+
def test_get_command(self):
19+
# test renamings
20+
pytest.skip()
21+
pass
22+
23+
def test_format_commands(self):
24+
pytest.skip()
25+
# use a mock to get the formatter
26+
# test that it skips a section if it is empty
27+
pass
28+
29+
@pytest.mark.parametrize('with_log', [True, False])
30+
def test_main_log(with_log):
31+
logged_stdout = "About to run command\n"
32+
cmd_stdout = "Running null command\n"
33+
logfile_text = "\n".join([
34+
"[loggers]", "keys=root", "",
35+
"[handlers]", "keys=std", "",
36+
"[formatters]", "keys=default", "",
37+
"[formatter_default]", "format=%(message)s", "",
38+
"[handler_std]", "class=StreamHandler", "level=NOTSET",
39+
"formatter=default", "args=(sys.stdout,)", ""
40+
"[logger_root]", "level=DEBUG", "handlers=std"
41+
])
42+
runner = CliRunner()
43+
invocation = {
44+
True: ['--log', 'logging.conf', 'null-command'],
45+
False: ['null-command']
46+
}[with_log]
47+
expected = {
48+
True: logged_stdout + cmd_stdout,
49+
False: ""
50+
}[with_log]
51+
with runner.isolated_filesystem():
52+
with open("logging.conf", mode='w') as log_conf:
53+
log_conf.write(logfile_text)
54+
55+
with NullCommandContext(main):
56+
result = runner.invoke(main, invocation)
57+
found = result.stdout_bytes
58+
assert found.decode('utf-8') == expected

0 commit comments

Comments
 (0)