diff --git a/PanACoTA/utils_argparse.py b/PanACoTA/utils_argparse.py index 1d3fb39dc0dfed6ce405d6f4947a85955d1177d0..27b816af7eddc6096ceedd84a9b53ab05bf26658 100644 --- a/PanACoTA/utils_argparse.py +++ b/PanACoTA/utils_argparse.py @@ -44,9 +44,15 @@ from PanACoTA import utils import argparse import configparser import sys +import os def gen_name(param): + """ + Check name given for the genome set: + - must contain 4 characters, all alphanumeric (calling utils.check_format) + - if does not fit, give advice on format (genus_genus_species_species) + """ if not utils.check_format(param): msg = ("The genome name must contain 4 characters. For example, this name can " "correspond to the 2 first letters of genus, and 2 first letters of " @@ -56,6 +62,11 @@ def gen_name(param): def date_name(param): + """ + Check format of given date: + - must contain 4 characters, all alphanumeric (calling utils.check_format) + - if does not fit, give advice on format (MMYY) + """ if not utils.check_format(param): msg = ("The date must contain 4 characters. Usually, it contains 4 digits, " "corresponding to the month (2 digits) and year (2 digits).") @@ -64,11 +75,19 @@ def date_name(param): def get_date(): + """ + Get current date in MMYY format + """ import time return time.strftime("%m%y") def cont_num(param): + """ + Check number of contigs given + - must be positive int + - not more than 10000 + """ try: param = int(param) except Exception: @@ -84,6 +103,12 @@ def cont_num(param): def thread_num(param): + """ + check number of threads given. + - must be a positive int + - Cannot be more than number of threads available + - if '0' given, return number of threads available + """ import multiprocessing try: param = int(param) @@ -105,6 +130,9 @@ def thread_num(param): def positive_int(param): + """ + Return a positive int for argument --cutn + """ try: param = int(param) except ValueError: @@ -117,6 +145,9 @@ def positive_int(param): def mash_dist(param): + """ + Check mash distance given. Must be a float between 0 and 1 included + """ try: param = float(param) except ValueError: @@ -129,19 +160,25 @@ def mash_dist(param): def percentage(param): - try: - param = float(param) - except Exception: - msg = "argument -t tol: invalid float value: {}".format(param) - raise argparse.ArgumentTypeError(msg) - if param < 0 or param > 1: - msg = ("The minimum %% of genomes required in a family to be persistent must " - "be in [0, 1]. Invalid value: {}".format(param)) - raise argparse.ArgumentTypeError(msg) - return param + """ + check argument given to parameter '-t tol' + """ + try: + param = float(param) + except Exception: + msg = "argument -t tol: invalid float value: {}".format(param) + raise argparse.ArgumentTypeError(msg) + if param < 0 or param > 1: + msg = ("The minimum %% of genomes required in a family to be persistent must " + "be in [0, 1]. Invalid value: {}".format(param)) + raise argparse.ArgumentTypeError(msg) + return param def perc_id(param): + """ + Check argument given to parameter -i percentage_id + """ try: param = float(param) except Exception: @@ -157,11 +194,15 @@ class Conf_all_parser(configparser.ConfigParser): """ Read configfile and return arguments found, according to required type """ - def __init__(self, conffile, sections): + def __init__(self, conffile, readsec=[]): super().__init__() + if not os.path.isfile(conffile): + print(f"Error: config file {conffile} not found.") + sys.exit(1) + self.conffile = conffile self.read(conffile) self.sec_dicts = {} - for sec in sections: + for sec in readsec: # If section in configfile, put its arguments and values to a dict # If not, create empty section, and associate with empty dict if sec in dict(self): @@ -174,7 +215,11 @@ class Conf_all_parser(configparser.ConfigParser): """ get dictionary of values for 'section' section """ - return self.sec_dicts[section] + if section in self.sec_dicts: + return self.sec_dicts[section] + else: + print(f"No section {section} in {self.conffile}") + sys.exit(1) def add_default(self, defargs, section): """ diff --git a/test/test_unit/test_utils-argparse.py b/test/test_unit/test_utils-argparse.py index fb2a35cc3b940a22f2ddd0e41261ba6d41189c65..70a796fd3a1dc391a2e8cd33925f2378012f715b 100644 --- a/test/test_unit/test_utils-argparse.py +++ b/test/test_unit/test_utils-argparse.py @@ -14,6 +14,7 @@ import multiprocessing import PanACoTA.utils_argparse as autils +CONFFILE = os.path.join("test", "data", "utils", "configfile.ini") def test_gen_name(): """ @@ -133,3 +134,273 @@ def test_mash_dist(): a = autils.mash_dist("-1e-4") assert ("error: mash distance must be between 0 and 1: " "invalid value: '-0.0001'") in str(err.value) + + +def test_percentage(): + """ + Test value given for parameter -t tol: + - string corresponding to a float or an int -> returns float value + - a non digital param for a percentage raises appropriate error + - a negative number -> appropriate error + - number>1 -> appropriate error + """ + assert autils.percentage("0.5") == 0.5 + assert type(autils.percentage("0.5")) == float + assert autils.percentage("1e-1") == 0.1 + assert autils.percentage("1") == 1.0 + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.percentage("one") + assert ("argument -t tol: invalid float value: one") in str(err.value) + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.percentage(-0.5) + assert ("The minimum %% of genomes required in a family to be persistent must " + "be in [0, 1]. Invalid value: -0.5") in str(err.value) + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.percentage("1.1") + assert ("The minimum %% of genomes required in a family to be persistent must " + "be in [0, 1]. Invalid value: 1.1") in str(err.value) + + +def test_perc_id(): + """ + Same as test_percentage, but for parameter -i percentage_id + """ + assert autils.perc_id("0.5") == 0.5 + assert type(autils.perc_id("0.5")) == float + assert autils.perc_id("1e-1") == 0.1 + assert autils.perc_id("1") == 1.0 + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.perc_id("one") + assert ("argument -i percentage_id: invalid float value: one") in str(err.value) + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.perc_id(-0.5) + assert ("The minimum %% of identity must be in [0, 1]. Invalid value: -0.5") in str(err.value) + with pytest.raises(argparse.ArgumentTypeError) as err: + a = autils.perc_id("1.1") + assert ("The minimum %% of identity must be in [0, 1]. Invalid value: 1.1") in str(err.value) + + +def test_conf_parser_init_empty(capsys): + """ + test class Conf_all_parser init when no config file or empty config file + """ + # No conf file, no section + with pytest.raises(SystemExit): + c = autils.Conf_all_parser("") + out, err = capsys.readouterr() + assert ('Error: config file not found.') in out + # No conf file, empty section list + with pytest.raises(SystemExit): + c = autils.Conf_all_parser("", []) + out, err = capsys.readouterr() + assert ('Error: config file not found.') in out + # No conf file, sections + with pytest.raises(SystemExit): + c = autils.Conf_all_parser("", ["sec1", "sec2"]) + out, err = capsys.readouterr() + assert ('Error: config file not found.') in out + + confdir = os.path.join("test", "data", "generated_by_utils") + os.makedirs(confdir) + conffile = os.path.join(confdir, "conf.ini") + open(conffile, "w").close() + # Empty conffile, no section + c = autils.Conf_all_parser(conffile) + assert c.sec_dicts == {} + # Empty conffile, sections + c = autils.Conf_all_parser(conffile, ["sec1", "sec2"]) + assert c.sec_dicts == {"sec1": {}, "sec2": {}} + shutil.rmtree(confdir) + + +def test_conf_parser_init(): + """ + Test config parser with a given config file. Check value of defaults etc. + """ + # configfile but no section + c = autils.Conf_all_parser(CONFFILE) + assert c["sec1"]["toto"] == "parameter" + assert c["sec1"]["param1"] == "10" + assert c["sec2"]["param1"] == "3" + assert c["sec3"]["param1"] == "3" + assert c["sec1"]["param2"] == "10" + assert c["sec2"]["param2"] == '' + assert c["sec3"]["param2"] == "10" + assert c.sec_dicts == {} + + # configfile and 2 sections among 3 existing in configfile + c = autils.Conf_all_parser(CONFFILE, ["sec1", "sec3"]) + assert c["sec1"]["toto"] == "parameter" + assert c["sec1"]["param1"] == "10" + assert c["sec2"]["param1"] == "3" + assert c["sec2"]["myval"] == "parameter" + assert c["sec3"]["param1"] == "3" + assert c["sec1"]["param2"] == "10" + assert c["sec2"]["param2"] == '' + assert c["sec3"]["param2"] == "10" + assert c.sec_dicts == {"sec1": {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value"}, + "sec3": {"param1": "3", "param2": "10"}} + + # configfile 2 sections, 1 not in configfile + c = autils.Conf_all_parser(CONFFILE, ["sec1", "sec4"]) + assert c["sec1"]["toto"] == "parameter" + assert c["sec1"]["param1"] == "10" + assert c["sec2"]["param1"] == "3" + assert c["sec4"]["param1"] == "3" # created sec4 with default parameters + assert c["sec4"]["param2"] == "10" # created sec4 with default parameters + assert not "toto" in c["sec4"] # created sec4 with default parameters + assert c.sec_dicts == {"sec1": {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value"}, + "sec4": {}} # But sec4 dict is empty (no param given in configfile) + + +def test_conf_parser_get_section(capsys): + """ + Test get dict of values for a given section + """ + c = autils.Conf_all_parser(CONFFILE, ["sec1", "sec4"]) + # Get sec1 dict + assert c.get_section_dict("sec1") == {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value"} + assert c.get_section_dict("sec4") == {} + # Try to get sec2 dict, but does not exist + with pytest.raises(SystemExit): + c.get_section_dict("sec2") + out, err = capsys.readouterr() + assert ('No section sec2 in test/data/utils/configfile.ini') in out + + +def test_conf_parser_add_default(capsys): + """ + Test add default parameters to config parser. + If parameter given already exists, do nothing. If does not exist, add it with given value + """ + c = autils.Conf_all_parser(CONFFILE, ["sec1", "sec4"]) + defaults = {"param1": "55", "defparam": "default value"} + # Add default parameters to sec1 + c.add_default(defaults, "sec1") + assert c.get_section_dict("sec1") == {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value", + "defparam": "default value"} + assert c.get_section_dict("sec4") == {} + c["sec1"]["param1"] == "10" + c["sec1"]["defparam"] == "default value" + c["sec4"]["param1"] == "3" # sec4 has default parameter found in configfile + + # Add default parameters to sec4 (sec1 already have them) + c.add_default(defaults, "sec4") + assert c.get_section_dict("sec1") == {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value", + "defparam": "default value"} + assert c.get_section_dict("sec4") == {"param1": "55", "defparam": "default value"} + c["sec1"]["param1"] == "10" + c["sec1"]["defparam"] == "default value" + c["sec4"]["param1"] == "55" + + +def test_conf_parser_update(capsys): + """ + Test update section + Update current parameters with values given + """ + c1 = autils.Conf_all_parser(CONFFILE, ["sec1", "sec4"]) + update = {"param1": "55", "defparam": "default value"} + # Add default parameters to sec1 + c1.update(update, "sec1") + assert c1.get_section_dict("sec1") == {"param1": "55", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value", + "defparam": "default value"} + assert c1.get_section_dict("sec4") == {} + c1["sec1"]["param1"] == "10" + c1["sec1"]["defparam"] == "default value" + c1["sec4"]["param1"] == "3" # sec4 has default parameter found in configfile + + # Add default parameters to sec1 + c2 = autils.Conf_all_parser(CONFFILE, ["sec1", "sec4"]) + c2.update(update, "sec4") + assert c2.get_section_dict("sec1") == {"param1": "10", "param2": "10", 'sec1p': "", + "toto": "parameter", "titi": "my value"} + assert c2.get_section_dict("sec4") == {"param1": "55", "defparam": "default value"} + c2["sec1"]["param1"] == "10" + c2["sec4"]["param1"] == "55" # sec4 has default parameter found in configfile + + +def test_conf_parser_setbool(capsys): + """ + try to convert a given parameter to a boolean + """ + c1 = autils.Conf_all_parser(CONFFILE, ["sec_bool"]) + # 0/1 to False/True + c1.set_boolean("sec_bool", "bool num_false") + assert c1["sec_bool"]["bool num_false"] == "0" + assert c1.sec_dicts["sec_bool"]["bool num_false"] == False + c1.set_boolean("sec_bool", "bool_num_true") + assert c1["sec_bool"]["bool_num_true"] == "1" + assert c1.sec_dicts["sec_bool"]["bool_num_true"] == True + + # off/on to False/True + c1.set_boolean("sec_bool", "bool_f") + assert c1["sec_bool"]["bool_f"] == "off" + assert c1.sec_dicts["sec_bool"]["bool_f"] == False + c1.set_boolean("sec_bool", "bool_t") + assert c1["sec_bool"]["bool_t"] == "ON" + assert c1.sec_dicts["sec_bool"]["bool_t"] == True + + # no/yes to False/True + c1.set_boolean("sec_bool", "bool_n") + assert c1["sec_bool"]["bool_n"] == "no" + assert c1.sec_dicts["sec_bool"]["bool_n"] == False + c1.set_boolean("sec_bool", "bool_y") + assert c1["sec_bool"]["bool_y"] == "YES" + assert c1.sec_dicts["sec_bool"]["bool_y"] == True + + # false/true to False/True + c1.set_boolean("sec_bool", "bool_false") + assert c1["sec_bool"]["bool_false"] == "FalSe" + assert c1.sec_dicts["sec_bool"]["bool_false"] == False + c1.set_boolean("sec_bool", "bool_true") + assert c1["sec_bool"]["bool_true"] == "tRUE" + assert c1.sec_dicts["sec_bool"]["bool_true"] == True + + # error + with pytest.raises(SystemExit): + c1.set_boolean('sec1', "param1") + out, err = capsys.readouterr() + assert ('ERROR: param1 must be a boolean. Wrong value: 10.') in out + + +def test_conf_parser_setint(capsys): + """ + try to convert a given parameter to an int + """ + c1 = autils.Conf_all_parser(CONFFILE, ["sec1"]) + c1.set_int("sec1", "param1") + assert c1["sec1"]["param1"] == "10" + assert c1.sec_dicts["sec1"]["param1"] == 10 + + with pytest.raises(SystemExit): + c1.set_int("sec1", "toto") + out, err = capsys.readouterr() + assert ('ERROR: toto must be an int. Wrong value: parameter.') in out + + +def test_conf_parser_setfloat(capsys): + """ + try to convert a given parameter to a float + """ + c1 = autils.Conf_all_parser(CONFFILE, ["sec_float"]) + # float with decimal + c1.set_float("sec_float", "float_param") + assert c1["sec_float"]["float_param"] == "0.12" + assert c1.sec_dicts["sec_float"]["float_param"] == 0.12 + + # exp format + c1.set_float("sec_float", "float_param2") + assert c1["sec_float"]["float_param2"] == "1e5" + assert c1.sec_dicts["sec_float"]["float_param2"] == 100000 + + with pytest.raises(SystemExit): + c1.set_float("sec1", "toto") + out, err = capsys.readouterr() + assert ('ERROR: toto must be a float. Wrong value: parameter.') in out