-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Fredrik Borg
committed
Dec 12, 2022
1 parent
c68a02f
commit f0854b9
Showing
7 changed files
with
245 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
from .config import handle_args # noqa: F401 | ||
from .schema import parse as parse # noqa: F401 | ||
from .xdg import get_cache_dir, get_config_dir # noqa: F401 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
#!/usr/bin/env python | ||
|
||
|
||
import argparse | ||
from typing import Dict, List, Optional, Type, TypeVar | ||
|
||
from pydantic import BaseModel | ||
|
||
import caep | ||
|
||
# Map of pydantic schema types to python types | ||
TYPE_MAPPING: Dict[str, type] = { | ||
"string": str, | ||
"integer": int, | ||
"number": float, | ||
"boolean": bool, | ||
} | ||
|
||
# Type of BaseModel Subclasses | ||
BaseModelType = TypeVar("BaseModelType", bound=BaseModel) | ||
|
||
|
||
class SchemaError(Exception): | ||
pass | ||
|
||
|
||
class FieldError(Exception): | ||
pass | ||
|
||
|
||
def parse( | ||
model: Type[BaseModelType], | ||
description: str, | ||
config_id: str, | ||
config_file_name: str, | ||
section_name: str, | ||
alias: bool = False, | ||
opts: Optional[List[str]] = None, | ||
) -> BaseModelType: | ||
|
||
""" | ||
TODO - document parameters | ||
""" | ||
|
||
parser = argparse.ArgumentParser(description) | ||
|
||
# Get all pydantic fields | ||
fields = model.schema(alias).get("properties") | ||
|
||
if not fields: | ||
raise SchemaError(f"Unable to get properties from schema {model}") | ||
|
||
# Map of all fields that are defined as arrays | ||
arrays = {} | ||
|
||
# Loop over all pydantic schema fields | ||
for field, schema in fields.items(): | ||
field_type: type | ||
|
||
# argparse arguments use dash instead of underscore | ||
field = field.replace("_", "-") | ||
|
||
if schema["type"] == "array": | ||
array_type = TYPE_MAPPING.get(schema["items"]["type"]) | ||
|
||
if not array_type: | ||
raise FieldError( | ||
f"Unsupported pydantic type for array field {field}: {schema}" | ||
) | ||
|
||
arrays[field] = (array_type, schema.get("split", " ")) | ||
|
||
# For arrays (lists), we parse as str in caep and split values by configured | ||
# split value later | ||
field_type = str | ||
else: | ||
|
||
if schema["type"] not in TYPE_MAPPING: | ||
raise FieldError( | ||
f"Unsupported pydantic type for field {field}: {schema}" | ||
) | ||
|
||
field_type = TYPE_MAPPING[schema["type"]] | ||
|
||
parser.add_argument( | ||
f"--{field}", | ||
help=schema.get("description", "No help provided"), | ||
type=field_type, | ||
default=schema.get("default"), | ||
) | ||
|
||
args = {} | ||
|
||
for field, value in vars( | ||
caep.config.handle_args( | ||
parser, config_id, config_file_name, section_name, opts=opts | ||
) | ||
).items(): | ||
|
||
if field in arrays: | ||
if not value: | ||
value = [] | ||
else: | ||
value_type, split = arrays[field] | ||
value = [value_type(v) for v in value.split(split)] | ||
|
||
args[field] = value | ||
|
||
return model(**args) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,5 +2,5 @@ | |
number = 3 | ||
|
||
[test] | ||
bool = True | ||
enabled = True | ||
str-arg = from ini |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
""" test config """ | ||
import os | ||
from typing import List, Optional | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
import caep | ||
|
||
INI_TEST_FILE = os.path.join(os.path.dirname(__file__), "data/config_testdata.ini") | ||
|
||
|
||
class Arguments(BaseModel): | ||
|
||
# Fields that are not specified as `Field` with description will not have | ||
# any help text | ||
|
||
str_arg: str = Field(description="Required String Argument") | ||
number: int = Field(default=1, description="Integer with default value") | ||
enabled: bool = Field(default=False, description="Boolean with default value") | ||
|
||
# List fields will be separated by space as default | ||
intlist: List[int] = Field(description="Space separated list of ints") | ||
|
||
# Can optionally use "split" argument to use another value to split based on | ||
strlist: List[str] = Field(description="Comma separated list of strings", split=",") | ||
|
||
|
||
def parse_args( | ||
commandline: Optional[List[str]] = None, | ||
description: str = "Program description", | ||
config_id: str = "config_id", | ||
config_filename: str = "config_filename", | ||
section_name: str = "test", | ||
) -> Arguments: | ||
return caep.parse( | ||
Arguments, | ||
description, | ||
config_id, | ||
config_filename, | ||
section_name, | ||
opts=commandline, | ||
) | ||
|
||
|
||
def test_schema_commandline() -> None: | ||
"""all arguments from command line, using default for number and bool""" | ||
commandline = "--str-arg test".split() | ||
|
||
config = parse_args(commandline) | ||
|
||
assert config.number == 1 | ||
assert config.str_arg == "test" | ||
assert not config.enabled | ||
|
||
|
||
def test_schema_ini() -> None: | ||
"""all arguments from ini file""" | ||
commandline = f"--config {INI_TEST_FILE}".split() | ||
|
||
config = parse_args(commandline, section_name="test") | ||
|
||
assert config.number == 3 | ||
assert config.str_arg == "from ini" | ||
assert config.enabled is True | ||
|
||
|
||
def test_argparse_env() -> None: | ||
"""all arguments from env""" | ||
|
||
env = { | ||
"STR_ARG": "from env", | ||
"NUMBER": 4, | ||
"ENABLED": "yes", # accepts both yes and true | ||
} | ||
|
||
for key, value in env.items(): | ||
os.environ[key] = str(value) | ||
|
||
config = parse_args(section_name="test") | ||
|
||
assert config.number == 4 | ||
assert config.str_arg == "from env" | ||
assert config.enabled is True | ||
|
||
# Remove from environment variables | ||
for key in env: | ||
del os.environ[key] | ||
|
||
|
||
def test_argparse_env_ini() -> None: | ||
""" | ||
--number from environment | ||
--bool from ini | ||
--str-arg from cmdline | ||
""" | ||
env = { | ||
"NUMBER": 4, | ||
} | ||
|
||
for key, value in env.items(): | ||
os.environ[key] = str(value) | ||
|
||
commandline = f"--config {INI_TEST_FILE} --str-arg cmdline".split() | ||
|
||
config = parse_args(commandline, section_name="test") | ||
|
||
assert config.number == 4 | ||
assert config.str_arg == "cmdline" | ||
assert config.enabled is True | ||
|
||
# Remove from environment variables | ||
for key in env: | ||
del os.environ[key] |