Skip to content
Open
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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Python's command line parsing modules such as argparse have very limited
support for config files and environment variables, so this module
extends argparse to add these features.

**API docs:** https://bw2.github.io/ConfigArgParse/
**API docs:** https://bw2.github.io/ConfigArgParse/

**PyPI:** http://pypi.python.org/pypi/ConfigArgParse

## Install
Expand Down Expand Up @@ -158,14 +158,14 @@ Only command line args that have a long version (eg. one that starts with '--')
can be set in a config file. For example, "--color" can be set by putting
"color=green" in a config file. The config file syntax depends on the constructor
arg: `config_file_parser_class` which can be set to one of the provided
classes:
classes:
- `DefaultConfigFileParser`
- `YAMLConfigFileParser`
- `ConfigparserConfigFileParser`
- `IniConfigParser`
- `TomlConfigParser`
- `CompositeConfigParser`

or to your own subclass of the `ConfigFileParser` abstract class.

#### *DefaultConfigFileParser* - the full range of valid syntax is:
Expand Down Expand Up @@ -332,6 +332,8 @@ parser = configargparse.ArgParser(

[TOML](https://github.com/toml-lang/toml/blob/main/toml.md) parser. This config parser can be used to integrate with `pyproject.toml` files.

Requires installation of [toml](https://pypi.org/project/toml/) for Python versions below 3.11.

Example:

```toml
Expand Down Expand Up @@ -387,7 +389,7 @@ parser = configargparse.ArgParser(
...
```

Note that it's required to put the TOML parser first because the INI syntax basically would accept anything whereas TOML.
Note that it's required to put the TOML parser first because the INI syntax basically would accept anything whereas TOML is a bit more strict.

## ArgParser Singletons

Expand Down
81 changes: 51 additions & 30 deletions configargparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import types
from collections import OrderedDict
import textwrap
import warnings
from io import StringIO

ACTION_TYPES_THAT_DONT_NEED_A_VALUE = [
Expand Down Expand Up @@ -134,6 +135,10 @@ class ConfigFileParserException(Exception):
"""Raised when config file parsing failed."""


class ConfigFileParserMissingDependency(Exception):
"""Raised when an optional dependency is missing."""


class DefaultConfigFileParser(ConfigFileParser):
"""
Based on a simplified subset of INI and YAML formats. Here is the
Expand Down Expand Up @@ -316,7 +321,7 @@ def _load_yaml(self):
try:
import yaml
except ImportError:
raise ConfigFileParserException(
raise ConfigFileParserMissingDependency(
"Could not import yaml. "
"It can be installed by running 'pip install PyYAML'"
)
Expand Down Expand Up @@ -373,7 +378,7 @@ def serialize(self, items, default_flow_style=False):
Provides `configargparse.ConfigFileParser` classes to parse ``TOML`` and ``INI`` files with **mandatory** support for sections.
Useful to integrate configuration into project files like ``pyproject.toml`` or ``setup.cfg``.

`TomlConfigParser` usage:
`TomlConfigParser` usage:

>>> TomlParser = TomlConfigParser(['tool.my_super_tool']) # Simple TOML parser.
>>> parser = ArgumentParser(..., default_config_files=['./pyproject.toml'], config_file_parser_class=TomlParser)
Expand Down Expand Up @@ -522,25 +527,25 @@ def parse(self, stream):
"""Parses the keys and values from a TOML config file."""
# Use tomllib (Python 3.11+) if available, otherwise fall back to toml package
try:
import tomllib

# tomllib.load() requires binary mode, so use loads() for stream compatibility
try:
content = stream.read()
# If content is bytes, decode it; if string, use as-is
if isinstance(content, bytes):
content = content.decode("utf-8")
config = tomllib.loads(content)
except Exception as e:
raise ConfigFileParserException("Couldn't parse TOML file: %s" % e)
import tomllib as toml
except ImportError:
# Fall back to toml package (supports text mode)
import toml

try:
config = toml.load(stream)
except Exception as e:
raise ConfigFileParserException("Couldn't parse TOML file: %s" % e)
import toml
except ImportError as e:
raise ConfigFileParserMissingDependency(
"Could not import toml or tomllib. "
"toml can be installed by running 'pip install toml'"
) from e

# tomllib.load() requires binary mode, so use loads() for stream compatibility
try:
content = stream.read()
# If content is bytes, decode it; if string, use as-is
if isinstance(content, bytes):
content = content.decode("utf-8")
config = toml.loads(content)
except Exception as e:
raise ConfigFileParserException("Couldn't parse TOML file: %s" % e)

# convert to dict and filter based on section names
result = OrderedDict()
Expand Down Expand Up @@ -577,7 +582,7 @@ def serialize(self, items):
try:
import toml
except ImportError:
raise ConfigFileParserException(
raise ConfigFileParserMissingDependency(
"The 'toml' package is required for TOML serialization. "
"Install it with: pip install toml"
)
Expand Down Expand Up @@ -764,7 +769,20 @@ class CompositeConfigParser(ConfigFileParser):

def __init__(self, config_parser_types):
super().__init__()
self.parsers = [p() for p in config_parser_types]
self.parsers: list[ConfigFileParser] = [p() for p in config_parser_types]

seen_ini = False
for parser in self.parsers:
if not seen_ini and isinstance(parser, IniConfigParser):
seen_ini = True
continue
if seen_ini and isinstance(parser, TomlConfigParser):
warnings.warn(
"IniConfigParser was found before TomlConfigParser in parsers for "
"CompositeConfigParser. This might lead to a TOML file being "
"parsed as an INI file. Reorder the parsers.",
category=SyntaxWarning,
)

def __call__(self):
return self
Expand All @@ -773,7 +791,12 @@ def parse(self, stream):
errors = []
for i, p in enumerate(self.parsers):
try:
print(f"USING PARSER {p.__class__.__name__}")
return p.parse(stream) # type: ignore[no-any-return]
except ConfigFileParserMissingDependency as e:
msg = f"Cannot use parser {p.__class__.__name__} without optional dependency."
print(msg)
raise
except Exception as e:
errors.append(e)
# Try to seek back to beginning for next parser
Expand Down Expand Up @@ -804,7 +827,7 @@ def guess_format_name(classname):

msg = "Uses multiple config parser settings (in order): \n"
for i, parser in enumerate(self.parsers):
msg += f"[{i+1}] {guess_format_name(parser.__class__.__name__)}: {parser.get_syntax_description()} \n"
msg += f"[{i + 1}] {guess_format_name(parser.__class__.__name__)}: {parser.get_syntax_description()} \n"
return msg

def serialize(self, items):
Expand Down Expand Up @@ -1447,9 +1470,9 @@ def convert_item_to_command_line_arg(self, action, key, value):
if action is not None and isinstance(
action, ACTION_TYPES_THAT_DONT_NEED_A_VALUE
):
assert isinstance(
value, str
), "config parser should convert anything that is not a list to string."
assert isinstance(value, str), (
"config parser should convert anything that is not a list to string."
)
if value.lower() in ("true", "yes", "on", "1"):
if not is_boolean_optional_action(action):
args.append(command_line_key)
Expand Down Expand Up @@ -1725,7 +1748,7 @@ def format_help(self):
added_config_file_help = True

msg += (
"Args that start with '%s' can also be set in " "a config file"
"Args that start with '%s' can also be set in a config file"
) % cc
config_arg_string = " or ".join(
a.option_strings[0] for a in config_path_actions if a.option_strings
Expand Down Expand Up @@ -1832,15 +1855,13 @@ def add_argument(self, *args, **kwargs):
if action.is_positional_arg and env_var:
raise ValueError("env_var can't be set for a positional arg.")
if action.is_config_file_arg and not isinstance(action, argparse._StoreAction):
raise ValueError("arg with is_config_file_arg=True must have " "action='store'")
raise ValueError("arg with is_config_file_arg=True must have action='store'")
if action.is_write_out_config_file_arg:
error_prefix = "arg with is_write_out_config_file_arg=True "
if not isinstance(action, argparse._StoreAction):
raise ValueError(error_prefix + "must have action='store'")
if is_config_file_arg:
raise ValueError(
error_prefix + "can't also have " "is_config_file_arg=True"
)
raise ValueError(error_prefix + "can't also have is_config_file_arg=True")

return action

Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ def launch_http_server(directory):
logging.debug("All network port. ")
except Exception as e:
logging.error(
"ERROR: while starting an HTTP server to serve "
"the coverage report: %s" % e
"ERROR: while starting an HTTP server to serve the coverage report: %s" % e
)


Expand Down Expand Up @@ -82,7 +81,7 @@ def launch_http_server(directory):
tests_require = [
"black",
"mock",
"toml",
"toml; python_version < '3.11'",
"PyYAML",
"pytest",
"pytest-cov",
Expand Down Expand Up @@ -126,6 +125,7 @@ def launch_http_server(directory):
install_requires=install_requires,
tests_require=tests_require,
extras_require={
"toml": ["toml; python_version < '3.11'"],
"yaml": ["PyYAML"],
"test": tests_require,
},
Expand Down
Loading