Skip to content

Commit

Permalink
Merge pull request #36 from Dunedan/support-config-file
Browse files Browse the repository at this point in the history
Support using a config file for config options
  • Loading branch information
Dunedan authored Apr 30, 2024
2 parents ddcee8f + 1d357e6 commit cd00d9a
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 11 deletions.
112 changes: 111 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@

"""Tests for utility functions."""

from contextlib import redirect_stderr
from io import BytesIO, StringIO
from unittest import TestCase
from unittest.mock import patch

from hypothesis import given
from hypothesis import strategies as st

from xpartamupp.utils import LimitedSizeDict
from xpartamupp.utils import ArgumentParserWithConfigFile, LimitedSizeDict


class TestLimitedSizeDict(TestCase):
Expand All @@ -43,3 +46,110 @@ def test_max_items(self, size_limit):
self.assertFalse(0 in test_dict.values())
self.assertTrue(1 in test_dict.values())
self.assertTrue(size_limit + 1 in test_dict.values())


class TestArgumentParserWithConfigFile(TestCase):
"""Test ArgumentParser with support for config files."""

def test_missing_config_file(self):
"""Test specified, but not existing config file."""
config_file_name = "config.toml"
args = ["--config-file", config_file_name]

with patch("xpartamupp.utils.open") as file_open_mock:
file_open_mock.side_effect = FileNotFoundError()

parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)

stderr = StringIO()
with self.assertRaises(SystemExit), redirect_stderr(stderr):
parser.parse_args(args=args)
self.assertIn(f"The given configuration file \"{config_file_name}\" "
"doesn't exist.", stderr.getvalue())

file_open_mock.assert_called_once_with(config_file_name, "rb")

def test_config_file(self):
"""Test successful reading options from a config file."""
config_file_name = "config.toml"
args = ["--config-file", config_file_name]

with patch("xpartamupp.utils.open") as file_open_mock:
file_open_mock.return_value = BytesIO(b"verbosity = 2\n")

parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)
parsed_args = parser.parse_args(args=args)

file_open_mock.assert_called_once_with(config_file_name, "rb")
self.assertEqual(2, parsed_args.verbosity)

def test_config_file_invalid_option(self):
"""Test invalid options in the config file."""
config_file_name = "config.toml"
args = ["--config-file", config_file_name]

with patch("xpartamupp.utils.open") as file_open_mock:
file_open_mock.return_value = BytesIO(b'foo = "bar"\n')

parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)

stderr = StringIO()
with self.assertRaises(SystemExit), redirect_stderr(stderr):
parser.parse_args(args=args)
self.assertIn("The configuration file contains an unrecognized option: foo",
stderr.getvalue())

def test_config_file_with_cmdl_option(self):
"""Test overwriting an option in config."""
config_file_name = "config.toml"
args = ["--config-file", config_file_name, "-v"]

with patch("xpartamupp.utils.open") as file_open_mock:
file_open_mock.return_value = BytesIO(b"verbosity = 2\n")

parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)
parsed_args = parser.parse_args(args=args)

file_open_mock.assert_called_once_with(config_file_name, "rb")
self.assertEqual(1, parsed_args.verbosity)

def test_namespace(self):
"""Test functionality of the namespace parameter."""
class Namespace:
verbosity = 3

args = ["-v"]
namespace = Namespace()

with patch("xpartamupp.utils.open") as file_open_mock:
parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)
parsed_args = parser.parse_args(args=args, namespace=namespace)

file_open_mock.load.assert_not_called()
self.assertIs(namespace, parsed_args)
self.assertEqual(4, parsed_args.verbosity)

def test_config_file_and_namespace(self):
"""Test combination of config file and namespace parameter."""
class Namespace:
verbosity = 3

config_file_name = "config.toml"
args = ["--config-file", config_file_name]
namespace = Namespace()

with patch("xpartamupp.utils.open") as file_open_mock:
file_open_mock.return_value = BytesIO(b"verbosity = 2\n")

parser = ArgumentParserWithConfigFile()
parser.add_argument("-v", action="count", dest="verbosity", default=0)
parsed_args = parser.parse_args(args=args, namespace=namespace)

file_open_mock.assert_called_once_with(config_file_name, "rb")
self.assertIs(namespace, parsed_args)
self.assertEqual(2, parsed_args.verbosity)
8 changes: 4 additions & 4 deletions xpartamupp/echelon.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@

"""0ad XMPP-bot responsible for managing game ratings."""

import argparse
import asyncio
import difflib
import logging
import ssl

from argparse import ArgumentDefaultsHelpFormatter
from asyncio import Future
from collections import deque
from datetime import datetime, timedelta, timezone
Expand All @@ -41,7 +41,7 @@
from xpartamupp.elo import get_rating_adjustment
from xpartamupp.lobby_ranking import Game, Player, PlayerInfo
from xpartamupp.stanzas import BoardListXmppPlugin, GameReportXmppPlugin, ProfileXmppPlugin
from xpartamupp.utils import LimitedSizeDict
from xpartamupp.utils import ArgumentParserWithConfigFile, LimitedSizeDict

# Rating that new players should be inserted into the
# database with, before they've played any games.
Expand Down Expand Up @@ -803,8 +803,8 @@ def parse_args():
Parsed command line arguments
"""
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="EcheLOn - XMPP Rating Bot")
parser = ArgumentParserWithConfigFile(formatter_class=ArgumentDefaultsHelpFormatter,
description="EcheLOn - XMPP Rating Bot")

verbosity_parser = parser.add_mutually_exclusive_group()
verbosity_parser.add_argument("-v", action="count", dest="verbosity", default=0,
Expand Down
5 changes: 3 additions & 2 deletions xpartamupp/modbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

from xpartamupp.lobby_moderation_db import (JIDNickWhitelist, KickEvent, Moderator, MuteEvent,
UnmuteEvent)
from xpartamupp.utils import ArgumentParserWithConfigFile

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -658,8 +659,8 @@ def parse_args():
Parsed command line arguments
"""
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter,
description="ModBot - XMPP Moderation Bot")
parser = ArgumentParserWithConfigFile(formatter_class=ArgumentDefaultsHelpFormatter,
description="ModBot - XMPP Moderation Bot")

verbosity_parser = parser.add_mutually_exclusive_group()
verbosity_parser.add_argument("-v", action="count", dest="verbosity", default=0,
Expand Down
64 changes: 64 additions & 0 deletions xpartamupp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

"""Collection of utility functions used by the XMPP-bots."""

import tomllib

from argparse import ArgumentParser, Namespace
from collections import OrderedDict
from typing import Sequence


class LimitedSizeDict(OrderedDict):
Expand Down Expand Up @@ -46,3 +50,63 @@ def _check_size_limit(self):
if self.size_limit:
while len(self) > self.size_limit:
self.popitem(last=False)


class ArgumentParserWithConfigFile(ArgumentParser):
"""ArgumentParser with support for values in TOML files.
This extends the ArgumentParser class by a pre-defined
`--config-file` parameter, which allows storing config options in
TOML files, instead of providing them as command line options.
The options in the configuration file have to be named like the
destination variables of the parser arguments.
If an option is present in the configuration file and in the
command line options, the value from the command line options takes
precedence.
"""

def __init__(self, *args, **kwargs):
"""Create a parser with an option for a config file."""
super().__init__(*args, **kwargs)
self.add_argument("--config-file",
help="Path to a TOML configuration file. Options in the configuration "
"will be used as defaults for command line options and will be "
"overwritten if the command line option is provided with a "
"non-default value.")

def parse_args(self, args: Sequence[str] | None = None,
namespace: Namespace | None = None) -> Namespace:
"""Parse arguments and use values from TOML as default."""
parsed_args = super().parse_args(args, namespace)

if not parsed_args.config_file:
delattr(parsed_args, "config_file")
return parsed_args

try:
with open(parsed_args.config_file, "rb") as r:
toml_data = tomllib.load(r)
except FileNotFoundError:
self.error(
f"The given configuration file \"{parsed_args.config_file}\" doesn't exist.")

delattr(parsed_args, "config_file")

default_args = vars(super().parse_args([]))
changed_args = []

for key, value in vars(parsed_args).items():
if key in default_args and value == default_args[key]:
continue

changed_args.append(key)

for key, value in toml_data.items():
if key not in default_args:
self.error(
f"The configuration file contains an unrecognized option: {key}")
if key in changed_args:
continue
setattr(parsed_args, key, value)

return parsed_args
8 changes: 4 additions & 4 deletions xpartamupp/xpartamupp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

"""0ad XMPP-bot responsible for managing game listings."""

import argparse
import asyncio
import logging
import ssl
import time

from argparse import ArgumentDefaultsHelpFormatter
from asyncio import Future
from datetime import datetime, timedelta, timezone

Expand All @@ -34,7 +34,7 @@
from slixmpp.xmlstream.stanzabase import register_stanza_plugin

from xpartamupp.stanzas import GameListXmppPlugin
from xpartamupp.utils import LimitedSizeDict
from xpartamupp.utils import ArgumentParserWithConfigFile, LimitedSizeDict

# Number of seconds to not respond to mentions after having responded
# to a mention.
Expand Down Expand Up @@ -382,8 +382,8 @@ def parse_args():
Parsed command line arguments
"""
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="XpartaMuPP - XMPP Multiplayer Game Manager")
parser = ArgumentParserWithConfigFile(formatter_class=ArgumentDefaultsHelpFormatter,
description="XpartaMuPP - XMPP Multiplayer Game Manager")

verbosity_parser = parser.add_mutually_exclusive_group()
verbosity_parser.add_argument("-v", action="count", dest="verbosity", default=0,
Expand Down

0 comments on commit cd00d9a

Please sign in to comment.