Skip to content

Commit

Permalink
Refactor config parsing
Browse files Browse the repository at this point in the history
- allow storing config file elsewhere than users' home directories
- add tests!
  • Loading branch information
hmpf committed Oct 12, 2023
1 parent 0c80bef commit 917e4bf
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 25 deletions.
Empty file added src/zinolib/config/__init__.py
Empty file.
157 changes: 157 additions & 0 deletions src/zinolib/config/tcl.py
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
29 changes: 29 additions & 0 deletions src/zinolib/config/utils.py
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")
26 changes: 1 addition & 25 deletions src/zinolib/ritz.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,11 @@
from datetime import datetime, timedelta
import errno
from time import mktime
import re
from os.path import expanduser
from typing import NamedTuple
import codecs
import select

from .config.tcl import parse_tcl_config
from .utils import windows_codepage_cp1252, generate_authtoken


Expand Down Expand Up @@ -170,29 +169,6 @@ def _decode_history(logarray):
return ret


# def parse_tcl_config(filename: str | Path):
def parse_tcl_config(filename):
"""Parse a .ritz.tcl config file
Used to fetch a users connection information to connect to zino
.ritz.tcl is formatted as a tcl file.
"""
config = {}
with open(expanduser(filename), "r") as f:
for line in f.readlines():
_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


class Case:
"""Zino case element
Expand Down
98 changes: 98 additions & 0 deletions tests/test_config_tcl.py
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)
34 changes: 34 additions & 0 deletions tests/test_config_utils.py
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)

0 comments on commit 917e4bf

Please sign in to comment.