Skip to content

Commit

Permalink
fix Parser do not examine PEP 526 __annotations__
Browse files Browse the repository at this point in the history
Signed-off-by: Zhiyuan Chen <[email protected]>
  • Loading branch information
ZhiyuanChen committed May 27, 2024
1 parent c7b9fb6 commit 5bf8144
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 14 deletions.
46 changes: 32 additions & 14 deletions chanfig/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
from ast import literal_eval
from collections.abc import Sequence
from contextlib import suppress
from typing import TYPE_CHECKING, Any
from dataclasses import Field
from inspect import isclass
from types import NoneType, UnionType
from typing import TYPE_CHECKING, Any, _UnionGenericAlias, get_args # type: ignore[attr-defined]
from warnings import warn

from .nested_dict import NestedDict
from .utils import Null, parse_bool
from .utils import Null, get_annotations, parse_bool
from .variable import Variable

if TYPE_CHECKING:
Expand Down Expand Up @@ -277,23 +280,38 @@ def parse_args( # type: ignore[override]
parsed[key] = value
return parsed

def add_config_arguments(self, config):
def add_config_arguments(self, config: Config):
for key, dtype in get_annotations(config).items():
self.add_config_argument(key, dtype=dtype)
for key, value in config.all_items():
self.add_config_argument(key, value)

def add_config_argument(self, key, value: Any | None = None, dtype: type | None = None):
if dtype is None:
if isinstance(value, Variable):
dtype = value._type or value.dtype # pylint: disable=W0212
elif isinstance(value, Field):
dtype = value.type
elif value is not None:
dtype = type(value)
else:
dtype = None
name = "--" + key
if name not in self:
help = value._help if isinstance(value, Variable) else None # pylint: disable=W0212,W0622
if isinstance(value, (list, tuple, dict, set)):
self.add_argument(name, type=dtype, nargs="+", help=help, dest=key)
elif isinstance(value, bool):
self.add_argument(name, type=parse_bool, help=help, dest=key)
else:
self.add_argument(name, type=dtype, help=help, dest=key)
if isinstance(dtype, (UnionType, _UnionGenericAlias)):
args = get_args(dtype)
if len(args) == 2 and NoneType in args:
dtype = args[0] if args[0] is not NoneType else args[1]
name = "--" + key
if name not in self:
help = None # pylint: disable=W0622
if isinstance(value, Variable):
help = value._help # pylint: disable=W0212
elif isinstance(value, Field):
help = value.metadata.get("help")
if dtype is None or not isclass(dtype):
return self.add_argument(name, help=help, dest=key)
if issubclass(dtype, (list, tuple, dict, set)):
return self.add_argument(name, type=dtype, nargs="+", help=help, dest=key)
if issubclass(dtype, bool):
return self.add_argument(name, type=parse_bool, help=help, dest=key)
return self.add_argument(name, type=dtype, help=help, dest=key)

def merge_default_config(self, parsed, default_config: str, no_default_config_action: str = "raise") -> NestedDict:
message = f"default_config is set to {default_config}, but not found in args."
Expand Down
88 changes: 88 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# CHANfiG, Easier Configuration.
# Copyright (c) 2022-Present, CHANfiG Contributors

# This program is free software: you can redistribute it and/or modify
# it under the terms of the following licenses:
# - The Unlicense
# - GNU Affero General Public License v3.0 or later
# - GNU General Public License v2.0 or later
# - BSD 4-Clause "Original" or "Old" License
# - MIT License
# - Apache License 2.0

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the LICENSE file for more details.

from __future__ import annotations

from typing import List, Optional

from chanfig import Config


class TestConfig(Config):
__test__ = False

t: bool
true: bool
y: bool
yes: Optional[bool]
f: bool
false: bool
n: bool
no: bool | None
not_recognized: List[bool]


class Test:

def test_parse_bool(self):
config = TestConfig()
config.parse(
[
"--t",
"t",
"--true",
"true",
"--y",
"y",
"--yes",
"yes",
"--f",
"f",
"--false",
"false",
"--n",
"n",
"--no",
"no",
]
)
assert config.t and config.true and config.y and config.yes
assert not config.f and not config.false and not config.n and not config.no

config = TestConfig()
config.parse(
[
"--t",
"T",
"--true",
"True",
"--y",
"Y",
"--yes",
"Yes",
"--f",
"F",
"--false",
"False",
"--n",
"N",
"--no",
"No",
]
)
assert config.t and config.true and config.y and config.yes
assert not config.f and not config.false and not config.n and not config.no

0 comments on commit 5bf8144

Please sign in to comment.