-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- allow storing config file elsewhere than users' home directories - add tests!
- Loading branch information
Showing
6 changed files
with
319 additions
and
25 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
from pathlib import Path | ||
import re | ||
|
||
from .utils import find_config_file | ||
|
||
|
||
TCL_FILENAMES = [".ritz.tcl", "ritz.tcl"] | ||
|
||
|
||
def load(filename): | ||
"""Get the contents of a .ritz.tcl config file | ||
.ritz.tcl is formatted as a tcl file. | ||
""" | ||
path = Path(filename).expanduser() | ||
with path.open("r") as f: | ||
# normalize lineends just in case | ||
lines = [line.strip() for line in f.readlines()] | ||
text = "\n".join(lines) | ||
return text | ||
|
||
|
||
def parse(text): | ||
""" | ||
Parse the text of a .ritz.tcl config file | ||
.ritz.tcl is formatted as a tcl file. | ||
A config-file with the following contents: | ||
set Secret 0123456789 | ||
set User admin | ||
set Server example.org | ||
set Port 8001 | ||
global Sortby | ||
set Sortby "upd-rev" | ||
set _Secret(dev-server) 0123456789 | ||
set _User(dev-server) admin | ||
set _Server(dev-server) example.com | ||
set _Port(dev-server) 8001 | ||
Results in the following dict: | ||
{ | ||
"default": { | ||
"Secret": "0123456789", | ||
"User": "admin", | ||
"Server": "example.org", | ||
"Port": "8001", | ||
"Sortby": "upd-rev", | ||
}, | ||
"dev-server": { | ||
"Secret": "0123456789", | ||
"User": "admin", | ||
"Server": "example.com", | ||
"Port": "8001", | ||
}, | ||
} | ||
""" | ||
lines = text.split("\n") | ||
config = {} | ||
for line in lines: | ||
_set = re.findall(r"^\s?set _?([a-zA-Z0-9]+)(?:\((.*)\))? (.*)$", line) | ||
if _set: | ||
group = _set[0][1] if _set[0][1] != "" else "default" | ||
key = _set[0][0] | ||
value = _set[0][2] | ||
|
||
if group not in config: | ||
config[group] = {} | ||
|
||
config[group][key] = value | ||
return config | ||
|
||
|
||
def normalize(tcl_config_dict): | ||
""" | ||
Standardize on snake-case key-names and break out global options | ||
Usage:: | ||
> config_dict = Normalizer.normalise(tcl_config_dict) | ||
A config-dict with the following contents:: | ||
{ | ||
"default": { | ||
"Secret": "0123456789", | ||
"User": "admin", | ||
"Server": "example.org", | ||
"Port": "8001", | ||
"Sortby": "upd-rev", | ||
}, | ||
"dev-server": { | ||
"Secret": "0123456789", | ||
"User": "admin", | ||
"Server": "example.com", | ||
"Port": "8001", | ||
}, | ||
} | ||
Results in a dict with the following contents:: | ||
{ | ||
"connections": { | ||
"default": { | ||
"secret": "0123456789", | ||
"username": "admin", | ||
"server": "example.org", | ||
"port": "8001", | ||
}, | ||
"dev_server": { | ||
"secret": "0123456789", | ||
"username": "admin", | ||
"server": "example.com", | ||
"port": "8001", | ||
}, | ||
}, | ||
"globals": { | ||
"sort_by": "upd-rev", | ||
}, | ||
} | ||
""" | ||
KEYMAP = {"Sortby": "sort_by", "User": "username"} | ||
CONNECTION_KEYS = set(("username", "secret", "server", "port")) | ||
connections = {} | ||
globals = {} | ||
for name in tcl_config_dict: | ||
connection = {} | ||
for key, value in tcl_config_dict[name].items(): | ||
key = KEYMAP.get(key, key.lower()) | ||
key = "_".join(key.split("-")) | ||
if key not in CONNECTION_KEYS: | ||
globals[key] = value | ||
else: | ||
connection[key] = value | ||
name = "_".join(name.split("-")) | ||
connections[name] = connection | ||
return {"globals": globals, "connections": connections} | ||
|
||
|
||
def generate_potential_filenames(): | ||
for filename in TCL_FILENAMES: | ||
filename = find_config_file(filename) | ||
if filename: | ||
return filename | ||
|
||
|
||
# legacy | ||
def parse_tcl_config(filename=None): | ||
if filename is None: | ||
filename = generate_potential_filenames() | ||
contents = load(filename) | ||
config = parse(contents) | ||
return config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from os import environ | ||
from pathlib import Path | ||
|
||
|
||
DEFAULT_XDG_CONFIG_HOME = Path.home() / '.config' | ||
XDG_CONFIG_HOME = environ.get('XDG_CONFIG_HOME', DEFAULT_XDG_CONFIG_HOME) | ||
VISIBLE_LOCATIONS = [XDG_CONFIG_HOME, Path('/usr/local/etc'), Path('/etc')] | ||
INVISIBLE_LOCATIONS = [Path.cwd(), Path.home()] | ||
CONFIG_DIRECTORIES = INVISIBLE_LOCATIONS + VISIBLE_LOCATIONS | ||
|
||
|
||
def find_config_file(filename): | ||
""" | ||
Look for filename in CONFIG_DIRECTORIES in order | ||
If the file isn't found in any of them, raise FileNotFoundError | ||
""" | ||
tried = [] | ||
for directory in CONFIG_DIRECTORIES: | ||
if directory in INVISIBLE_LOCATIONS: | ||
used_filename = f'.{filename}' | ||
else: | ||
used_filename = filename | ||
path = directory / used_filename | ||
tried.append(path) | ||
if path.is_file(): | ||
return path | ||
tried_paths = [str(path) for path in tried] | ||
raise FileNotFoundError(f"Looked for config in {tried_paths}, none found") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import os | ||
from pathlib import Path | ||
from tempfile import mkstemp | ||
from unittest import TestCase | ||
|
||
from zinolib.config.tcl import parse_tcl_config, normalize, parse | ||
|
||
|
||
RITZ_CONFIG = """ | ||
set Secret 0123456789 | ||
set User admin | ||
set Server example.org | ||
set Port 8001 | ||
global Sortby | ||
set Sortby "upd-rev" | ||
set _Secret(dev-server) 0123456789 | ||
set _User(dev-server) admin | ||
set _Server(dev-server) example.com | ||
set _Port(dev-server) 8001 | ||
""" | ||
|
||
|
||
def clean_config(configtext): | ||
config = "\n".join(line.strip() for line in configtext.split("\n")) | ||
return config | ||
|
||
|
||
def make_configfile(text): | ||
config = clean_config(text) | ||
fd, filename = mkstemp(text=True, suffix=".tcl") | ||
os.write(fd, bytes(config, encoding="ascii")) | ||
return filename | ||
|
||
|
||
def delete_configfile(filename): | ||
Path(filename).unlink(missing_ok=True) | ||
|
||
|
||
class ParseTclConfigTest(TestCase): | ||
def test_parse_tcl_config_missing_config_file(self): | ||
with self.assertRaises(FileNotFoundError): | ||
tcl_config_dict = parse_tcl_config("cvfgdh vghj vbjhk") | ||
|
||
def test_parse_tcl_config_empty_config_file(self): | ||
filename = make_configfile("") | ||
tcl_config_dict = parse_tcl_config(filename) | ||
self.assertEqual(tcl_config_dict, {}) | ||
delete_configfile(filename) | ||
|
||
def test_parse_tcl_config_golden_path(self): | ||
filename = make_configfile(RITZ_CONFIG) | ||
tcl_config_dict = parse_tcl_config(filename) | ||
expected = { | ||
"default": { | ||
"Port": "8001", | ||
"Secret": "0123456789", | ||
"Server": "example.org", | ||
"Sortby": '"upd-rev"', | ||
"User": "admin", | ||
}, | ||
"dev-server": { | ||
"Port": "8001", | ||
"Secret": "0123456789", | ||
"Server": "example.com", | ||
"User": "admin", | ||
}, | ||
} | ||
self.assertEqual(tcl_config_dict, expected) | ||
delete_configfile(filename) | ||
|
||
|
||
class ParseNormalizeTest(TestCase): | ||
def test_normalize_empty_dict(self): | ||
expected = {"globals": {}, "connections": {}} | ||
self.assertEqual(normalize({}), expected) | ||
|
||
def test_normalize_golden_path(self): | ||
tcl_config_dict = parse(clean_config(RITZ_CONFIG)) | ||
expected = { | ||
"globals": {"sort_by": '"upd-rev"'}, | ||
"connections": { | ||
"default": { | ||
"secret": "0123456789", | ||
"username": "admin", | ||
"server": "example.org", | ||
"port": "8001", | ||
}, | ||
"dev_server": { | ||
"secret": "0123456789", | ||
"username": "admin", | ||
"server": "example.com", | ||
"port": "8001", | ||
}, | ||
}, | ||
} | ||
self.assertEqual(normalize(tcl_config_dict), expected) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import os | ||
from pathlib import Path | ||
from tempfile import mkstemp, mkdtemp | ||
from unittest import TestCase | ||
|
||
from zinolib.config.utils import find_config_file | ||
|
||
|
||
def make_file(): | ||
fd, filename = mkstemp(dir=str(Path.cwd())) | ||
os.write(fd, b"") | ||
return filename | ||
|
||
|
||
def delete_file(filename): | ||
Path(filename).unlink(missing_ok=True) | ||
|
||
|
||
class FindConfigFileTest(TestCase): | ||
def test_find_config_file_missing_config_file(self): | ||
with self.assertRaises(FileNotFoundError): | ||
find_config_file("bcekjyfbu eyxxgyikyvub iysbiucbcsiu") | ||
|
||
def test_find_config_file_golden_path(self): | ||
filename = make_file() | ||
found_filename = find_config_file(filename) | ||
delete_file(filename) | ||
self.assertEqual(Path.cwd() / filename, found_filename) | ||
|
||
def test_find_config_file_unusuable_file(self): | ||
with self.assertRaises(FileNotFoundError): | ||
filename = mkdtemp(dir=str(Path.cwd())) | ||
found_filename = find_config_file(filename) | ||
delete_file(filename) |