Skip to content

Commit c32345f

Browse files
committed
Added config file support
1 parent 254f84d commit c32345f

5 files changed

Lines changed: 114 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"path": "", // starting dir to search for project root (default: CWD)
3+
"max_depth": 9, // max levels to traverse up
4+
"markers": "" // custom marker files to look for
5+
}

find-project-root/docs/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,24 @@ find-project-root --path=src --max-depth=5 --markers=.manifest.json
130130

131131
<hr>
132132

133+
## Config file
134+
135+
Run `find-project-root --init` to create `.project-root.config.json5` in your project root to set default options.
136+
137+
Example defaults:
138+
139+
```json5
140+
{
141+
"path": "", // starting dir to search for project root (default: CWD)
142+
"max_depth": 9, // max levels to traverse up
143+
"markers": "" // custom marker files to look for
144+
}
145+
```
146+
147+
_Note: CLI arguments always override config file._
148+
149+
<hr>
150+
133151

134152
## MIT License
135153

find-project-root/src/find_project_root/cli/lib/init.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from sys_lang import get_sys_lang
55

6-
from . import data
6+
from . import data, log
77

88
def cli() -> sn:
99
from . import env, language, settings
@@ -12,3 +12,62 @@ def cli() -> sn:
1212
language.generate_random_lang(excludes=['en']) if env.is_debug_mode() else get_sys_lang())
1313
settings.load(cli)
1414
return cli
15+
16+
def config_file(cli: sn) -> None: # for --init
17+
18+
# Init target_path
19+
import find_project_root
20+
project_root = find_project_root(max_depth=20) # type: ignore
21+
if project_root:
22+
config_dir, in_project_root = Path(project_root), True
23+
else:
24+
log.warn(cli.msgs.warn_NO_PROJECT_ROOT_FOUND)
25+
user_resp = input(f'{cli.msgs.prompt_INIT_CONFIG_HERE_ANYWAY}? (y/N): ').strip().lower()
26+
if not user_resp.startswith('y') : return
27+
config_dir, in_project_root = Path.cwd(), False
28+
target_path = config_dir / f'.{ cli.short_name or cli.name }.config.json5'
29+
30+
# Handle existing file
31+
if target_path.exists():
32+
if cli.config.force:
33+
log.info(f'{cli.msgs.log_OVERWRITING_CONFIG_AT} {target_path}...\n')
34+
else:
35+
log.warn(f'{cli.msgs.warn_CONFIG_EXISTS_AT} {target_path}. {cli.msgs.log_SKIPPING} init...')
36+
log.tip(f'{cli.msgs.tip_PASS_FORCE_TO_OVERWRITE}.')
37+
return
38+
39+
# Fetch/write from jsDelivr
40+
if not getattr(config_file, 'cached_default', None):
41+
from . import jsdelivr, url
42+
jsd_url = f'{jsdelivr.create_pkg_ver_url(cli)}/{target_path.name}'
43+
log.debug(f'{log.colors.bw}{jsd_url}')
44+
config_file.cached_default = url.get(jsd_url)
45+
data.file.write(str(target_path), config_file.cached_default)
46+
log.success(f'{cli.msgs.log_DEFAULT_CONFIG_CREATED_AT} {target_path}')
47+
if not in_project_root : log.tip(f'{cli.msgs.tip_MOVE_CONFIG_TO_ROOT}.')
48+
49+
def config_filepath(cli: sn) -> None: # for settings.load()
50+
51+
# Check --config <path>
52+
if getattr(cli.config, 'config', ''):
53+
cli.config_filepath = Path(cli.config.config).resolve()
54+
if cli.config_filepath.exists():
55+
log.debug(f'Config file found: {cli.config_filepath}')
56+
return
57+
else:
58+
log.warn(f'{cli.msgs.warn_SPECIFIED_CONFIG} {cli.config_filepath} {cli.msgs.warn_NOT_FOUND}')
59+
60+
# Search upwards
61+
possible_config_filenames = [
62+
f'{prefix}{name}.config.json{suffix}'
63+
for prefix in ['.', ''] for name in [cli.short_name, cli.name] for suffix in ['5', '', 'c']
64+
]
65+
current_dir = Path.cwd().resolve()
66+
for parent in [current_dir, *current_dir.parents]:
67+
for filename in possible_config_filenames:
68+
possible_config_filepath = parent / filename
69+
if possible_config_filepath.exists():
70+
cli.config_filepath = possible_config_filepath
71+
return
72+
73+
cli.config_filepath = None

find-project-root/src/find_project_root/cli/lib/log.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def error(msg: str, *args, **kwargs) -> None : print(f'\n{colors.br}ERROR: {msg.
2222
def help_cmd(cli: sn) -> None : info(f"{cli.msgs.log_TYPE} '{cli.cmds[0]} --help' {cli.msgs.log_FOR_AVAIL_OPTIONS}\n")
2323
def info(msg: str, *args, end: str = '', **kwargs) -> None:
2424
print(f'\n{colors.by}{msg.format(*args, **kwargs)}{colors.nc}', end=end)
25+
def init_cmd(cli: sn) -> None:
26+
info(f"{cli.msgs.log_TYPE} '{cli.cmds[0]} --init' {cli.msgs.log_TO_CREATE_DEFAULT_CONFIG}\n")
2527
def line_break() : print()
2628
def overwrite_print(msg: str, *args, **kwargs) -> None:
2729
sys.stdout.write('\r' + msg.format(*args, **kwargs).ljust(terminal_width)[:terminal_width])

find-project-root/src/find_project_root/cli/lib/settings.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from types import SimpleNamespace as sn
33
from typing import Optional
44

5-
from . import log, string, url
5+
from . import init, log, string, url
66

77
controls = sn(
88
path=sn(
@@ -11,6 +11,12 @@
1111
args=['-d', '--max-depth'], action='store', type=int, default_val=9, metavar='N'),
1212
markers=sn(
1313
args=['-m', '--markers'], metavar='MARKER', type=str, parser='csv'),
14+
config=sn(
15+
args=['--config'], type=str),
16+
init=sn(
17+
args=['-i', '--init'],
18+
action='store_true', subcmd='true', exit=True, handler=lambda cli: init.config_file(cli)
19+
),
1420
help=sn(
1521
args=['-h', '--help'], action='help'),
1622
version=sn(
@@ -49,6 +55,28 @@ def load(cli: sn) -> None:
4955
if ctrl_key.startswith('legacy_') : continue
5056
if not hasattr(ctrl, 'help') : ctrl.help = getattr(cli.msgs, f'help_{ctrl_key.upper()}')
5157

58+
# Load from config file
59+
init.config_filepath(cli)
60+
if getattr(cli, 'config_filepath', None):
61+
config_data = data.json.read(cli.config_filepath)
62+
for config_key in config_data:
63+
if not get_canonical_key(config_key):
64+
log.cmd_docs_url_exit(cli,
65+
f'{cli.msgs.err_INVALID_KEY} {config_key!r} {cli.msgs.err_FOUND_IN}'
66+
f'\n{log.colors.gry}{cli.config_filepath}',
67+
cmd='init')
68+
for config_key, config_val in config_data.items():
69+
canonical_key = get_canonical_key(config_key)
70+
if canonical_key and config_key != canonical_key: # re-map config_key -> canonical_key
71+
log.warn_legacy_option(cli, config_key, source='config')
72+
if is_neg_key(config_key) != is_neg_key(canonical_key):
73+
config_val = not config_val # flip bool val of opposite keys first
74+
config_key = canonical_key
75+
setattr(cli.config, config_key, config_val)
76+
log.debug(f'Config file loaded! {log.colors.dg}{len(config_data)} keys processed', cli)
77+
else:
78+
log.debug('No config file found.')
79+
5280
# Parse CLI args (overriding config file loads)
5381
argp = argparse.ArgumentParser(description=cli.description, add_help=False)
5482
valid_argparse_kwargs = {

0 commit comments

Comments
 (0)