Compare commits

..

45 Commits
0.6.2 ... main

Author SHA1 Message Date
pre-commit-ci[bot]
3b0ae0f24a
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0)
- [github.com/hhatto/autopep8: v2.2.0 → v2.3.1](https://github.com/hhatto/autopep8/compare/v2.2.0...v2.3.1)
- [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0)
2024-06-24 17:18:37 +00:00
Timmy Welch
f1735c879d Don't inherit from argparse Namespace 2024-06-06 19:33:07 -07:00
pre-commit-ci[bot]
afdc11eb6f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/reorder-python-imports: v3.12.0 → v3.13.0](https://github.com/asottile/reorder-python-imports/compare/v3.12.0...v3.13.0)
- [github.com/hhatto/autopep8: v2.1.1 → v2.2.0](https://github.com/hhatto/autopep8/compare/v2.1.1...v2.2.0)
2024-06-03 17:17:48 +00:00
pre-commit-ci[bot]
9b71af1c75
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/hhatto/autopep8: v2.1.0 → v2.1.1](https://github.com/hhatto/autopep8/compare/v2.1.0...v2.1.1)
2024-05-27 17:15:51 +00:00
Timmy Welch
8d772c6513 Improve types 2024-05-20 15:09:33 -07:00
Timmy Welch
73552e0dd2 Improve Generic list and set typing 2024-05-19 14:11:18 -07:00
Timmy Welch
58daa4b274 Fix deduplication 2024-05-18 16:46:44 -07:00
Timmy Welch
43f6bf1eac Improve type detection 2024-05-18 15:49:32 -07:00
Timmy Welch
eca7be0c51 Set optional None types 2024-05-18 13:03:50 -07:00
Timmy Welch
709aec31f0 Merge branch 'pre-commit-ci-update-config' 2024-04-29 19:12:57 -07:00
Timmy Welch
a3eb2f8e31 Fix metavar deprecation on BooleanOptionAction 2024-04-29 19:12:48 -07:00
pre-commit-ci[bot]
20dd942784
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)
2024-04-29 17:23:54 +00:00
pre-commit-ci[bot]
3f9cfbb8b4
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2)
- [github.com/hhatto/autopep8: v2.0.4 → v2.1.0](https://github.com/hhatto/autopep8/compare/v2.0.4...v2.1.0)
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)
2024-03-25 17:17:17 +00:00
Timmy Welch
c588fc891e Support type generation for dicts an addition to namespaces 2024-02-22 19:15:47 -08:00
Timmy Welch
1ce6079285 Fix pre-commit 2024-02-22 19:14:45 -08:00
Timmy Welch
7c748f6815 Merge branch 'pre-commit-ci-update-config' 2024-02-22 14:44:51 -08:00
Timmy Welch
8d5b30546e Improve type guessing for generic Sequence types 2024-02-22 14:42:07 -08:00
pre-commit-ci[bot]
cebca481fc
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1)
2024-02-19 17:21:12 +00:00
pre-commit-ci[bot]
dd8cd1188e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0)
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.8.0)
2024-01-08 17:17:39 +00:00
Timmy Welch
d30e73a679 Do not add duplicate settings when generating a namespace 2023-12-17 18:26:57 -08:00
Timmy Welch
fc2a175e5b Fix normalization of settings using a custom dest 2023-12-17 16:09:41 -08:00
Timmy Welch
4c385667e8 Fix settings being overwritten when using the same dest attribute 2023-12-16 16:51:00 -08:00
Timmy Welch
9ffefb3e21 Fix readme example 2023-11-19 00:20:43 -08:00
Timmy Welch
23d0139144 Map argparse --help output to python versions 2023-11-19 00:14:06 -08:00
Timmy Welch
0c49b9309d Persist unknown groups 2023-11-18 23:58:18 -08:00
Timmy Welch
ccacca1b32 Improve generated namespace
Improve formatting of namespace
Allow _guess_type to return any string
Add tests
2023-11-18 23:31:24 -08:00
Timmy Welch
2c79e62765 Fix exported names 2023-11-18 12:43:53 -08:00
pre-commit-ci[bot]
27ca830b60
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/asottile/setup-cfg-fmt: v2.4.0 → v2.5.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.4.0...v2.5.0)
- [github.com/asottile/reorder-python-imports: v3.10.0 → v3.12.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.12.0)
- [github.com/asottile/pyupgrade: v3.10.1 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.15.0)
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.7.0)
2023-11-13 17:18:39 +00:00
Timmy Welch
101eef56ca Sanitize group names 2023-09-04 18:32:14 -05:00
pre-commit-ci[bot]
2f000e12f3
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/add-trailing-comma: v3.0.1 → v3.1.0](https://github.com/asottile/add-trailing-comma/compare/v3.0.1...v3.1.0)
- https://github.com/pre-commit/mirrors-autopep8https://github.com/hhatto/autopep8
- [github.com/hhatto/autopep8: v2.0.2 → v2.0.4](https://github.com/hhatto/autopep8/compare/v2.0.2...v2.0.4)
- [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1)
2023-09-04 19:22:46 +00:00
Timmy Welch
0301e1698c
Merge pull request #5 from lordwelch/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-08-15 07:43:05 -07:00
pre-commit-ci[bot]
6af1e3c562
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/setup-cfg-fmt: v2.3.0 → v2.4.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.3.0...v2.4.0)
- [github.com/asottile/add-trailing-comma: v2.5.1 → v3.0.1](https://github.com/asottile/add-trailing-comma/compare/v2.5.1...v3.0.1)
- [github.com/asottile/pyupgrade: v3.7.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.10.1)
- [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0)
- [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.5.0)
2023-08-14 19:13:59 +00:00
pre-commit-ci[bot]
fe3bce42fd
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0)
- [github.com/asottile/add-trailing-comma: v2.4.0 → v2.5.1](https://github.com/asottile/add-trailing-comma/compare/v2.4.0...v2.5.1)
- [github.com/asottile/dead: v1.5.1 → v1.5.2](https://github.com/asottile/dead/compare/v1.5.1...v1.5.2)
- [github.com/asottile/pyupgrade: v3.4.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.7.0)
2023-06-19 19:05:50 +00:00
Timmy Welch
4c41e6f588 Use a custom class for typing
Add support for nargs to default to list[str]
2023-06-09 15:41:18 -07:00
Timmy Welch
b599097cc1 Adds a function to generate a class for typing a namespace 2023-06-08 22:54:36 -07:00
Timmy Welch
8af75d3962 Update docstrings and ensure parameters are consistent 2023-06-08 21:44:07 -07:00
Timmy Welch
e57ee25a60 Make get_namespace consistent with normalize_config 2023-06-08 21:44:07 -07:00
pre-commit-ci[bot]
14357c14de
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/setup-cfg-fmt: v2.2.0 → v2.3.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.2.0...v2.3.0)
- https://github.com/asottile/reorder_python_importshttps://github.com/asottile/reorder-python-imports
- [github.com/asottile/dead: v1.5.0 → v1.5.1](https://github.com/asottile/dead/compare/v1.5.0...v1.5.1)
- [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0)
- [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0)
2023-06-05 18:41:50 +00:00
Timmy Welch
3ed9571156
Merge pull request #2 from lordwelch/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-05-06 09:32:31 -07:00
pre-commit-ci[bot]
c76ded2c4f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)
2023-05-01 18:48:23 +00:00
Timmy Welch
78ac274e31
Include pyproject.toml 2023-04-26 15:32:06 -07:00
Timmy Welch
05c2196fcf
Update minimum requirements for typing-extensions 2023-04-26 15:04:57 -07:00
Timmy Welch
e2ff779c30
Add py.typed 2023-04-25 00:13:51 -07:00
Timmy Welch
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
pre-commit-ci[bot]
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
13 changed files with 1952 additions and 837 deletions

View File

@ -33,7 +33,7 @@ jobs:
- name: Build and install wheel
run: |
python -m build
tox run -m build
python -m pip install dist/*.whl
- name: tox

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

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@ -10,38 +10,39 @@ repos:
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.13.0
hooks:
- id: reorder-python-imports
args: [--py38-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.4.0
rev: v3.1.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/dead
rev: v1.5.0
rev: v1.5.2
hooks:
- id: dead
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.16.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.1
exclude: tests
- repo: https://github.com/hhatto/autopep8
rev: v2.3.1
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 7.1.0
hooks:
- id: flake8
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print]
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-print]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
rev: v1.10.0
hooks:
- id: mypy

View File

@ -29,14 +29,14 @@ $ python -m settngs
Hello lordwelch
$ python -m settngs -v
Hello lordwelch
merged_namespace.values.example_verbose=True
merged_namespace.values.Example_Group__verbose=True
$ python -m settngs -v -s
Hello lordwelch
Successfully saved settings to settings.json
merged_namespace.values.example_verbose=True
merged_namespace.values.Example_Group__verbose=True
$ python -m settngs
Hello lordwelch
merged_namespace.values.example_verbose=True
merged_namespace.values.Example_Group__verbose=True
$ cat >settings.json << EOF
{
"example": {
@ -64,7 +64,7 @@ Hello world
settngs.json at the end:
```json
{
"example": {
"Example Group": {
"hello": "world",
"verbose": false
},

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[build-system]
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
local_scheme = "no-local-version"

View File

@ -1,687 +0,0 @@
from __future__ import annotations
import argparse
import json
import logging
import pathlib
import re
import sys
from argparse import Namespace
from collections import defaultdict
from collections.abc import Sequence
from typing import Any
from typing import Callable
from typing import Dict
from typing import Generic
from typing import NoReturn
from typing import overload
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
logger = logging.getLogger(__name__)
if sys.version_info < (3, 11): # pragma: no cover
from typing_extensions import NamedTuple
else: # pragma: no cover
from typing import NamedTuple
if sys.version_info < (3, 9): # pragma: no cover
def removeprefix(self: str, prefix: str, /) -> str:
if self.startswith(prefix):
return self[len(prefix):]
else:
return self[:]
class BooleanOptionalAction(argparse.Action):
def __init__(
self,
option_strings,
dest,
default=None,
type=None, # noqa: A002
choices=None,
required=False,
help=None, # noqa: A002
metavar=None,
):
_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
option_string = '--no-' + option_string[2:]
_option_strings.append(option_string)
if help is not None and default is not None and default is not argparse.SUPPRESS:
help += ' (default: %(default)s)'
super().__init__(
option_strings=_option_strings,
dest=dest,
nargs=0,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar,
)
def __call__(self, parser, namespace, values, option_string=None): # dead: disable
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--no-'))
else: # pragma: no cover
from argparse import BooleanOptionalAction
removeprefix = str.removeprefix
class Setting:
def __init__(
self,
# From argparse
*names: str,
action: type[argparse.Action] | None = None,
nargs: str | int | None = None,
const: str | None = None,
default: str | None = None,
type: Callable[..., Any] | None = None, # noqa: A002
choices: Sequence[Any] | None = None,
required: bool | None = None,
help: str | None = None, # noqa: A002
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
# Argument names will still cause an exception if there is a conflict e.g. if '-f' is defined twice
self.internal_name, dest, flag = self.get_dest(group, names, dest)
args: Sequence[str] = names
# We then also set the metavar so that '--config' in the group runtime shows as 'CONFIG' instead of 'RUNTIME_CONFIG'
if not metavar and action not in ('store_true', 'store_false', 'count'):
metavar = dest.upper()
# If we are not a flag, no '--' or '-' in front
# we prefix the first name with the group as argparse sets dest to args[0]
# I believe internal name may be able to be used here
if not flag:
args = tuple((f'{group}_{names[0]}'.lstrip('_'), *names[1:]))
self.action = action
self.nargs = nargs
self.const = const
self.default = default
self.type = type
self.choices = choices
self.required = required
self.help = help
self.metavar = metavar
self.dest = dest
self.cmdline = cmdline
self.file = file
self.argparse_args = args
self.group = group
self.exclusive = exclusive
self.display_name = display_name or dest
self.argparse_kwargs = {
'action': action,
'nargs': nargs,
'const': const,
'default': default,
'type': type,
'choices': choices,
'required': required,
'help': help,
'metavar': metavar,
'dest': self.internal_name if flag else None,
}
def __str__(self) -> str: # pragma: no cover
return f'Setting({self.argparse_args}, type={self.type}, file={self.file}, cmdline={self.cmdline}, kwargs={self.argparse_kwargs})'
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
for n in names:
if n.startswith('--'):
flag = True
dest_name = sanitize_name(n)
break
if n.startswith('-'):
flag = True
if dest_name is None:
dest_name = names[0]
if dest:
dest_name = dest
if not dest_name.isidentifier():
raise Exception(f'Cannot use {dest_name} in a namespace')
internal_name = f'{prefix}_{dest_name}'.lstrip('_')
return internal_name, dest_name, flag
def filter_argparse_kwargs(self) -> dict[str, Any]:
return {k: v for k, v in self.argparse_kwargs.items() if v is not None}
def to_argparse(self) -> tuple[Sequence[str], dict[str, Any]]:
return self.argparse_args, self.filter_argparse_kwargs()
class Group(NamedTuple):
persistent: bool
v: dict[str, Setting]
Values = Dict[str, Dict[str, Any]]
Definitions = Dict[str, Group]
T = TypeVar('T', Values, Namespace)
class Config(NamedTuple, Generic[T]):
values: T
definitions: Definitions
if TYPE_CHECKING:
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
ns = Namespace | Config[T] | None
def sanitize_name(name: str) -> str:
return re.sub('[' + re.escape(' -_,.!@#$%^&*(){}[]\',."<>;:') + ']+', '_', name).strip('_')
def get_option(options: Values | Namespace, setting: Setting) -> tuple[Any, bool]:
"""
Helper function to retrieve the value for a setting and if the value is the default value
Args:
options: Dictionary or namespace of options
setting: The setting object describing the value to retrieve
"""
if isinstance(options, dict):
value = options.get(setting.group, {}).get(setting.dest, setting.default)
else:
value = getattr(options, setting.internal_name, setting.default)
return value, value == setting.default
def get_options(options: Config[T], group: str) -> dict[str, Any]:
"""
Helper function to retrieve all of the values for a group. Only to be used on persistent groups.
Args:
options: Dictionary or namespace of options
group: The name of the group to retrieve
"""
if isinstance(options[0], dict):
values = options[0].get(group, {}).copy()
else:
internal_names = {x.internal_name: x for x in options[1][group].v.values()}
values = {}
v = vars(options[0])
for name, value in v.items():
if name.startswith(f'{group}_'):
if name in internal_names:
values[internal_names[name].dest] = value
else:
values[removeprefix(name, f'{group}_')] = value
return values
def normalize_config(
config: Config[T],
file: bool = False,
cmdline: bool = False,
defaults: bool = True,
persistent: bool = True,
) -> Config[Values]:
"""
Creates an `OptionValues` dictionary with setting definitions taken from `self.definitions`
and values taken from `raw_options` and `raw_options_2' if defined.
Values are assigned so if the value is a dictionary mutating it will mutate the original.
Args:
raw_options: The dict or Namespace to normalize options from
definitions: The definition of the options
file: Include file options
cmdline: Include cmdline options
defaults: Include default values in the returned dict
persistent: Include unknown keys in persistent groups
"""
normalized: Values = {}
options, definitions = config
for group_name, group in definitions.items():
group_options = {}
if group.persistent and persistent:
group_options = get_options(config, group_name)
for setting_name, setting in group.v.items():
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):
# 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)
def parse_file(definitions: Definitions, filename: pathlib.Path) -> tuple[Config[Values], bool]:
"""
Helper function to read options from a json dictionary from a file
Args:
filename: A pathlib.Path object to read a json dictionary from
"""
options: Values = {}
success = True
if filename.exists():
try:
with filename.open() as file:
opts = json.load(file)
if isinstance(opts, dict):
options = opts
except Exception:
logger.exception('Failed to load config file: %s', filename)
success = False
else:
logger.info('No config file found')
success = True
return (normalize_config(Config(options, definitions), file=True), success)
def clean_config(
config: Config[T], file: bool = False, cmdline: bool = False,
) -> Values:
"""
Normalizes options and then cleans up empty groups
Args:
options:
file:
cmdline:
Returns:
"""
clean_options, definitions = normalize_config(config, file=file, cmdline=cmdline)
for group in list(clean_options.keys()):
if not clean_options[group]:
del clean_options[group]
return clean_options
def defaults(definitions: Definitions) -> Config[Values]:
return normalize_config(Config(Namespace(), definitions), file=True, cmdline=True)
def get_namespace(config: Config[T], defaults: bool = True, persistent: bool = True) -> Config[Namespace]:
"""
Returns an Namespace object with options in the form "{group_name}_{setting_name}"
`options` should already be normalized.
Throws an exception if the internal_name is duplicated
Args:
options: Normalized options to turn into a Namespace
defaults: Include default values in the returned dict
persistent: Include unknown keys in persistent groups
"""
if isinstance(config.values, Namespace):
options, definitions = normalize_config(config, True, True, defaults=defaults, persistent=persistent)
else:
options, definitions = config
namespace = Namespace()
for group_name, group in definitions.items():
if group.persistent and persistent:
group_options = get_options(config, group_name)
for name, value in group_options.items():
if name in group.v:
internal_name, default = group.v[name].internal_name, group.v[name].default == value
else:
internal_name, default = f'{group_name}_' + sanitize_name(name), None
if hasattr(namespace, internal_name):
raise Exception(f'Duplicate internal name: {internal_name}')
if not default or default and defaults:
setattr(namespace, internal_name, value)
else:
for setting_name, setting in group.v.items():
if hasattr(namespace, setting.internal_name):
raise Exception(f'Duplicate internal name: {setting.internal_name}')
value, default = get_option(options, setting)
if not default or default and defaults:
setattr(namespace, setting.internal_name, value)
return Config(namespace, definitions)
def save_file(
config: Config[T], filename: pathlib.Path,
) -> bool:
"""
Helper function to save options from a json dictionary to a file
Args:
options: The options to save to a json dictionary
filename: A pathlib.Path object to save the json dictionary to
"""
file_options = clean_config(config, file=True)
if not filename.exists():
filename.parent.mkdir(exist_ok=True, parents=True)
filename.touch()
try:
json_str = json.dumps(file_options, indent=2)
filename.write_text(json_str + '\n', encoding='utf-8')
except Exception:
logger.exception('Failed to save config file: %s', filename)
return False
return True
def create_argparser(definitions: Definitions, description: str, epilog: str) -> argparse.ArgumentParser:
"""Creates an :class:`argparse.ArgumentParser` from all cmdline settings"""
groups: dict[str, ArgParser] = {}
argparser = argparse.ArgumentParser(
description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter,
)
for group_name, group in definitions.items():
for setting_name, setting in group.v.items():
if setting.cmdline:
argparse_args, argparse_kwargs = setting.to_argparse()
current_group: ArgParser = argparser
if setting.group:
if setting.group not in groups:
if setting.exclusive:
groups[setting.group] = argparser.add_argument_group(
setting.group,
).add_mutually_exclusive_group()
else:
groups[setting.group] = argparser.add_argument_group(setting.group)
# hard coded exception for files
if not (setting.group == 'runtime' and setting.nargs == '*'):
current_group = groups[setting.group]
current_group.add_argument(*argparse_args, **argparse_kwargs)
return argparser
def parse_cmdline(
definitions: Definitions,
description: str,
epilog: str,
args: list[str] | None = None,
config: ns[T] = None,
) -> Config[Values]:
"""
Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`.
`args` and `namespace` are passed to `argparse.ArgumentParser.parse_args`
Args:
args: Passed to argparse.ArgumentParser.parse
namespace: Passed to argparse.ArgumentParser.parse
"""
namespace = None
if isinstance(config, Config):
if isinstance(config.values, Namespace):
namespace = config.values
else:
namespace = get_namespace(config, defaults=False)[0]
else:
namespace = config
argparser = create_argparser(definitions, description, epilog)
ns = argparser.parse_args(args, namespace=namespace)
return normalize_config(Config(ns, definitions), cmdline=True, file=True)
def parse_config(
definitions: Definitions,
description: str,
epilog: str,
config_path: pathlib.Path,
args: list[str] | None = None,
) -> tuple[Config[Values], bool]:
file_options, success = parse_file(definitions, config_path)
cmdline_options = parse_cmdline(
definitions, description, epilog, args, get_namespace(file_options, defaults=False),
)
final_options = normalize_config(cmdline_options, file=True, cmdline=True)
return (final_options, success)
class Manager:
"""docstring for Manager"""
def __init__(self, description: str = '', epilog: str = '', definitions: Definitions | Config[T] | None = None):
# This one is never used, it just makes MyPy happy
self.argparser = argparse.ArgumentParser(description=description, epilog=epilog)
self.description = description
self.epilog = epilog
if isinstance(definitions, Config):
self.definitions = definitions.definitions
else:
self.definitions = defaultdict(lambda: Group(False, {}), definitions or {})
self.exclusive_group = False
self.current_group_name = ''
def create_argparser(self) -> None:
self.argparser = create_argparser(self.definitions, self.description, self.epilog)
def add_setting(self, *args: Any, **kwargs: Any) -> None:
"""Takes passes all arguments through to `Setting`, `group` and `exclusive` are already set"""
setting = Setting(*args, **kwargs, group=self.current_group_name, exclusive=self.exclusive_group)
self.definitions[self.current_group_name].v[setting.dest] = setting
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.
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)
self.current_group_name = ''
self.exclusive_group = False
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.
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
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
def exit(self, *args: Any, **kwargs: Any) -> NoReturn:
"""See :class:`~argparse.ArgumentParser`"""
self.argparser.exit(*args, **kwargs)
raise SystemExit(99)
def defaults(self) -> Config[Values]:
return defaults(self.definitions)
def clean_config(
self, options: T | Config[T], file: bool = False, cmdline: bool = False,
) -> Values:
if isinstance(options, Config):
config = options
else:
config = Config(options, self.definitions)
return clean_config(config, file=file, cmdline=cmdline)
def normalize_config(
self,
options: T | Config[T],
file: bool = False,
cmdline: bool = False,
defaults: bool = True,
) -> Config[Values]:
if isinstance(options, Config):
config = options
else:
config = Config(options, self.definitions)
return normalize_config(
config=config,
file=file,
cmdline=cmdline,
defaults=defaults,
)
@overload
def get_namespace(self, options: Values, defaults: bool = True) -> Namespace:
...
@overload
def get_namespace(self, options: Config[Values], defaults: bool = True) -> Config[Namespace]:
...
def get_namespace(self, options: Values | Config[Values], defaults: bool = True) -> Config[Namespace] | Namespace:
if isinstance(options, Config):
self.definitions = options[1]
return get_namespace(options, defaults=defaults)
else:
return get_namespace(Config(options, self.definitions), defaults=defaults)
def parse_file(self, filename: pathlib.Path) -> tuple[Config[Values], bool]:
return parse_file(filename=filename, definitions=self.definitions)
def save_file(self, options: T | Config[T], filename: pathlib.Path) -> bool:
if isinstance(options, Config):
return save_file(options, filename=filename)
return save_file(Config(options, self.definitions), filename=filename)
def parse_cmdline(self, args: list[str] | None = None, namespace: ns[T] = None) -> Config[Values]:
return parse_cmdline(self.definitions, self.description, self.epilog, args, namespace)
def parse_config(self, config_path: pathlib.Path, args: list[str] | None = None) -> tuple[Config[Values], bool]:
return parse_config(self.definitions, self.description, self.epilog, config_path, args)
def example(manager: Manager) -> None:
manager.add_setting(
'--hello',
default='world',
)
manager.add_setting(
'--save', '-s',
default=False,
action='store_true',
file=False,
)
manager.add_setting(
'--verbose', '-v',
default=False,
action=BooleanOptionalAction, # Added in Python 3.9
)
def persistent(manager: Manager) -> None:
manager.add_setting(
'--test', '-t',
default=False,
action=BooleanOptionalAction, # Added in Python 3.9
)
def _main(args: list[str] | None = None) -> None:
settings_path = pathlib.Path('./settings.json')
manager = Manager(description='This is an example', epilog='goodbye!')
manager.add_group('example', example)
manager.add_persistent_group('persistent', persistent)
file_config, success = manager.parse_file(settings_path)
file_namespace = manager.get_namespace(file_config)
merged_config = manager.parse_cmdline(args=args, namespace=file_namespace)
merged_namespace = manager.get_namespace(merged_config)
print(f'Hello {merged_config.values["example"]["hello"]}') # noqa: T201
if merged_namespace.values.example_save:
if manager.save_file(merged_config, settings_path):
print(f'Successfully saved settings to {settings_path}') # noqa: T201
else:
print(f'Failed saving settings to a {settings_path}') # noqa: T201
if merged_namespace.values.example_verbose:
print(f'{merged_namespace.values.example_verbose=}') # noqa: T201
if __name__ == '__main__':
_main()

1164
settngs/__init__.py Normal file

File diff suppressed because it is too large Load Diff

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,5 @@
[metadata]
name = settngs
version = 0.6.2
description = A library for managing settings
long_description = file: README.md
long_description_content_type = text/markdown
@ -17,16 +16,71 @@ classifiers =
Programming Language :: Python :: Implementation :: PyPy
[options]
py_modules = settngs
packages = find:
install_requires =
typing-extensions;python_version < '3.11'
typing-extensions>=4.3.0;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,py3.12,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
per-file-ignores =
*_test.py: LN001
[coverage:run]
plugins = covdefaults
@ -36,6 +90,7 @@ fail_under = 95
[mypy]
check_untyped_defs = true
disallow_any_generics = true
warn_return_any = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_optional = true
@ -43,7 +98,9 @@ warn_redundant_casts = true
warn_unused_ignores = true
[mypy-testing.*]
warn_return_any = false
disallow_untyped_defs = false
[mypy-tests.*]
warn_return_any = false
disallow_untyped_defs = false

View File

@ -15,52 +15,52 @@ example: list[tuple[list[str], str, str]] = [
(
['--hello', 'lordwelch', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
),
(
[],
'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
),
(
['-v'],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
'Hello lordwelch\nmerged_namespace.values.Example_Group__verbose=True\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
),
(
['-v', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
'Hello lordwelch\nSuccessfully saved settings to settings.json\nmerged_namespace.values.Example_Group__verbose=True\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
),
(
[],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
'Hello lordwelch\nmerged_namespace.values.Example_Group__verbose=True\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
),
(
['manual settings.json'],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
'Hello lordwelch\nmerged_namespace.values.Example_Group__verbose=True\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
),
(
['--no-verbose', '-t'],
'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
),
(
['--no-verbose', '-s', '-t'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": true,\n "hello": "world"\n }\n}\n',
'{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": true,\n "hello": "world"\n }\n}\n',
),
(
['--hello', 'world', '--no-verbose', '--no-test', '-s'],
'Hello world\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
'{\n "Example Group": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
),
(
[],
'Hello world\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
'{\n "Example Group": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
),
]
success = [
@ -77,13 +77,15 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test_setting', # dest is calculated by Setting and is not used by argparse
'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,
'flag': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_test_setting', # Should almost always be "{group}_{dest}"
'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,
@ -94,7 +96,7 @@ success = [
'choices': None,
'const': None,
'default': None,
'dest': 'tst_test_setting',
'dest': 'tst__test_setting',
'help': None,
'metavar': 'TEST_SETTING',
'nargs': None,
@ -117,13 +119,15 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test', # setting_name is calculated by Setting and is not used by argparse
'dest': 'testing', # dest is calculated by Setting and is not used by argparse
'display_name': 'testing', # defaults to dest
'exclusive': False,
'file': True,
'flag': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_testing', # Should almost always be "{group}_{dest}"
'internal_name': 'tst__testing', # Should almost always be "{group}__{dest}"
'metavar': 'TESTING', # Set manually so argparse doesn't use TST_TEST
'nargs': None,
'required': None,
@ -134,7 +138,7 @@ success = [
'choices': None,
'const': None,
'default': None,
'dest': 'tst_testing',
'dest': 'tst__testing',
'help': None,
'metavar': 'TESTING',
'nargs': None,
@ -156,13 +160,15 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test', # dest is calculated by Setting and is not used by argparse
'dest': 'test', # dest is calculated by Setting and is not used by argparse
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'flag': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_test', # Should almost always be "{group}_{dest}"
'internal_name': 'tst__test', # Should almost always be "{group}__{dest}"
'metavar': 'TEST', # Set manually so argparse doesn't use TST_TEST
'nargs': None,
'required': None,
@ -173,7 +179,7 @@ success = [
'choices': None,
'const': None,
'default': None,
'dest': 'tst_test',
'dest': 'tst__test',
'help': None,
'metavar': 'TEST',
'nargs': None,
@ -196,13 +202,15 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test', # dest is calculated by Setting and is not used by argparse
'dest': 'test', # dest is calculated by Setting and is not used by argparse
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'flag': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_test', # Should almost always be "{group}_{dest}"
'internal_name': 'tst__test', # Should almost always be "{group}__{dest}"
'metavar': None, # store_true does not get a metavar
'nargs': None,
'required': None,
@ -213,7 +221,7 @@ success = [
'choices': None,
'const': None,
'default': None,
'dest': 'tst_test',
'dest': 'tst__test',
'help': None,
'metavar': None,
'nargs': None,
@ -235,13 +243,15 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test',
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'flag': True,
'group': 'tst',
'help': None,
'internal_name': 'tst_test',
'internal_name': 'tst__test',
'metavar': 'TEST',
'nargs': None,
'required': None,
@ -252,7 +262,7 @@ success = [
'choices': None,
'const': None,
'default': None,
'dest': 'tst_test',
'dest': 'tst__test',
'help': None,
'metavar': 'TEST',
'nargs': None,
@ -274,18 +284,20 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test',
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'flag': False,
'group': 'tst',
'help': None,
'internal_name': 'tst_test',
'internal_name': 'tst__test',
'metavar': 'TEST',
'nargs': None,
'required': None,
'type': None,
'argparse_args': ('tst_test',),
'argparse_args': ('tst__test',),
'argparse_kwargs': {
'action': None,
'choices': None,
@ -311,10 +323,12 @@ success = [
'cmdline': True,
'const': None,
'default': None,
'setting_name': 'test',
'dest': 'test',
'display_name': 'test', # defaults to dest
'exclusive': False,
'file': True,
'flag': True,
'group': '',
'help': None,
'internal_name': 'test', # No group, leading _ is stripped

View File

@ -1,8 +1,12 @@
from __future__ import annotations
import argparse
import ast
import json
import pathlib
import sys
from collections import defaultdict
from typing import Generator
import pytest
@ -13,8 +17,47 @@ from testing.settngs import failure
from testing.settngs import success
if sys.version_info >= (3, 10): # pragma: no cover
List = list
Set = set
help_output = '''\
usage: __main__.py [-h] [TEST ...]
positional arguments:
TEST
options:
-h, --help show this help message and exit
'''
elif sys.version_info < (3, 9): # pragma: no cover
from typing import List
from typing import Set
help_output = '''\
usage: __main__.py [-h] [TEST [TEST ...]]
positional arguments:
TEST
optional arguments:
-h, --help show this help message and exit
'''
else: # pragma: no cover
List = list
Set = set
help_output = '''\
usage: __main__.py [-h] [TEST ...]
positional arguments:
TEST
optional arguments:
-h, --help show this help message and exit
'''
@pytest.fixture
def settngs_manager():
def settngs_manager() -> Generator[settngs.Manager, None, None]:
manager = settngs.Manager()
yield manager
@ -53,92 +96,313 @@ def test_add_setting(settngs_manager):
assert settngs_manager.add_setting('--test') is None
def test_get_defaults(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.defaults()
assert defaults['']['test'] == 'hello'
def test_add_setting_invalid_name(settngs_manager):
with pytest.raises(Exception, match='Cannot use test¥ in a namespace'):
assert settngs_manager.add_setting('--test¥') is None
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'
def test_sub_group(settngs_manager):
with pytest.raises(Exception, match='Sub groups are not allowed'):
settngs_manager.add_group('tst', lambda parser: parser.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello')))
def test_sub_persistent_group(settngs_manager):
with pytest.raises(Exception, match='Sub groups are not allowed'):
settngs_manager.add_persistent_group('tst', lambda parser: parser.add_persistent_group('tst', lambda parser: parser.add_setting('--test2', default='hello')))
def test_redefine_persistent_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'))
with pytest.raises(Exception, match='Group already exists and is not persistent'):
settngs_manager.add_persistent_group('tst', None)
def test_exclusive_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'), exclusive_group=True)
settngs_manager.create_argparser()
args = settngs_manager.argparser.parse_args(['--test', 'never'])
assert args.tst__test == 'never'
with pytest.raises(SystemExit):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'), exclusive_group=True)
settngs_manager.create_argparser()
args = settngs_manager.argparser.parse_args(['--test', 'never', '--test2', 'never'])
def test_files_group(capsys, settngs_manager):
settngs_manager.add_group('runtime', lambda parser: parser.add_setting('test', default='hello', nargs='*'))
settngs_manager.create_argparser()
settngs_manager.argparser.print_help()
captured = capsys.readouterr()
assert captured.out == help_output
def test_setting_without_group(capsys, settngs_manager):
settngs_manager.add_setting('test', default='hello', nargs='*')
settngs_manager.create_argparser()
settngs_manager.argparser.print_help()
captured = capsys.readouterr()
assert captured.out == help_output
class TestValues:
def test_invalid_normalize(self, settngs_manager):
with pytest.raises(ValueError) as excinfo:
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.normalize_config({}, file=False, cmdline=False)
assert str(excinfo.value) == 'Invalid parameters: you must set either file or cmdline to True'
def test_get_defaults(self, settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.defaults()
assert defaults['']['test'] == 'hello'
def test_get_defaults_group(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.defaults()
assert defaults['tst']['test'] == 'hello'
def test_get_defaults_group_space(self, settngs_manager):
settngs_manager.add_group('Testing tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.defaults()
assert defaults['Testing tst']['test'] == 'hello'
def test_cmdline_only(self, settngs_manager):
settngs_manager.add_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' not in file_normalized['tst'] # cmdline option not in normalized config
assert 'test2' in file_normalized['tst2'] # file option in normalized config
assert 'test' in cmdline_normalized['tst'] # cmdline option in normalized config
assert 'test2' not in cmdline_normalized['tst2'] # file option not in normalized config
def test_cmdline_only_persistent_group(self, 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' not in file_normalized['tst']
assert 'test2' in file_normalized['tst2']
assert 'test' in cmdline_normalized['tst']
assert 'test2' not in cmdline_normalized['tst2']
def test_normalize_defaults(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.normalize_config(defaults, file=True, default=False)
assert defaults_normalized.values['tst'] == {}
assert defaults_normalized.values['tst_persistent'] == {}
non_defaults = settngs_manager.defaults()
non_defaults.values['tst']['test'] = 'world'
non_defaults.values['tst_persistent']['test'] = 'world'
non_defaults_normalized = settngs_manager.normalize_config(non_defaults, file=True, default=False)
assert non_defaults_normalized.values['tst'] == {'test': 'world'}
assert non_defaults_normalized.values['tst_persistent'] == {'test': 'world'}
def test_normalize_dest(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', dest='test', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.normalize_config(defaults, file=True, default=False)
assert defaults_normalized.values['tst'] == {}
assert defaults_normalized.values['tst_persistent'] == {}
non_defaults = settngs_manager.defaults()
non_defaults.values['tst']['test'] = 'world'
non_defaults.values['tst_persistent']['test'] = 'world'
non_defaults_normalized = settngs_manager.normalize_config(non_defaults, file=True, default=False)
assert non_defaults_normalized.values['tst'] == {'test': 'world'}
assert non_defaults_normalized.values['tst_persistent'] == {'test': 'world'}
def test_normalize(self, 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'))
defaults = settngs_manager.defaults()
defaults.values['test'] = 'fail' # Not defined in settngs_manager, should be removed
defaults.values['persistent']['hello'] = 'success' # Not defined in settngs_manager, should stay
normalized, _ = settngs_manager.normalize_config(defaults, file=True)
assert 'test' not in normalized
assert 'tst' in normalized
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
assert normalized['persistent']['hello'] == 'success'
assert normalized['persistent']['world'] == 'world'
def test_unknown_group(self):
manager = settngs.Manager()
manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
manager_unknown = settngs.Manager()
manager_unknown.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
# This manager doesn't know about this group
# manager_unknown.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
defaults = manager.defaults()
defaults.values['test'] = 'fail' # type: ignore[index] # Not defined in manager, should be removed
defaults.values['persistent']['hello'] = 'success' # Group is not defined in manager_unknown, should stay
normalized, _ = manager_unknown.normalize_config(defaults.values, file=True)
assert 'test' not in normalized
assert 'tst' in normalized
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
assert normalized['persistent']['hello'] == 'success'
assert normalized['persistent']['world'] == 'world'
class TestNamespace:
def test_invalid_normalize(self, settngs_manager):
with pytest.raises(ValueError) as excinfo:
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=False, cmdline=False)
assert str(excinfo.value) == 'Invalid parameters: you must set either file or cmdline to True'
def test_get_defaults(self, settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
assert defaults.test == 'hello'
def test_get_defaults_group(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
assert defaults.tst__test == 'hello'
def test_get_defaults_group_space(self, settngs_manager):
settngs_manager.add_group('Testing tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
assert defaults.Testing_tst__test == 'hello'
def test_cmdline_only(self, settngs_manager):
settngs_manager.add_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.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), file=True), file=True)
cmdline_normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True), cmdline=True)
assert 'tst__test' not in file_normalized.__dict__
assert 'tst2__test2' in file_normalized.__dict__
assert 'tst__test' in cmdline_normalized.__dict__
assert 'tst2__test2' not in cmdline_normalized.__dict__
def test_cmdline_only_persistent_group(self, 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.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), file=True), file=True)
cmdline_normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True), cmdline=True)
assert 'tst__test' not in file_normalized.__dict__
assert 'tst2__test2' in file_normalized.__dict__
assert 'tst__test' in cmdline_normalized.__dict__
assert 'tst2__test2' not in cmdline_normalized.__dict__
def test_normalize_defaults(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True, default=False), file=True, default=False)
assert defaults_normalized.values.__dict__ == {}
non_defaults = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
non_defaults.values.tst__test = 'world'
non_defaults.values.tst_persistent__test = 'world'
non_defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(non_defaults, file=True, default=False), file=True, default=False)
assert non_defaults_normalized.values.tst__test == 'world'
assert non_defaults_normalized.values.tst_persistent__test == 'world'
def test_normalize_dest(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', dest='test', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True, default=False), file=True, default=False)
assert defaults_normalized.values.__dict__ == {}
non_defaults = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
non_defaults.values.tst__test = 'world'
non_defaults.values.tst_persistent__test = 'world'
non_defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(non_defaults, file=True, default=False), file=True, default=False)
assert non_defaults_normalized.values.tst__test == 'world'
assert non_defaults_normalized.values.tst_persistent__test == 'world'
def test_normalize(self, 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'))
defaults = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
defaults.values.test = 'fail' # Not defined in settngs_manager, should be removed
defaults.values.persistent__hello = 'success' # Not defined in settngs_manager, should stay
normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True), file=True)
assert not hasattr(normalized, 'test')
assert hasattr(normalized, 'tst__test')
assert normalized.tst__test == 'hello'
assert normalized.persistent__hello == 'success'
assert normalized.persistent__world == 'world'
def test_normalize_unknown_group(self, settngs_manager):
manager = settngs.Manager()
manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
manager_unknown = settngs.Manager()
manager_unknown.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
# This manager doesn't know about this group
# manager_unknown.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
defaults = manager.get_namespace(manager.defaults(), file=True, cmdline=True)
defaults.values.test = 'fail' # Not defined in manager, should be removed
defaults.values.persistent__hello = 'success' # Not defined in manager, should stay
normalized, _ = manager_unknown.get_namespace(defaults.values, file=True)
assert not hasattr(normalized, 'test')
assert hasattr(normalized, 'tst__test')
assert normalized.tst__test == 'hello'
assert normalized.persistent__hello == 'success'
assert normalized.persistent__world == 'world'
def test_get_namespace_with_namespace(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='success'))
defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='success'), file=True)
assert defaults.test == 'success'
def test_get_defaults_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.defaults()
assert defaults['tst']['test'] == 'hello'
def test_get_namespace_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults())
assert defaults.tst_test == 'hello'
def test_cmdline_only(settngs_manager):
settngs_manager.add_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_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'))
defaults = settngs_manager.defaults()
defaults.values['test'] = 'fail' # Not defined in settngs_manager, should be removed
defaults.values['persistent']['hello'] = 'success' # Not defined in settngs_manager, should stay
defaults_namespace = settngs_manager.get_namespace(settngs_manager.defaults())
defaults_namespace.values.test = 'fail' # Not defined in settngs_manager, should be removed
defaults_namespace.values.persistent_hello = 'success' # Not defined in settngs_manager, should stay
normalized, _ = settngs_manager.normalize_config(defaults, file=True)
normalized_from_namespace = settngs_manager.normalize_config(defaults_namespace, file=True)
normalized_namespace, _ = settngs_manager.get_namespace(normalized_from_namespace)
assert 'test' not in normalized
assert 'tst' in normalized
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'
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True)
assert defaults.tst__test == 'hello'
def test_clean_config(settngs_manager):
@ -169,8 +433,8 @@ def test_parse_cmdline(settngs_manager):
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'),
lambda definitions: settngs.Config(argparse.Namespace(tst__test='fail', tst__test2='success'), definitions),
lambda definitions: argparse.Namespace(tst__test='fail', tst__test2='success'),
)
@ -180,7 +444,7 @@ def test_parse_cmdline_with_namespace(settngs_manager, ns):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='fail', cmdline=True))
normalized, _ = settngs_manager.parse_cmdline(
['--test', 'success'], namespace=ns(settngs_manager.definitions),
['--test', 'success'], config=ns(settngs_manager.definitions),
)
assert 'test' in normalized['tst']
@ -331,7 +595,7 @@ def test_adding_to_existing_group(settngs_manager, tmp_path):
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 test_adding_to_existing_persistent_group(settngs_manager: settngs.Manager, tmp_path: pathlib.Path) -> None:
def default_to_regular(d):
if isinstance(d, defaultdict):
d = {k: default_to_regular(v) for k, v in d.items()}
@ -349,6 +613,233 @@ def test_adding_to_existing_persistent_group(settngs_manager, tmp_path):
assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions)
class test_type(int):
...
def _typed_function(something: str) -> test_type: # pragma: no cover
return test_type()
def _typed_list_generic_function(something: test_type) -> List[test_type]: # pragma: no cover
return [test_type()]
def _typed_list_function() -> List: # type: ignore[type-arg] # pragma: no cover
return []
def _typed_set_function() -> Set: # type: ignore[type-arg] # pragma: no cover
return set()
def _untyped_function(something):
...
class _customAction(argparse.Action): # pragma: no cover
def __init__(
self,
option_strings,
dest,
const=None,
default=None,
required=False,
help=None, # noqa: A002
metavar=None,
):
super().__init__(
option_strings=option_strings,
dest=dest,
nargs=0,
const=const,
default=default,
required=required,
help=help,
)
def __call__(self, parser, namespace, values, option_string=None): # pragma: no cover
setattr(namespace, self.dest, 'Something')
types = (
(0, settngs.Setting('-t', '--test'), str, True),
(1, settngs.Setting('-t', '--test', cmdline=False), 'Any', True),
(2, settngs.Setting('-t', '--test', default=1, file=True, cmdline=False), int, False),
(3, settngs.Setting('-t', '--test', default='test'), str, False),
(4, settngs.Setting('-t', '--test', default='test', file=True, cmdline=False), str, False),
(5, settngs.Setting('-t', '--test', action='count'), int, True),
(6, settngs.Setting('-t', '--test', action='append'), List[str], True),
(7, settngs.Setting('-t', '--test', action='extend'), List[str], True),
(8, settngs.Setting('-t', '--test', nargs='+'), List[str], True),
(9, settngs.Setting('-t', '--test', action='store_const', const=1), int, True),
(10, settngs.Setting('-t', '--test', action='append_const', const=1), List[int], True),
(11, settngs.Setting('-t', '--test', action='store_true'), bool, False),
(12, settngs.Setting('-t', '--test', action='store_false'), bool, False),
(13, settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool, True),
(14, settngs.Setting('-t', '--test', action=_customAction), 'Any', True),
(15, settngs.Setting('-t', '--test', action='help'), None, True),
(16, settngs.Setting('-t', '--test', action='version'), None, True),
(17, settngs.Setting('-t', '--test', type=int), int, True),
(18, settngs.Setting('-t', '--test', type=int, nargs='+'), List[int], True),
(19, settngs.Setting('-t', '--test', type=_typed_function), test_type, True),
(20, settngs.Setting('-t', '--test', type=_untyped_function, default=1), int, False),
(21, settngs.Setting('-t', '--test', type=_untyped_function, default=[1]), List[int], False),
(22, settngs.Setting('-t', '--test', type=_untyped_function), 'Any', True),
(23, settngs.Setting('-t', '--test', type=_untyped_function, default={1}), Set[int], False),
(24, settngs.Setting('-t', '--test', action='append', type=int), List[int], True),
(25, settngs.Setting('-t', '--test', action='extend', type=int, nargs=2), List[int], True),
(26, settngs.Setting('-t', '--test', action='append', type=int, nargs=2), List[List[int]], True),
(27, settngs.Setting('-t', '--test', action='extend', nargs='+'), List[str], True),
(28, settngs.Setting('-t', '--test', action='extend', type=_typed_list_generic_function), List[test_type], True),
(29, settngs.Setting('-t', '--test', action='extend', type=_typed_list_function), List, True),
(30, settngs.Setting('-t', '--test', action='extend', type=_typed_set_function), Set, True),
)
@pytest.mark.parametrize('num,setting,typ,noneable_expected', types)
def test_guess_type(num, setting, typ, noneable_expected):
x = setting._guess_type()
guessed_type, noneable = x
assert guessed_type == typ
assert noneable == noneable_expected
expected_src = '''from __future__ import annotations
import settngs
{extra_imports}
class SettngsNS(settngs.TypedNS):
test__test: {typ}
'''
no_type_expected_src = '''from __future__ import annotations
import settngs
class SettngsNS(settngs.TypedNS):
...
'''
settings = (
(0, lambda parser: parser.add_setting('-t', '--test'), expected_src.format(extra_imports='', typ='str | None')),
(1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), expected_src.format(extra_imports='', typ='int')),
(3, lambda parser: parser.add_setting('-t', '--test', default='test'), expected_src.format(extra_imports='', typ='str')),
(4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), expected_src.format(extra_imports='', typ='str')),
(5, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src.format(extra_imports='', typ='int | None')),
(6, lambda parser: parser.add_setting('-t', '--test', action='append'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')),
(7, lambda parser: parser.add_setting('-t', '--test', action='extend'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')),
(8, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src.format(extra_imports='', typ='int | None')),
(10, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')),
(11, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src.format(extra_imports='', typ='bool')),
(12, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src.format(extra_imports='', typ='bool')),
(13, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src.format(extra_imports='', typ='bool | None')),
(14, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(15, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src),
(16, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src),
(17, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src.format(extra_imports='', typ='int | None')),
(18, lambda parser: parser.add_setting('-t', '--test', type=int, nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')),
(19, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), expected_src.format(extra_imports='import tests.settngs_test\n', typ='tests.settngs_test.test_type | None')),
(20, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src.format(extra_imports='', typ='int')),
(21, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=[1]), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]}')),
(22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(23, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default={1}), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{Set[int]}')),
(24, lambda parser: parser.add_setting('-t', '--test', action='append', type=int), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')),
(25, lambda parser: parser.add_setting('-t', '--test', action='extend', type=int, nargs=2), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')),
(26, lambda parser: parser.add_setting('-t', '--test', action='append', type=int, nargs=2), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[List[int]]} | None')),
(27, lambda parser: parser.add_setting('-t', '--test', action='extend', nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')),
(28, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_generic_function), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[test_type]} | None')),
(29, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_function), expected_src.format(extra_imports='', typ=f'{settngs._type_to_string(List)[0]} | None')),
(30, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_set_function), expected_src.format(extra_imports='', typ=f'{settngs._type_to_string(Set)[0]} | None')),
)
@pytest.mark.parametrize('num,set_options,expected', settings)
def test_generate_ns(settngs_manager, num, set_options, expected):
settngs_manager.add_group('test', set_options)
imports, types = settngs_manager.generate_ns()
generated_src = '\n\n\n'.join((imports, types))
assert generated_src == expected
ast.parse(generated_src)
expected_src_dict = '''from __future__ import annotations
import typing
{extra_imports}
class test(typing.TypedDict):
test: {typ}
class SettngsDict(typing.TypedDict):
test: test
'''
no_type_expected_src_dict = '''from __future__ import annotations
import typing
class test(typing.TypedDict):
...
class SettngsDict(typing.TypedDict):
test: test
'''
settings_dict = (
(0, lambda parser: parser.add_setting('-t', '--test'), expected_src_dict.format(extra_imports='', typ='str | None')),
(1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), expected_src_dict.format(extra_imports='', typ='int')),
(3, lambda parser: parser.add_setting('-t', '--test', default='test'), expected_src_dict.format(extra_imports='', typ='str')),
(4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), expected_src_dict.format(extra_imports='', typ='str')),
(5, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src_dict.format(extra_imports='', typ='int | None')),
(6, lambda parser: parser.add_setting('-t', '--test', action='append'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(7, lambda parser: parser.add_setting('-t', '--test', action='extend'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(8, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src_dict.format(extra_imports='', typ='int | None')),
(10, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(11, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src_dict.format(extra_imports='', typ='bool')),
(12, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src_dict.format(extra_imports='', typ='bool')),
(13, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src_dict.format(extra_imports='', typ='bool | None')),
(14, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(15, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src_dict),
(16, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src_dict),
(17, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src_dict.format(extra_imports='', typ='int | None')),
(18, lambda parser: parser.add_setting('-t', '--test', type=int, nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(19, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), expected_src_dict.format(extra_imports='import tests.settngs_test\n', typ=f'{test_type.__module__}.{test_type.__name__} | None')),
(20, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src_dict.format(extra_imports='', typ='int')),
(21, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=[1]), expected_src_dict.format(extra_imports='', typ=f'{List[int]}')),
(22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(23, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default={1}), expected_src_dict.format(extra_imports='', typ=f'{Set[int]}')),
(24, lambda parser: parser.add_setting('-t', '--test', action='append', type=int), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(25, lambda parser: parser.add_setting('-t', '--test', action='extend', type=int, nargs=2), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(26, lambda parser: parser.add_setting('-t', '--test', action='append', type=int, nargs=2), expected_src_dict.format(extra_imports='', typ=f'{List[List[int]]} | None')),
(27, lambda parser: parser.add_setting('-t', '--test', action='extend', nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(28, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_generic_function), expected_src_dict.format(extra_imports='', typ=f'{List[test_type]} | None')),
(29, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_function), expected_src_dict.format(extra_imports='', typ=f'{settngs._type_to_string(List)[0]} | None')),
(30, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_set_function), expected_src_dict.format(extra_imports='', typ=f'{settngs._type_to_string(Set)[0]} | None')),
)
@pytest.mark.parametrize('num,set_options,expected', settings_dict)
def test_generate_dict(settngs_manager, num, set_options, expected):
settngs_manager.add_group('test', set_options)
imports, types = settngs_manager.generate_dict()
generated_src = '\n\n\n'.join((imports, types))
assert generated_src == expected
ast.parse(generated_src)
def test_example(capsys, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
settings_file = tmp_path / 'settings.json'
@ -358,7 +849,9 @@ def test_example(capsys, tmp_path, monkeypatch):
for args, expected_out, expected_file in example:
if args == ['manual settings.json']:
settings_file.unlink()
settings_file.write_text('{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n')
settings_file.write_text('{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n')
i += 1
continue
else:
settngs._main(args)
captured = capsys.readouterr()

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