<gh_stars>0

import argparse
import configparser
import functools
import textwrap

import pytest

import configfetch

fetch_ = configfetch.fetch

fetch = functools.partial(
    configfetch.fetch, option_builder=configfetch.FiniOptionBuilder)


def f(string):
    return textwrap.dedent(string.strip('\n'))


def _get_action(conf, option_strings):
    parser = argparse.ArgumentParser(prog='test')
    conf.build_arguments(parser)
    # parser.print_help()
    for action in parser._get_optional_actions():
        if option_strings in action.option_strings:
            return action
    raise ValueError('No action with option_strings: %r' % option_strings)


class TestEscapedSplit:

    def check_comma(self, value, expected):
        ret = configfetch._parse_comma(value)
        assert ret == expected

    def check_line(self, value, expected):
        ret = configfetch._parse_line(value)
        assert ret == expected

    def test_comma(self):
        self.check_comma('aaaa', ['aaaa'])
        self.check_comma(r'\aaaa', [r'\aaaa'])
        self.check_comma(r'aa\aa', [r'aa\aa'])
        self.check_comma(r'aaa\a', [r'aaa\a'])
        self.check_comma(r'aaaa\\', [r'aaaa\\'])

        self.check_comma(r'aa\\aa', [r'aa\\aa'])
        self.check_comma(r'aa\\\aa', [r'aa\\\aa'])

        self.check_comma('aa, bb', ['aa', 'bb'])
        self.check_comma(r'aa\, bb', ['aa, bb'])
        self.check_comma(r'aa\\, bb', [r'aa\, bb'])
        self.check_comma(r'aa\\\, bb', [r'aa\\, bb'])

        self.check_comma(r'aa\a, bb', [r'aa\a', 'bb'])
        self.check_comma(r'aa\\a, bb', [r'aa\\a', 'bb'])
        self.check_comma(r'aa\\\a, bb', [r'aa\\\a', 'bb'])

        self.check_comma(',aa', ['aa'])
        self.check_comma('aa,', ['aa'])
        self.check_comma('aa,,', ['aa'])

    def test_line(self):
        self.check_line('aa\nbb', ['aa', 'bb'])
        self.check_line('aa\\\nbb', ['aa\nbb'])
        self.check_line('aa\\\\\nbb', ['aa\\\nbb'])
        self.check_line('aa\\\\\\\nbb', ['aa\\\\\nbb'])

        self.check_line('aa\nbb,', ['aa', 'bb,'])

class TestInheritance:

    def test_iter(self):
        data = f("""
        [sec1]
        [sec2]
        """)
        conf = fetch(data)
        assert list(conf.__iter__()) == ['DEFAULT', 'sec1', 'sec2']

    def test_iter_option(self):
        data = f("""
        [sec1]
        aa = xxx
        bb = yyy
        """)
        conf = fetch(data)
        assert list(conf.sec1.__iter__()) == ['aa', 'bb']

    def test_contains(self):
        data = f("""
        [sec1]
        [sec2]
        """)
        conf = fetch(data)
        assert 'sec2' in conf

    def test_contains_option(self):
        data = f("""
        [sec1]
        aa = xxx
        bb = yyy
        """)
        conf = fetch(data)
        assert 'bb' in conf.sec1


class TestParseConfig:

    def test_conf_str(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        conf = fetch(data)
        assert conf.sec1.aa == 'xxx'

    def test_conf_str_blank(self):
        data = f("""
        [sec1]
        """)
        conf = fetch(data)
        with pytest.raises(configfetch.NoOptionError):
            assert conf.sec1.aa == ''

    def test_conf_str_nosection(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        conf = fetch(data)
        with pytest.raises(configfetch.NoSectionError):
            assert conf.sec2

    def test_conf_str_default(self):
        data = f("""
        [DEFAULT]
        aa = xxx
        [sec1]
        """)
        conf = fetch(data)
        assert conf.sec1.aa == 'xxx'

    def test_conf_str_default_nosection(self):
        data = f("""
        [DEFAULT]
        aa = xxx
        """)
        conf = fetch(data)
        with pytest.raises(configfetch.NoSectionError):
            assert conf.sec1.aa == 'xxx'

    def test_conf_str_default_read_section(self):
        data = f("""
        [DEFAULT]
        aa = xxx
        """)
        conf = fetch(data)
        data = f("""
        [sec1]
        """)
        conf._config.read_string(data)
        assert conf.sec1.aa == 'xxx'

    def test_conf_str_default_blank(self):
        data = f("""
        [DEFAULT]
        [sec1]
        """)
        conf = fetch(data)
        with pytest.raises(configfetch.NoOptionError):
            assert conf.sec1.aa == ''

    def test_conf_str_default_blank_nosection(self):
        data = ''
        conf = fetch(data)
        with pytest.raises(configfetch.NoSectionError):
            assert conf.sec1.aa == ''

    def test_conf_bool(self):
        data = f("""
        [sec1]
        aa = :: f: bool
             Yes
        """)
        conf = fetch(data)
        assert conf.sec1.aa is True

    def test_conf_bool_no(self):
        data = f("""
        [sec1]
        aa = :: f: bool
             No
        """)
        conf = fetch(data)
        assert conf.sec1.aa is False

    # blank string returns ``None``
    def test_conf_bool_blank(self):
        data = f("""
        [sec1]
        aa = :: f: bool
              
        """)
        conf = fetch(data)
        assert conf.sec1.aa is None

    def test_conf_comma(self):
        data = f("""
        [sec1]
        aa = :: f: comma
             xxx1, xxx2, xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2', 'xxx3']

    def test_conf_comma_indent(self):
        data = f("""
        [sec1]
        aa = :: f: comma
             xxx1, xxx2,
            xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2', 'xxx3']

    def test_conf_comma_newline(self):
        data = f("""
        [sec1]
        aa = :: f: comma
             xxx1, xxx2
             xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2\nxxx3']

    def test_conf_comma_blank(self):
        data = f("""
        [sec1]
        aa = :: f: comma
              
        """)
        conf = fetch(data)
        assert conf.sec1.aa == []

    def test_conf_line(self):
        data = f("""
        [sec1]
        aa = :: f: line
             xxx1
             xxx2
             xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2', 'xxx3']

    def test_conf_line_comma(self):
        data = f("""
        [sec1]
        aa = :: f: line
             xxx1
             xxx2
             xxx3, xxx4
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2', 'xxx3, xxx4']

    def test_conf_line_blank(self):
        data = f("""
        [sec1]
        aa = :: f: line
              
        """)
        conf = fetch(data)
        assert conf.sec1.aa == []

    def test_conf_line_multiblanks(self):
        data = f("""
        [sec1]
        aa = :: f: line
              
            
            
        """)
        conf = fetch(data)
        assert conf.sec1.aa == []

    def test_conf_bar_comma(self):
        data = f("""
        [sec1]
        aa = :: f: comma, bar
             xxx1, xxx2, xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == 'xxx1|xxx2|xxx3'

    def test_conf_bar_comma_blank(self):
        data = f("""
        [sec1]
        aa = :: f: comma, bar
              
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ''

    def test_conf_bar_comma_blank_spaces(self):
        data = f("""
        [sec1]
        aa = :: f: comma, bar
                 
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ''

    def test_conf_bar_line(self):
        data = f("""
        [sec1]
        aa = :: f: line, bar
             xxx1
             xxx2
             xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == 'xxx1|xxx2|xxx3'

    def test_conf_bar_line_blank(self):
        data = f("""
        [sec1]
        aa = :: f: line, bar
              
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ''

    def test_conf_bar_line_blank_spaces(self):
        data = f("""
        [sec1]
        aa = :: f: line, bar
                 
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ''

    def test_conf_cmd(self):
        data = f("""
        [sec1]
        aa = :: f: cmd
             --aaa -b "ccc cc" ddd,dd
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['--aaa', '-b', 'ccc cc', 'ddd,dd']

    def test_conf_cmds(self):
        data = f("""
        [sec1]
        aa = :: f: line, cmds
             ls *.txt
             find . "aaa"
        """)
        conf = fetch(data)
        assert conf.sec1.aa == [['ls', '*.txt'], ['find', '.', 'aaa']]

    def test_conf_fmt(self):
        data = f("""
        [sec1]
        aa = :: f: fmt
             {USER}/data/my.css
        """)
        conf = fetch(data, fmts={'USER': '/home/john'})
        assert conf.sec1.aa == '/home/john/data/my.css'


class TestParseContexts:

    def test_ctx_default_bool(self):
        data = f("""
        [DEFAULT]
        aa = :: f: bool
             no
        [sec1]
        """)
        conf = fetch(data)
        assert conf.sec1.aa is False

    def test_ctx_default_bool_noop(self):
        data = f("""
        [DEFAULT]
        aa = :: f: bool
              
        [sec1]
        aa = no
        """)
        conf = fetch(data)
        assert conf.sec1.aa is False

    def test_ctx_default_comma(self):
        data = f("""
        [DEFAULT]
        aa = :: f: comma
              
        [sec1]
        aa = xxx1, xxx2, xxx3
        """)
        conf = fetch(data)
        assert conf.sec1.aa == ['xxx1', 'xxx2', 'xxx3']


class TestParseFunc:

    def test_func_newline(self):
        data = f("""
        [sec1]
        aa =
            :: f: bool
            no
        """)
        conf = fetch(data)
        assert conf.sec1.aa is False


# Just checking the standard library's behaviors.
class TestConfigParser:

    def test_indent(self):
        data = f("""
        [sec1]
          aa =
           xxx
        """)
        config = configparser.ConfigParser()
        config.read_string(data)
        assert config['sec1']['aa'] == '\nxxx'

        data = f("""
        [sec1]
          aa =
          xxx
        """)
        config = configparser.ConfigParser()
        with pytest.raises(configparser.ParsingError):
            config.read_string(data)

    def test_allow_no_value(self):
        data = f("""
        [sec1]
        aa =
            :: f: bool
              no
        """)
        config = configparser.ConfigParser(allow_no_value=True)
        config.read_string(data)
        assert config['sec1']['aa'] == '\n:: f: bool\nno'


class TestArgparse:

    parser = argparse.ArgumentParser()
    parser.add_argument('-a', '--aa')
    parser.add_argument('-b', '--bb')
    parser.add_argument('-c', '--cc', action='store_const', default='', const='yes')
    parser.add_argument('-d', '--no-cc', action='store_const', const='no', dest='cc')
    parser.add_argument('-e', '--ee-eee')

    def get_args(self, cmd):
        return self.parser.parse_args(cmd)

    def test_args_and_conf(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        args = self.get_args(['--aa', 'axxx'])
        conf = fetch(data, args=args)
        assert conf.sec1.aa == 'axxx'

    def test_args_and_conf_short(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        args = self.get_args(['-a', 'axxx'])
        conf = fetch(data, args=args)
        assert conf.sec1.aa == 'axxx'

    def test_args_and_conf_none(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        args = self.get_args([])
        conf = fetch(data, args=args)
        assert conf.sec1.aa == 'xxx'

    def test_args_and_conf_const(self):
        data = f("""
        [sec1]
        cc = :: f: bool
        """)
        args = self.get_args(['--cc'])
        conf = fetch(data, args=args)
        assert conf.sec1.cc is True

    def test_args_and_conf_const_false(self):
        data = f("""
        [sec1]
        cc = :: f: bool
             true
        """)
        args = self.get_args(['--no-cc'])
        conf = fetch(data, args=args)
        assert conf.sec1.cc is False

    def test_args_and_conf_dash(self):
        data = f("""
        [sec1]
        ee_eee = xxx
        """)
        args = self.get_args(['-e', 'axxx'])
        conf = fetch(data, args=args)
        assert conf.sec1.ee_eee == 'axxx'


class _CustomFunc(configfetch.Func):
    """Used the test below."""

    @configfetch.register
    def custom(self, value):
        return 'test'


class TestCustomize:

    def test_customfunc(self):
        data = f("""
        [sec1]
        aa = :: f: custom
             xxx
        """)
        conf = fetch(data, Func=_CustomFunc)
        assert conf.sec1.aa == 'test'


class TestDouble:

    def test_nooption_nooption(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        conf1 = fetch(data)
        data = f("""
        [sec1]
        aa = yyy
        """)
        conf2 = fetch(data)
        double = configfetch.Double(conf2.sec1, conf1.sec1)
        with pytest.raises(configfetch.NoOptionError):
            assert double.bb == 'zzz'

    def test_nooption_blank(self):
        data = f("""
        [sec1]
        aa = xxx
        """)
        conf1 = fetch(data)
        data = f("""
        [sec1]
        bb =
        """)
        conf2 = fetch(data)
        double = configfetch.Double(conf2.sec1, conf1.sec1)
        assert double.bb == ''

    def test_blank_nooption(self):
        data = f("""
        [sec1]
        bb =
        """)
        conf1 = fetch(data)
        data = f("""
        [sec1]
        aa = yyy
        """)
        conf2 = fetch(data)
        double = configfetch.Double(conf2.sec1, conf1.sec1)
        assert double.bb == ''

    def test_blank_blank(self):
        data = f("""
        [sec1]
        bb =
        """)
        conf1 = fetch(data)
        data = f("""
        [sec1]
        bb = :: f: comma
              
        """)
        conf2 = fetch(data)
        double = configfetch.Double(conf2.sec1, conf1.sec1)
        assert double.bb == ''

    def test_plus(self):
        data = f("""
        [sec1]
        aa = :: f: plus
             xxx, yyy
        """)
        conf1 = fetch(data)
        data = f("""
        [sec1]
        aa = :: f: plus
             -yyy
        """)
        conf2 = fetch(data)
        double = configfetch.Double(conf2.sec1, conf1.sec1)
        assert double.aa == ['xxx']


class TestGetPlusMinusValues:

    initial = ['aaa', 'bbb', 'ccc']

    def compare(self, adjusts, initial, expected):
        values = configfetch._get_plusminus_values(adjusts, initial)
        assert values == expected

    def test_adjusts_argument(self):
        args = (['ddd'], None, ['ddd'])
        self.compare(*args)
        args = (['+ddd'], None, ['ddd'])
        self.compare(*args)
        args = (['-bbb'], None, [])
        self.compare(*args)

        args = (['ddd'], self.initial, ['ddd'])
        self.compare(*args)
        args = (['+ddd'], self.initial, ['aaa', 'bbb', 'ccc', 'ddd'])
        self.compare(*args)
        args = (['-bbb'], self.initial, ['aaa', 'ccc'])
        self.compare(*args)

        args = (['-aaa, -bbb'], self.initial, ['ccc'])
        self.compare(*args)
        args = (['-aaa, +ddd, +eee'], self.initial,
            ['bbb', 'ccc', 'ddd', 'eee'])
        self.compare(*args)


class TestMinusAdapter:

    parser = argparse.ArgumentParser()
    parser.add_argument('-a', '--aa', action='store_const', const='A')
    parser.add_argument('-b', '--bb', action='store_true')
    parser.add_argument('-c', '--cc', action='store_false')
    parser.add_argument('-d', '--dd', action='append')
    parser.add_argument('-e', '--ee', action='append_const', const='E')
    parser.add_argument('-f', '--ff', action='count')

    parser.add_argument('-x', '--xx')
    parser.add_argument('-y', '--yy', nargs=1)

    def compare(self, args, new_args, matcher=None):
        assert configfetch.minusadapter(self.parser, matcher, args) == new_args

    def test(self):
        # No Minus argument
        args = ['--aa', '--xx', 'xxxx', '--bb']
        new_args = ['--aa', '--xx', 'xxxx', '--bb']
        self.compare(args, new_args)

        # Minus argument
        args = ['--aa', '--xx', '-xxxx', '--bb']
        new_args = ['--aa', '--xx=-xxxx', '--bb']
        self.compare(args, new_args)

        # Minus with another StoreAction
        args = ['--aa', '--xx', '-xxxx', '--yy', 'yyyy']
        new_args = ['--aa', '--xx=-xxxx', '--yy', 'yyyy']
        self.compare(args, new_args)

        # Minus with AppendAction
        args = ['--dd', '-dddd', '--xx', '-xxxx', '--bb']
        new_args = ['--dd=-dddd', '--xx=-xxxx', '--bb']
        self.compare(args, new_args)

        # Minus, short option version
        args = ['--aa', '-x', '-xxxx', '--bb']
        new_args = ['--aa', '-x-xxxx', '--bb']
        self.compare(args, new_args)


class TestParseArgs:

    def test_help(self):
        data = f("""
        [sec1]
        aa = : help string
             :: f: comma
             xxx1, xxx2
        """)
        conf = fetch(data)
        args = conf._ctx['aa']['argparse']
        assert args['help'] == 'help string'

    def test_help_multilines(self):
        data = f("""
        [sec1]
        aa = : This
             : is a
             : help.
             :: f: comma
             xxx1, xxx2
        """)
        conf = fetch(data)
        args = conf._ctx['aa']['argparse']
        assert args['help'] == 'This\nis a\nhelp.'

    def test_help_multilines_blank(self):
        # testing both ':' and ': '
        data = f("""
        [sec1]
        aa = : This
             : is a
             :
             : 
             : help.
             :: f: comma
             xxx1, xxx2
        """)
        conf = fetch(data)
        args = conf._ctx['aa']['argparse']
        assert args['help'] == 'This\nis a\n\n\nhelp.'

    def test_help_and_choices(self):
        data = f("""
        [sec1]
        aa = : help string
             :: choices: ss, tt
             tt
        """)
        conf = fetch(data)
        args = conf._ctx['aa']['argparse']
        assert args['help'] == 'help string'
        assert args['choices'] == ['ss', 'tt']


class TestBuildArgs:

    def test_help(self):
        data = f("""
        [sec1]
        aa = : help string
             :: f: comma
             xxx1, xxx2
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.help == 'help string'

    def test_help_and_choices(self):
        data = f("""
        [sec1]
        aa = : help string
             :: choices: ss, tt
             tt
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.choices == ['ss', 'tt']

    def test_names(self):
        data = f("""
        [sec1]
        aa = : help string
             :: names: a
             true
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.option_strings == ['-a', '--aa']

    def test_bool(self):
        data = f("""
        [sec1]
        aa = : help string
             :: f: bool
             yes
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreConstAction)
        assert action.const == 'yes'

    def test_bool_opposite(self):
        data = f("""
        [sec1]
        aa =    : help string
                :: f: bool
                yes
        no_aa = : help string2
                :: dest: aa
                :: f: bool
                no
        """)
        conf = fetch(data)
        parser = argparse.ArgumentParser(prog='test')
        conf.build_arguments(parser)
        namespace = parser.parse_args(['--aa'])
        assert namespace.__dict__['aa'] == 'yes'
        namespace = parser.parse_args(['--no-aa'])
        assert namespace.__dict__['aa'] == 'no'

    def test_bool_default_no(self):
        data = f("""
        [sec1]
        overwrite = : help string
                    :: f: bool
                    no
        """)
        conf = fetch(data)
        action = _get_action(conf, '--overwrite')
        assert isinstance(action, argparse._StoreConstAction)
        assert action.const == 'yes'

    def test_bool_opposite_default_no(self):
        data = f("""
        [sec1]
        overwrite =     : help string
                        :: f: bool
                        no
        no_overwrite =  : help string2
                        :: dest: overwrite
                        :: f: bool
                        yes
        """)
        conf = fetch(data)
        parser = argparse.ArgumentParser(prog='test')
        conf.build_arguments(parser)
        namespace = parser.parse_args(['--overwrite'])
        assert namespace.__dict__['overwrite'] == 'yes'
        namespace = parser.parse_args(['--no-overwrite'])
        assert namespace.__dict__['overwrite'] == 'no'


class TestBuildArgsCommandlineOnly:

    def test_int(self):
        data = f("""
        [sec1]
        aa = : help string
             :: default: 1
             xxx
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.default == 1

    def test_int_like_string(self):
        data = f("""
        [sec1]
        aa = : help string
             :: default: '1'
             xxx
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.default == '1'

    def test_type(self):
        data = f("""
        [sec1]
        aa = : help string
             :: type: int
             42
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.type == int

    def test_suppress(self):
        data = f("""
        [DEFAULT]
        aa = : argparse.SUPPRESS
             :: default: argparse.SUPPRESS
        [sec1]
        aa = xxx
        """)
        conf = fetch(data)
        action = _get_action(conf, '--aa')
        assert isinstance(action, argparse._StoreAction)
        assert action.help == argparse.SUPPRESS
        assert action.default == argparse.SUPPRESS
        assert conf.sec1.aa == 'xxx'


def test_print_data():
    data = f("""
    [DEFAULT]
    aa = aaa
    [sec1]
    bb = bbb
    cc = : help string
         :: names: c
         :: f: bool
         ccc
    dd =
    """)

    dict_string = f("""
    {
        'DEFAULT': {
            'aa': {
                'value': 'aaa',
            },
        },
        'sec1': {
            'bb': {
                'value': 'bbb',
            },
            'cc': {
                'argparse': {
                    'help': 'help string',
                    'names': ['c'],
                },
                'func': ['bool'],
                'value': 'ccc',
            },
            'dd': {
                'value': '',
            },
        },
    }
    """)
            
    ini_string = f("""
    [DEFAULT]
    aa= aaa

    [sec1]
    bb= bbb
    cc= ccc
    dd=

    """)

    conf = fetch(data, option_builder=configfetch.FiniOptionBuilder)
    printer = configfetch.ConfigPrinter

    ret = []
    printer(conf, print=ret.append).print_dict()
    assert '\n'.join(ret) == dict_string[:-1]

    ret = []
    printer(conf, print=ret.append).print_ini()
    assert '\n'.join(ret) == ini_string[:-1]

    dict_ = eval(dict_string)
    conf = fetch(dict_, option_builder=configfetch.DictOptionBuilder)

    ret = []
    printer(conf, print=ret.append).print_dict()
    assert '\n'.join(ret) == dict_string[:-1]

    ret = []
    printer(conf, print=ret.append).print_ini()
    assert '\n'.join(ret) == ini_string[:-1]
