Skip to content

Commit 3bdeb2e

Browse files
committed
going crazy with configs
1 parent 2df5a34 commit 3bdeb2e

File tree

6 files changed

+91
-35
lines changed

6 files changed

+91
-35
lines changed

src/snippet/cli.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import argparse
2+
import os
3+
import logging
24

35
import snippet
46

57

68
def get_cli_opts():
79
parser = argparse.ArgumentParser()
8-
parser.add_argument('--config', type=str, help='path to config file')
10+
parser.add_argument('--config', type=str,
11+
help='path to config file')
12+
parser.add_argument('dir', nargs='?', default=os.getcwd(),
13+
help='path to project root, used by relative paths in config [cwd]')
14+
parser.add_argument('-v', '--verbosity', action='count', default=0,
15+
help='increase output verbosity')
916
return parser
1017

1118

1219
def run_from_cli():
13-
parser = get_cli_opts()
14-
config_path = parser.parse_args().config
15-
snippet.main(snippet.config.get_config(config_path))
20+
args = get_cli_opts().parse_args()
21+
log_level = logging.WARNING - 10 * args.verbosity
22+
logging.basicConfig(level=log_level)
23+
snippet.main(snippet.config.get_config(
24+
config_path=args.config,
25+
project_root=args.dir,
26+
))
1627

1728

1829
if __name__ == '__main__':

src/snippet/config.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import glob
12
import logging
23
import os
34

@@ -6,10 +7,11 @@
67

78
class Config:
89
# IO
10+
project_root = '.' # the project root used for relative IO paths (set by commandline)
911
input_glob = 'tests/example/*.py'
1012
output_template = '```python\n# example: {{{name}}}\n{{{code}}}\n```\n' # a mustache template for each file
1113
output_append = False # if the output file exists, append to it
12-
output_dir = None
14+
output_dir = '.'
1315
output_file_ext = 'md'
1416

1517
# Logging
@@ -31,15 +33,41 @@ class Config:
3133
stop_on_first_failure = False # fail early
3234

3335

36+
def find_config(root):
37+
return (
38+
glob.glob(os.path.join(root, '**\*.toml'), recursive=True) +
39+
glob.glob(os.path.join(root, '**\*.cfg'), recursive=True)
40+
)
41+
42+
3443
def get_config(config_path=None, **options):
44+
config = Config()
45+
project_root = os.path.abspath(options.get('project_root', config.project_root))
46+
3547
new_options = {}
36-
if config_path or os.environ.get('SNIPPET_CONFIG_PATH'):
37-
with open(config_path) as f:
38-
config_file_contents = toml.load(f)
39-
snippet_config = config_file_contents['snippet']
40-
new_options.update(snippet_config)
48+
config_path = config_path or os.environ.get('SNIPPET_CONFIG_PATH')
49+
toml_files = [config_path] if config_path else find_config(root=project_root)
50+
for toml_file in toml_files:
51+
logging.debug('trying config from %s', toml_file)
52+
with open(toml_file) as f:
53+
try:
54+
config_file_contents = toml.load(f)
55+
except toml.TomlDecodeError as e:
56+
logging.debug('failed to load %s: %s', toml_file, e)
57+
continue
58+
snippet_config = config_file_contents.get('snippet')
59+
if snippet_config:
60+
logging.info('loading config from %s', toml_file)
61+
new_options.update(snippet_config)
62+
63+
# passed keyword args override other parameters
4164
new_options.update(options)
42-
config = Config()
65+
66+
# update the config object
4367
for k, v in new_options.items():
4468
setattr(config, k, v)
69+
70+
# validate and set IO directories that are relative to project root
71+
config.input_glob = os.path.abspath(os.path.join(config.project_root, config.input_glob))
72+
config.output_dir = os.path.abspath(os.path.join(config.project_root, config.output_dir))
4573
return config

tests/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import os
22
tmp_test_dirname = 'tmp_test_dir'
3-
tmp_test_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), tmp_test_dirname)
3+
tmp_test_dir = os.path.join(os.path.dirname(__file__), tmp_test_dirname)
4+
sample_input_dir = os.path.join(os.path.dirname(__file__), 'samples')

tests/samples/config_fixture.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```python
2+
# example: this config is itself an example
3+
input_glob = '*'
4+
5+
stop_on_first_failure = true
6+
```

tests/test_config.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import shutil
3+
import filecmp
34
import subprocess
45
import sys
56
import textwrap
@@ -8,39 +9,53 @@
89
import snippet
910

1011
from tests import tmp_test_dir
12+
from tests import sample_input_dir
1113

1214

1315
class Test(unittest.TestCase):
14-
tmpdir = tmp_test_dir
1516

1617
@classmethod
1718
def setUpClass(cls):
1819
# use a plain directory not-really-in-tmp to avoid cross-process perms issues in windows
19-
os.makedirs(cls.tmpdir)
20-
cls.tmp_fp = os.path.join(cls.tmpdir, 'config.toml')
20+
os.makedirs(tmp_test_dir)
21+
cls.tmp_fp = os.path.join(tmp_test_dir, 'config.toml')
2122
with open(cls.tmp_fp, 'w') as fh:
2223
fh.write(textwrap.dedent("""
2324
[snippet]
25+
# an example: this config is itself an example
2426
input_glob = '*'
25-
end_flag = 'custom value'
27+
2628
stop_on_first_failure = true
27-
foo = 'bar'
29+
end_flag = 'custom value'
30+
31+
foo = 'bar'
2832
""").lstrip())
2933

3034
@classmethod
3135
def tearDownClass(cls):
32-
shutil.rmtree(cls.tmpdir)
36+
shutil.rmtree(tmp_test_dir)
3337

3438
def test_config_from_file(self):
39+
# explicitly load config from a file
3540
config = snippet.config.get_config(config_path=self.tmp_fp)
3641
self.assertEqual(config.end_flag, 'custom value')
3742
self.assertEqual(config.foo, 'bar')
3843

3944
def test_config_from_cli(self):
40-
with self.assertRaises(subprocess.CalledProcessError) as ctx:
41-
subprocess.check_output(
42-
[sys.executable, '-m', 'snippet', '--config', self.tmp_fp],
43-
cwd=self.tmpdir,
44-
stderr=subprocess.STDOUT
45-
)
46-
self.assertIn('snippet.exceptions', str(ctx.exception.output))
45+
# load config when run as a module
46+
subprocess.check_call(
47+
[sys.executable, '-m', 'snippet', tmp_test_dir, '--config', self.tmp_fp],
48+
stderr=subprocess.STDOUT
49+
)
50+
51+
self.assertTrue(filecmp.cmp(
52+
os.path.join(tmp_test_dir, 'thisconfigisitselfanexample.md'),
53+
os.path.join(sample_input_dir, 'config_fixture.md'),
54+
shallow=False,
55+
))
56+
57+
def test_auto_config(self):
58+
# load config, without explicitly setting the config path
59+
config = snippet.config.get_config()
60+
self.assertEqual(config.end_flag, 'custom value')
61+
self.assertEqual(config.foo, 'bar')

tests/test_direct.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,19 @@
55
import filecmp
66

77
from tests import tmp_test_dir
8+
from tests import sample_input_dir
89

910

1011
class Test(unittest.TestCase):
11-
sample_input = os.path.join(os.path.dirname(__file__), 'samples')
12-
sample_output_dir = os.path.join(os.path.dirname(__file__), tmp_test_dir)
13-
14-
def setUp(self):
15-
if os.path.exists(self.sample_output_dir):
16-
shutil.rmtree(self.sample_output_dir)
1712

1813
def tearDown(self):
19-
shutil.rmtree(self.sample_output_dir)
14+
shutil.rmtree(tmp_test_dir)
2015

2116
def test_run(self):
2217
# writing two different languages sequentially to the same file
2318

2419
config = snippet.Config()
25-
config.output_dir = self.sample_output_dir
20+
config.output_dir = tmp_test_dir
2621
config.output_append = True
2722

2823
# only detect the python file
@@ -35,7 +30,7 @@ def test_run(self):
3530
snippet.main(config=config)
3631

3732
self.assertTrue(filecmp.cmp(
38-
os.path.join(self.sample_output_dir, 'number1.md'),
39-
os.path.join(self.sample_input, 'fixture.md'),
33+
os.path.join(tmp_test_dir, 'number1.md'),
34+
os.path.join(sample_input_dir, 'fixture.md'),
4035
shallow=False,
4136
))

0 commit comments

Comments
 (0)