Correct handling of boolean selections

The selections proxy class, which mashes together command-line
arguments and configuration options, contained a longstanding and
fatal flaw with its handling of boolean values. In particular,
falsey values were consistently treated as truthy due to naively
recasting str to bool (which will always yield True unless empty).
This went unnoticed for so long because the majority of these
settings default to False, meaning the only reason most users had to
set them was to override them to True.

Many thanks to Jordan Russell for bringing this bug to my attention,
and for supplying an initial patch on which this fix is heavily
based.

Co-Authored-By: Jordan Russell
This commit is contained in:
Jeremy Stanley
2021-11-10 18:38:34 +00:00
parent d556621fea
commit cbdccf95dc

View File

@@ -95,7 +95,19 @@ class Selections:
return None return None
def get_bool(self, option, argument=None): def get_bool(self, option, argument=None):
"""Get data and coerce to a boolean if necessary.""" """Get data and coerce to a boolean if necessary."""
return bool(self.get(option, argument)) # Mimic configparser's getboolean() method by treating
# false/no/off/0 as False and true/yes/on/1 as True values,
# case-insensitively
value = self.get(option, argument)
if isinstance(value, bool):
return value
if isinstance(value, str):
vlower = value.lower()
if vlower in ('false', 'no', 'off', '0'):
return False
elif vlower in ('true', 'yes', 'on', '1'):
return True
raise ValueError("Not a boolean: %s" % value)
def getint(self, option, argument=None): def getint(self, option, argument=None):
"""Get data and coerce to an integer if necessary.""" """Get data and coerce to an integer if necessary."""
value = self.get(option, argument) value = self.get(option, argument)
@@ -365,7 +377,7 @@ def get_options(config):
# the -a/--alert option # the -a/--alert option
if config.has_option("default", "alert"): if config.has_option("default", "alert"):
default_alert = bool(config.get("default", "alert")) default_alert = config.getboolean("default", "alert")
else: default_alert = False else: default_alert = False
option_parser.add_option("-a", "--alert", option_parser.add_option("-a", "--alert",
dest="alert", dest="alert",
@@ -420,7 +432,7 @@ def get_options(config):
# the -f/--forecast option # the -f/--forecast option
if config.has_option("default", "forecast"): if config.has_option("default", "forecast"):
default_forecast = bool(config.get("default", "forecast")) default_forecast = config.getboolean("default", "forecast")
else: default_forecast = False else: default_forecast = False
option_parser.add_option("-f", "--forecast", option_parser.add_option("-f", "--forecast",
dest="forecast", dest="forecast",
@@ -448,7 +460,7 @@ def get_options(config):
# the --imperial option # the --imperial option
if config.has_option("default", "imperial"): if config.has_option("default", "imperial"):
default_imperial = bool(config.get("default", "imperial")) default_imperial = config.getboolean("default", "imperial")
else: default_imperial = False else: default_imperial = False
option_parser.add_option("--imperial", option_parser.add_option("--imperial",
dest="imperial", dest="imperial",
@@ -479,7 +491,7 @@ def get_options(config):
# the -m/--metric option # the -m/--metric option
if config.has_option("default", "metric"): if config.has_option("default", "metric"):
default_metric = bool(config.get("default", "metric")) default_metric = config.getboolean("default", "metric")
else: default_metric = False else: default_metric = False
option_parser.add_option("-m", "--metric", option_parser.add_option("-m", "--metric",
dest="metric", dest="metric",
@@ -489,7 +501,7 @@ def get_options(config):
# the -n/--no-conditions option # the -n/--no-conditions option
if config.has_option("default", "conditions"): if config.has_option("default", "conditions"):
default_conditions = bool(config.get("default", "conditions")) default_conditions = config.getboolean("default", "conditions")
else: default_conditions = True else: default_conditions = True
option_parser.add_option("-n", "--no-conditions", option_parser.add_option("-n", "--no-conditions",
dest="conditions", dest="conditions",
@@ -499,7 +511,7 @@ def get_options(config):
# the --no-cache option # the --no-cache option
if config.has_option("default", "cache"): if config.has_option("default", "cache"):
default_cache = bool(config.get("default", "cache")) default_cache = config.getboolean("default", "cache")
else: default_cache = True else: default_cache = True
option_parser.add_option("--no-cache", option_parser.add_option("--no-cache",
dest="cache", dest="cache",
@@ -509,7 +521,7 @@ def get_options(config):
# the --no-cache-data option # the --no-cache-data option
if config.has_option("default", "cache_data"): if config.has_option("default", "cache_data"):
default_cache_data = bool(config.get("default", "cache_data")) default_cache_data = config.getboolean("default", "cache_data")
else: default_cache_data = True else: default_cache_data = True
option_parser.add_option("--no-cache-data", option_parser.add_option("--no-cache-data",
dest="cache_data", dest="cache_data",
@@ -519,7 +531,7 @@ def get_options(config):
# the --no-cache-search option # the --no-cache-search option
if config.has_option("default", "cache_search"): if config.has_option("default", "cache_search"):
default_cache_search = bool(config.get("default", "cache_search")) default_cache_search = config.getboolean("default", "cache_search")
else: default_cache_search = True else: default_cache_search = True
option_parser.add_option("--no-cache-search", option_parser.add_option("--no-cache-search",
dest="cache_search", dest="cache_search",
@@ -529,7 +541,7 @@ def get_options(config):
# the -q/--quiet option # the -q/--quiet option
if config.has_option("default", "quiet"): if config.has_option("default", "quiet"):
default_quiet = bool(config.get("default", "quiet")) default_quiet = config.getboolean("default", "quiet")
else: default_quiet = False else: default_quiet = False
option_parser.add_option("-q", "--quiet", option_parser.add_option("-q", "--quiet",
dest="quiet", dest="quiet",
@@ -548,7 +560,7 @@ def get_options(config):
# the -v/--verbose option # the -v/--verbose option
if config.has_option("default", "verbose"): if config.has_option("default", "verbose"):
default_verbose = bool(config.get("default", "verbose")) default_verbose = config.getboolean("default", "verbose")
else: default_verbose = False else: default_verbose = False
option_parser.add_option("-v", "--verbose", option_parser.add_option("-v", "--verbose",
dest="verbose", dest="verbose",