Skip to content

Commit

Permalink
Various improvements: dynamic dataclasses, helptext, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
brentyi committed Oct 13, 2021
1 parent e50d9ac commit 8bf2239
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 44 deletions.
21 changes: 11 additions & 10 deletions dcargs/_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ArgumentDefinition:
name: str
field: dataclasses.Field
parent_class: Type
type: Optional[Type[Any]]
type: Optional[Union[Type, TypeVar]]

# Fields that will be handled by argument transformations
required: Optional[bool] = None
Expand Down Expand Up @@ -64,15 +64,16 @@ def make_from_field(
role: _construction.FieldRole = _construction.FieldRole.VANILLA_FIELD

def _handle_generics(arg: ArgumentDefinition) -> _ArgumentTransformOutput:
return (
dataclasses.replace(
arg,
type=type_from_typevar[arg.type] # type:ignore
if arg.type in type_from_typevar
else arg.type,
),
None,
)
if isinstance(arg.type, TypeVar):
assert arg.type in type_from_typevar, "TypeVar not bounded"
return (
dataclasses.replace(
arg, type=type_from_typevar[arg.type] # type:ignore
),
None,
)
else:
return arg, None

while True:
for transform in [_handle_generics] + _argument_transforms: # type: ignore
Expand Down
10 changes: 8 additions & 2 deletions dcargs/_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ def get_field_docstring(cls: Type, field_name: str) -> Optional[str]:
if isinstance(cls, _GenericAlias):
cls = cls.__origin__

assert dataclasses.is_dataclass(cls)
if cls not in _cached_tokenization:
_cached_tokenization[cls] = _Tokenization.make(cls)
try:
_cached_tokenization[cls] = _Tokenization.make(cls)
except OSError as e:
# Dynamic dataclasses
assert "could not find class definition" in e.args[0]
return None

tokens = _cached_tokenization[cls].tokens
tokens_from_line = _cached_tokenization[cls].tokens_from_line

Expand Down Expand Up @@ -87,7 +94,6 @@ def get_field_docstring(cls: Type, field_name: str) -> Optional[str]:
return comment[1:].strip()

# Check for comment on the line before the field
# TODO: this may result in unintentional helptext?
comment_index = field_index
comments: List[str] = []
while True:
Expand Down
14 changes: 5 additions & 9 deletions dcargs/_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,16 @@ class ParserDefinition:
def apply(self, parser: argparse.ArgumentParser) -> None:
"""Create defined arguments and subparsers."""

groups: Dict[str, Union[argparse.ArgumentParser, argparse._ArgumentGroup]] = {
"optional arguments": parser,
}
# Put required group at start of group list
required_group = parser.add_argument_group("required arguments")
parser._action_groups = parser._action_groups[::-1]

# Add each argument
for arg in self.args:
if arg.required:
group = "required arguments"
arg.add_argument(required_group)
else:
group = "optional arguments"

if group not in groups:
groups[group] = parser.add_argument_group(group)
arg.add_argument(groups[group])
arg.add_argument(parser)

# Add subparsers
if self.subparsers is not None:
Expand Down
23 changes: 0 additions & 23 deletions tests/test_dcargs.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import dataclasses
import enum
import io
import pathlib
from contextlib import redirect_stdout
from typing import ClassVar, List, Optional, Sequence, Tuple, Union

import pytest
Expand Down Expand Up @@ -385,24 +383,3 @@ class OptionalSubparser:
dcargs.parse(OptionalSubparser, args=["--x", "1", "B", "--z", "3"])
with pytest.raises(SystemExit):
dcargs.parse(OptionalSubparser, args=["--x", "1", "C", "--y", "3"])


def test_helptext():
@dataclasses.dataclass
class Helptext:
x: int # Documentation 1

# Documentation 2
y: int

z: int = 3
"""Documentation 3"""

f = io.StringIO()
with pytest.raises(SystemExit):
with redirect_stdout(f):
dcargs.parse(Helptext, args=["--help"])
helptext = f.getvalue()
assert "--x INT Documentation 1\n" in helptext
assert "--y INT Documentation 2\n" in helptext
assert "--z INT Documentation 3 (default: 3)\n" in helptext
57 changes: 57 additions & 0 deletions tests/test_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import contextlib
import dataclasses
import io

import pytest

import dcargs


def test_helptext():
@dataclasses.dataclass
class Helptext:
x: int # Documentation 1

# Documentation 2
y: int

z: int = 3
"""Documentation 3"""

f = io.StringIO()
with pytest.raises(SystemExit):
with contextlib.redirect_stdout(f):
dcargs.parse(Helptext, args=["--help"])
helptext = f.getvalue()
assert "--x INT Documentation 1\n" in helptext
assert "--y INT Documentation 2\n" in helptext
assert "--z INT Documentation 3 (default: 3)\n" in helptext


def test_multiline_helptext():
@dataclasses.dataclass
class HelptextMultiline:
x: int # Documentation 1

# Documentation 2
# Next line of documentation 2
y: int

z: int = 3
"""Documentation 3
Next line of documentation 3"""

f = io.StringIO()
with pytest.raises(SystemExit):
with contextlib.redirect_stdout(f):
dcargs.parse(HelptextMultiline, args=["--help"])
helptext = f.getvalue()
assert " --x INT Documentation 1\n" in helptext
assert (
" --y INT Documentation 2\n Next line of documentation 2\n"
in helptext
)
assert (
" --z INT Documentation 3\n Next line of documentation 3 (default: 3)\n"
in helptext
)
14 changes: 14 additions & 0 deletions tests/test_dynamic_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from dataclasses import field, make_dataclass

import pytest

import dcargs


def test_dynamic():
B = make_dataclass("B", [("c", int, field())])
A = make_dataclass("A", [("b", B, field())])

with pytest.raises(SystemExit):
dcargs.parse(A, args=[])
assert dcargs.parse(A, args=["--b.c", "5"]) == A(b=B(c=5))

0 comments on commit 8bf2239

Please sign in to comment.