Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using a config file for config options #36

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading