12 Commits
0.4.0 ... 0.6.3

Author SHA1 Message Date
e2ff779c30 Add py.typed 2023-04-25 00:13:51 -07:00
e9d0e874f3 Merge pull request #1 from lordwelch/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-04-24 23:20:57 -07:00
c9f5d57ed1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-autopep8: v2.0.1 → v2.0.2](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.1...v2.0.2)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.2.0)
2023-04-24 18:48:25 +00:00
ea5be60c63 Add un-committed fix and version bump 2023-02-20 02:07:35 -08:00
391f65c71f Version Bump 2023-02-20 02:01:02 -08:00
ba645eb7c6 Implement qoc fixes
Fix fstring
Add comments explaining execution of normalize_config with defaults arg
Fix not removing file or cmdline settings in persistent groups
Fix a bug in get_namespace when config is a namespace
Add additional tests
2023-02-20 01:59:40 -08:00
69800f01b6 Version Bump 2023-02-19 22:22:45 -08:00
b2eaa12a0e Upgrade pre-commit 2023-02-19 22:21:05 -08:00
fe7c821605 Add a display_name attribute to Setting 2023-02-19 18:39:35 -08:00
d07cf9949b Allow adding settings to existing groups
Calling add_group or add_persistent_group twice will add any new
settings defined.

Raise a ValueError if add_group or add_persistent_group is called during
a call to add_group or add_persistent_group.
2023-02-19 18:07:14 -08:00
41cf2dc7cd Version Bump 2023-01-31 19:35:35 -08:00
577b43c4e8 Fix regression with settings with a '-' 2023-01-31 19:35:10 -08:00
9 changed files with 314 additions and 42 deletions

90
.gitignore vendored
View File

@ -1,7 +1,85 @@
*.egg-info
*.py[co]
/.coverage
/.tox
/dist
.vscode/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
*.iml
## Directory-based project format:
.idea/
### Other editors
.*.swp
nbproject/
.vscode
*.exe
*.zip
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# for testing
temp/
tmp/

View File

@ -33,7 +33,7 @@ repos:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.0
rev: v2.0.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
@ -42,6 +42,6 @@ repos:
- id: flake8
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.2.0
hooks:
- id: mypy

View File

@ -93,11 +93,35 @@ class Setting:
metavar: str | None = None,
dest: str | None = None,
# ComicTagger
display_name: str = '',
cmdline: bool = True,
file: bool = True,
group: str = '',
exclusive: bool = False,
):
"""
Args:
*names: Passed directly to argparse
action: Passed directly to argparse
nargs: Passed directly to argparse
const: Passed directly to argparse
default: Passed directly to argparse
type: Passed directly to argparse
choices: Passed directly to argparse
required: Passed directly to argparse
help: Passed directly to argparse
metavar: Passed directly to argparse, defaults to `dest` uppercased
dest: This is the name used to retrieve the value from a `Config` object as a dictionary
display_name: This is not used by settngs. This is a human-readable name to be used when generating a GUI.
Defaults to `dest`.
cmdline: If this setting can be set via the commandline
file: If this setting can be set via a file
group: The group this option is in.
This is an internal argument and should only be set by settngs
exclusive: If this setting is exclusive to other settings in this group.
This is an internal argument and should only be set by settngs
"""
if not names:
raise ValueError('names must be specified')
# We prefix the destination name used by argparse so that there are no conflicts
@ -130,6 +154,7 @@ class Setting:
self.argparse_args = args
self.group = group
self.exclusive = exclusive
self.display_name = display_name or dest
self.argparse_kwargs = {
'action': action,
@ -150,6 +175,11 @@ class Setting:
def __repr__(self) -> str: # pragma: no cover
return self.__str__()
def __eq__(self, other: object) -> bool:
if not isinstance(other, Setting):
return NotImplemented
return self.__dict__ == other.__dict__
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, bool]:
dest_name = None
flag = False
@ -167,7 +197,7 @@ class Setting:
if dest:
dest_name = dest
if not dest_name.isidentifier():
raise Exception('Cannot use {dest_name} in a namespace')
raise Exception(f'Cannot use {dest_name} in a namespace')
internal_name = f'{prefix}_{dest_name}'.lstrip('_')
return internal_name, dest_name, flag
@ -201,7 +231,7 @@ if TYPE_CHECKING:
def sanitize_name(name: str) -> str:
return re.sub('[' + re.escape(' -_,.!@#$%^&*(){}[]\',."<>;:') + ']+', '-', name).strip('-')
return re.sub('[' + re.escape(' -_,.!@#$%^&*(){}[]\',."<>;:') + ']+', '_', name).strip('_')
def get_option(options: Values | Namespace, setting: Setting) -> tuple[Any, bool]:
@ -274,10 +304,15 @@ def normalize_config(
if (setting.cmdline and cmdline) or (setting.file and file):
# Ensures the option exists with the default if not already set
value, default = get_option(options, setting)
if not default or default and defaults:
if not default or (default and defaults):
# User has set a custom value or has requested the default value
group_options[setting_name] = value
elif setting_name in group_options:
# defaults have been requested to be removed
del group_options[setting_name]
elif setting_name in group_options:
# Setting type (file or cmdline) has not been requested and should be removed for persistent groups
del group_options[setting_name]
normalized[group_name] = group_options
return Config(normalized, definitions)
@ -344,7 +379,7 @@ def get_namespace(config: Config[T], defaults: bool = True, persistent: bool = T
"""
if isinstance(config.values, Namespace):
options, definitions = normalize_config(config)
options, definitions = normalize_config(config, True, True, defaults=defaults, persistent=persistent)
else:
options, definitions = config
namespace = Namespace()
@ -429,7 +464,7 @@ def parse_cmdline(
description: str,
epilog: str,
args: list[str] | None = None,
config: Namespace | Config[T] | None = None,
config: ns[T] = None,
) -> Config[Values]:
"""
Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`.
@ -496,13 +531,15 @@ class Manager:
def add_group(self, name: str, group: Callable[[Manager], None], exclusive_group: bool = False) -> None:
"""
The primary way to add define options on this class
The primary way to add define options on this class.
Args:
name: The name of the group to define
group: A function that registers individual options using :meth:`add_setting`
exclusive_group: If this group is an argparse exclusive group
"""
if self.current_group_name != '':
raise ValueError('Sub groups are not allowed')
self.current_group_name = name
self.exclusive_group = exclusive_group
group(self)
@ -511,16 +548,23 @@ class Manager:
def add_persistent_group(self, name: str, group: Callable[[Manager], None], exclusive_group: bool = False) -> None:
"""
The primary way to add define options on this class
The primary way to add define options on this class.
This group allows existing values to persist even if there is no corresponding setting defined for it.
Args:
name: The name of the group to define
group: A function that registers individual options using :meth:`add_setting`
exclusive_group: If this group is an argparse exclusive group
"""
if self.current_group_name != '':
raise ValueError('Sub groups are not allowed')
self.current_group_name = name
self.exclusive_group = exclusive_group
self.definitions[self.current_group_name] = Group(True, {})
if self.current_group_name in self.definitions:
if not self.definitions[self.current_group_name].persistent:
raise ValueError('Group already existis and is not persistent')
else:
self.definitions[self.current_group_name] = Group(True, {})
group(self)
self.current_group_name = ''
self.exclusive_group = False

6
settngs/__main__.py Normal file
View File

@ -0,0 +1,6 @@
from __future__ import annotations
from settngs import _main
if __name__ == '__main__':
_main()

0
settngs/py.typed Normal file
View File

View File

@ -1,6 +1,6 @@
[metadata]
name = settngs
version = 0.4.0
version = 0.6.3
description = A library for managing settings
long_description = file: README.md
long_description_content_type = text/markdown
@ -17,16 +17,69 @@ classifiers =
Programming Language :: Python :: Implementation :: PyPy
[options]
py_modules = settngs
packages = find:
install_requires =
typing-extensions;python_version < '3.11'
python_requires = >=3.8
include_package_data = True
[options.packages.find]
exclude =
tests*
testing*
[options.package_data]
settngs = py.typed
[tox:tox]
envlist = py3.8,py3.9,py3.10,py3.11,pypy3
[testenv]
deps = -rrequirements-dev.txt
commands =
coverage erase
coverage run -m pytest {posargs:tests}
coverage report
[testenv:wheel]
description = Generate wheel and tar.gz
labels =
release
build
skip_install = true
deps =
build
commands_pre =
-python -c 'import shutil,pathlib; \
shutil.rmtree("./build/", ignore_errors=True); \
shutil.rmtree("./dist/", ignore_errors=True)'
commands =
python -m build
[testenv:pypi-upload]
description = Upload wheel to PyPi
platform = Linux
labels =
release
skip_install = true
depends = wheel
deps =
twine
passenv =
TWINE_*
setenv =
TWINE_NON_INTERACTIVE=true
commands =
python -m twine upload dist/*.whl dist/*.tar.gz
[pep8]
ignore = E265,E501
max_line_length = 120
[flake8]
extend-ignore = E501, A003
max_line_length = 120
[coverage:run]
plugins = covdefaults

View File

@ -64,6 +64,45 @@ example: list[tuple[list[str], str, str]] = [
),
]
success = [
(
(
('--test-setting',),
dict(
group='tst',
),
), # Equivalent to Setting("--test-setting", group="tst")
{
'action': None,
'choices': None,
'cmdline': True,
'const': None,
'default': None,
'dest': 'test_setting', # dest is calculated by Setting and is not used by argparse
'display_name': 'test_setting', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_test_setting', # Should almost always be "{group}_{dest}"
'metavar': 'TEST_SETTING', # Set manually so argparse doesn't use TST_TEST
'nargs': None,
'required': None,
'type': None,
'argparse_args': ('--test-setting',), # *args actually sent to argparse
'argparse_kwargs': {
'action': None,
'choices': None,
'const': None,
'default': None,
'dest': 'tst_test_setting',
'help': None,
'metavar': 'TEST_SETTING',
'nargs': None,
'required': None,
'type': None,
}, # Non-None **kwargs sent to argparse
},
),
(
(
('--test',),
@ -71,7 +110,7 @@ success = [
group='tst',
dest='testing',
),
), # Equivalent to Setting("--test", group="tst")
), # Equivalent to Setting("--test", group="tst", dest="testing")
{
'action': None,
'choices': None,
@ -79,6 +118,7 @@ success = [
'const': None,
'default': None,
'dest': 'testing', # dest is calculated by Setting and is not used by argparse
'display_name': 'testing', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
@ -117,6 +157,7 @@ success = [
'const': None,
'default': None,
'dest': 'test', # dest is calculated by Setting and is not used by argparse
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
@ -156,6 +197,7 @@ success = [
'const': None,
'default': None,
'dest': 'test', # dest is calculated by Setting and is not used by argparse
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
@ -194,6 +236,7 @@ success = [
'const': None,
'default': None,
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
@ -232,6 +275,7 @@ success = [
'const': None,
'default': None,
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'group': 'tst',
@ -268,6 +312,7 @@ success = [
'const': None,
'default': None,
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'group': '',

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import argparse
import json
from collections import defaultdict
import pytest
@ -58,7 +59,7 @@ def test_get_defaults(settngs_manager):
assert defaults['']['test'] == 'hello'
def test_get_namespace(settngs_manager):
def test_get_defaults_namespace(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults())
assert defaults.test == 'hello'
@ -66,8 +67,8 @@ def test_get_namespace(settngs_manager):
def test_get_namespace_with_namespace(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='hello'))
assert defaults.test == 'hello'
defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='success'))
assert defaults.test == 'success'
def test_get_defaults_group(settngs_manager):
@ -96,6 +97,20 @@ def test_cmdline_only(settngs_manager):
assert 'test2' in file_normalized['tst2']
def test_cmdline_only_persistent_group(settngs_manager):
settngs_manager.add_persistent_group('tst', lambda parser: parser.add_setting('--test', default='hello', file=False))
settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False))
file_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), file=True)
cmdline_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True)
assert 'test' in cmdline_normalized['tst']
assert 'test2' not in cmdline_normalized['tst2']
assert 'test' not in file_normalized['tst']
assert 'test2' in file_normalized['tst2']
def test_normalize(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
@ -117,11 +132,13 @@ def test_normalize(settngs_manager):
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
assert normalized['persistent']['hello'] == 'success'
assert normalized['persistent']['world'] == 'world'
assert not hasattr(normalized_namespace, 'test')
assert hasattr(normalized_namespace, 'tst_test')
assert normalized_namespace.tst_test == 'hello'
assert normalized_namespace.persistent_hello == 'success'
assert normalized_namespace.persistent_world == 'world'
def test_clean_config(settngs_manager):
@ -141,7 +158,7 @@ def test_clean_config(settngs_manager):
assert cleaned['persistent']['hello'] == 'success'
def test_parse_cmdline(settngs_manager, tmp_path):
def test_parse_cmdline(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=True))
normalized, _ = settngs_manager.parse_cmdline(['--test', 'success'])
@ -150,15 +167,25 @@ def test_parse_cmdline(settngs_manager, tmp_path):
assert normalized['tst']['test'] == 'success'
def test_parse_cmdline_with_namespace(settngs_manager, tmp_path):
namespaces = (
lambda definitions: settngs.Config({'tst': {'test': 'fail', 'test2': 'success'}}, definitions),
lambda definitions: settngs.Config(argparse.Namespace(tst_test='fail', tst_test2='success'), definitions),
lambda definitions: argparse.Namespace(tst_test='fail', tst_test2='success'),
)
@pytest.mark.parametrize('ns', namespaces)
def test_parse_cmdline_with_namespace(settngs_manager, ns):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=True))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='fail', cmdline=True))
normalized, _ = settngs_manager.parse_cmdline(
['--test', 'success'], namespace=settngs.Config({'tst': {'test': 'fail'}}, settngs_manager.definitions),
['--test', 'success'], namespace=ns(settngs_manager.definitions),
)
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
assert normalized['tst']['test2'] == 'success'
def test_parse_file(settngs_manager, tmp_path):
@ -286,6 +313,42 @@ def test_cli_explicit_default(settngs_manager, tmp_path):
assert normalized['tst']['test'] == 'success'
def test_adding_to_existing_group(settngs_manager, tmp_path):
def default_to_regular(d):
if isinstance(d, defaultdict):
d = {k: default_to_regular(v) for k, v in d.items()}
return d
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='success'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='success'))
def tst(parser):
parser.add_setting('--test', default='success')
parser.add_setting('--test2', default='success')
settngs_manager2 = settngs.Manager()
settngs_manager2.add_group('tst', tst)
assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions)
def test_adding_to_existing_persistent_group(settngs_manager, tmp_path):
def default_to_regular(d):
if isinstance(d, defaultdict):
d = {k: default_to_regular(v) for k, v in d.items()}
return d
settngs_manager.add_persistent_group('tst', lambda parser: parser.add_setting('--test', default='success'))
settngs_manager.add_persistent_group('tst', lambda parser: parser.add_setting('--test2', default='success'))
def tst(parser):
parser.add_setting('--test', default='success')
parser.add_setting('--test2', default='success')
settngs_manager2 = settngs.Manager()
settngs_manager2.add_persistent_group('tst', tst)
assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions)
def test_example(capsys, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
settings_file = tmp_path / 'settings.json'

17
tox.ini
View File

@ -1,17 +0,0 @@
[tox]
envlist = py3.8,py3.9,py3.10,py3.11,pypy3
[testenv]
deps = -rrequirements-dev.txt
commands =
coverage erase
coverage run -m pytest {posargs:tests}
coverage report
[pep8]
ignore = E265,E501
max_line_length = 120
[flake8]
extend-ignore = E501, A003
max_line_length = 120