Skip to content

Commit

Permalink
refactor: merge into single module parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
jnoortheen committed Nov 3, 2020
1 parent 08f9080 commit 73e4f08
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 113 deletions.
3 changes: 2 additions & 1 deletion arger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from pkg_resources import DistributionNotFound, get_distribution

from arger.funcs import Argument, Option

from .main import Arger
from .parser.funcs import Argument, Option

try:
__version__ = get_distribution("arger").version
Expand Down
81 changes: 37 additions & 44 deletions arger/parser/docstring.py → arger/docstring.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
import functools
import re
from typing import Any, Dict, List, NamedTuple, Optional, Pattern, Tuple
import typing as tp


class ParamDocTp(NamedTuple):
type_hint: Any
flags: List[str]
class ParamDocTp(tp.NamedTuple):
type_hint: tp.Any
flags: tp.List[str]
doc: str

@classmethod
def init(cls, type_hint: tp.Any, doc: str, flag_symbol='-'):
"""Parse flags defined in param's doc
class _ParamDoc(NamedTuple):
name: str
type_hint: Any
doc: List[str] # lines
Examples:
''':param arg1: -a --arg this is the document'''
"""
doc_parts: tp.List[str] = []
flags: tp.List[str] = []
for part in doc.split():
if part.startswith(flag_symbol) and not doc_parts:
# strip both comma and empty space
flags.append(part.strip(", ").strip())
else:
doc_parts.append(part)

return cls(type_hint, flags=flags, doc=" ".join(doc_parts))

class DocstringTp(NamedTuple):

class DocstringTp(tp.NamedTuple):
description: str
epilog: str
params: Dict[str, ParamDocTp]


def get_flags_from_param_doc(doc: str, flag_symbol='-') -> Tuple[List[str], str]:
"""Parse flags defined in param's doc
Examples:
''':param arg1: -a --arg this is the document'''
"""
doc_parts: List[str] = []
flags: List[str] = []
for part in doc.split():
if part.startswith(flag_symbol) and not doc_parts:
# strip both comma and empty space
flags.append(part.strip(", ").strip())
else:
doc_parts.append(part)
return flags, " ".join(doc_parts)
params: tp.Dict[str, ParamDocTp]


class DocstringParser:
"""Abstract class"""

pattern: Pattern
section_ptrn: Pattern
param_ptrn: Pattern
pattern: tp.Pattern
section_ptrn: tp.Pattern
param_ptrn: tp.Pattern

def parse(self, doc: str) -> DocstringTp:
raise NotImplementedError
Expand All @@ -64,29 +59,27 @@ def __init__(self):
r'^(?P<param>\w+)[ \t]*:[ \t]*(?P<type>\w+)?'
) # matches parameter_name e.g. param1: or param2 (int):

def get_rest_of_section(self, params: str) -> Tuple[str, str]:
def get_rest_of_section(self, params: str) -> tp.Tuple[str, str]:
other_sect = self.section_ptrn.search(params)
if other_sect:
pos = other_sect.start()
return params[pos:].strip(), params[:pos]
return '', params

def parse_params(self, params: str) -> Dict[str, ParamDocTp]:
docs: List[_ParamDoc] = []
def parse_params(self, params: str) -> tp.Dict[str, ParamDocTp]:
docs = []
for line in params.splitlines():
match = self.param_ptrn.search(line)
if match:
result = match.groupdict()
doc = [result['doc']] if 'doc' in result else []
docs.append(_ParamDoc(result['param'], result['type'], doc))
doc = result.get('doc', '')
docs.append((result['param'], result['type'], doc))
elif docs:
docs[-1].doc.append(line)
docs[-1][-1] += line

return {
p.name.strip('*'): ParamDocTp(
p.type_hint, *get_flags_from_param_doc(' '.join(p.doc))
)
for p in docs
param.strip('*'): ParamDocTp.init(tphint, doc)
for param, tphint, doc in docs
}

def parse(self, doc: str) -> DocstringTp:
Expand Down Expand Up @@ -118,7 +111,7 @@ def __init__(self):
self.section_ptrn = re.compile(r'\n:[\w]+') # matches any start of the section
self.param_ptrn = re.compile(r'^[ ]+(?P<tp_param>.+):[ ]*(?P<doc>[\s\S]+)')

def parse_doc(self, line: str, params: Dict[str, ParamDocTp]):
def parse_doc(self, line: str, params: tp.Dict[str, ParamDocTp]):
match = self.param_ptrn.match(line)
if match:
tp_param, doc = match.groups() # type: str, str
Expand All @@ -127,13 +120,13 @@ def parse_doc(self, line: str, params: Dict[str, ParamDocTp]):
type_hint = None
if len(parts) > 1:
type_hint = parts[0].strip()
params[param] = ParamDocTp(type_hint, *get_flags_from_param_doc(doc))
params[param] = ParamDocTp.init(type_hint, doc)

def parse(self, doc: str) -> DocstringTp:
lines = self.pattern.split(doc)
long_desc = lines.pop(0)
epilog = ''
params: Dict[str, ParamDocTp] = {}
params: tp.Dict[str, ParamDocTp] = {}
for idx, lin in enumerate(lines):
sections = self.section_ptrn.split(lin, maxsplit=1)
if idx + 1 == len(lines) and len(sections) > 1:
Expand All @@ -149,7 +142,7 @@ def get_parsers():
return [NumpyDocParser(), GoogleDocParser(), RstDocParser()]


def parse_docstring(doc: Optional[str]) -> DocstringTp:
def parse_docstring(doc: tp.Optional[str]) -> DocstringTp:
if doc:
for parser in get_parsers():
if parser.matches(doc):
Expand Down
73 changes: 58 additions & 15 deletions arger/parser/funcs.py → arger/funcs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import argparse
import inspect
import typing as tp
from argparse import Action, ArgumentParser
from collections import OrderedDict
from itertools import filterfalse, tee
from typing import Any, Tuple, Union

from arger.parser.docstring import parse_docstring

from ..typing_utils import UNDEFINED, T, VarArg, VarKw
from .actions import TypeAction
from arger import typing_utils as tp_utils
from arger.docstring import parse_docstring


class Param(tp.NamedTuple):
Expand All @@ -18,8 +18,8 @@ class Param(tp.NamedTuple):


def partition(
pred, iterable: tp.Iterable[T]
) -> tp.Tuple[tp.Iterable[T], tp.Iterable[T]]:
pred, iterable: tp.Iterable[tp_utils.T]
) -> tp.Tuple[tp.Iterable[tp_utils.T], tp.Iterable[tp_utils.T]]:
"""Use a predicate to partition entries into false entries and true entries"""
# partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9
t1, t2 = tee(iterable)
Expand All @@ -40,11 +40,11 @@ def prepare_params(func):
)

def get_param(param: inspect.Parameter) -> Param:
annot = get_val(param.annotation, UNDEFINED)
annot = get_val(param.annotation, tp_utils.UNDEFINED)
if param.kind == inspect.Parameter.VAR_POSITIONAL:
annot = VarArg(annot)
annot = tp_utils.VarArg(annot)
elif param.kind == inspect.Parameter.VAR_KEYWORD:
annot = VarKw(annot)
annot = tp_utils.VarKw(annot)

name = param.name
doc = docstr.params.get(name)
Expand Down Expand Up @@ -117,7 +117,7 @@ def set_dest(self, name: str, typ: tp.Any):

def update_type(self, typ: tp.Any):
"""Update type externally."""
if 'type' not in self.kwargs and typ is not UNDEFINED:
if 'type' not in self.kwargs and typ is not tp_utils.UNDEFINED:
self.kwargs['type'] = typ


Expand Down Expand Up @@ -149,25 +149,25 @@ def set_flags(
self,
name: str,
typ: tp.Any,
default: tp.Any = UNDEFINED,
default: tp.Any = tp_utils.UNDEFINED,
option_generator: tp.Optional[FlagsGenerator] = None,
):
self.kwargs.setdefault('dest', name)
if not self.flags and option_generator is not None:
self.flags = tuple(option_generator.generate(name))
self.update_default(typ, default)

def update_default(self, typ: tp.Any, default: tp.Any = UNDEFINED):
def update_default(self, typ: tp.Any, default: tp.Any = tp_utils.UNDEFINED):
"""Update type and default externally"""
if default is not UNDEFINED and 'default' not in self.kwargs:
if default is not tp_utils.UNDEFINED and 'default' not in self.kwargs:
self.kwargs["default"] = default
else:
default = self.kwargs["default"]

if isinstance(default, bool):
self.kwargs['action'] = "store_true" if default is False else "store_false"
typ = self.kwargs.pop('type', UNDEFINED)
elif default is not None and typ is UNDEFINED:
typ = self.kwargs.pop('type', tp_utils.UNDEFINED)
elif default is not None and typ is tp_utils.UNDEFINED:
typ = type(default)

self.update_type(typ)
Expand Down Expand Up @@ -218,3 +218,46 @@ def parse_function(func: tp.Optional[tp.Callable]) -> ParsedFunc:
arguments[param.name] = create_option(param, default, option_generator)

return ParsedFunc(arguments, func, docstr.description, docstr.epilog)


def get_nargs(typ: Any) -> Tuple[Any, Union[int, str]]:
inner = tp_utils.unpack_type(typ)
if (
tp_utils.is_tuple(typ)
and typ != tuple
and not isinstance(typ, (tp_utils.VarKw, tp_utils.VarArg))
and tp_utils.get_inner_args(typ)
):
args = tp_utils.get_inner_args(typ)
inner = inner if len(set(args)) == 1 else str
return inner, '+' if (... in args) else len(args)
return inner, "*"


class TypeAction(argparse.Action):
"""After the parse update the type of value"""

def __init__(self, *args, **kwargs):
typ = kwargs.pop("type", tp_utils.UNDEFINED)
self.orig_type = typ
if typ is not tp_utils.UNDEFINED:
origin = tp_utils.get_origin(typ)
if tp_utils.is_iterable(origin):
origin, kwargs["nargs"] = get_nargs(typ)

if tp_utils.is_enum(origin):
kwargs.setdefault("choices", [e.name for e in origin])
origin = str

kwargs["type"] = origin
super().__init__(*args, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
if tp_utils.is_iterable(self.orig_type):
items = getattr(namespace, self.dest, ()) or ()
items = list(items)
items.extend(values)
vals = items
else:
vals = values
setattr(namespace, self.dest, tp_utils.cast(self.orig_type, vals))
3 changes: 2 additions & 1 deletion arger/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import sys
import typing as tp

from .parser.funcs import ParsedFunc, parse_function
from arger.funcs import ParsedFunc, parse_function

from .typing_utils import VarArg

CMD_TITLE = "commands"
Expand Down
1 change: 0 additions & 1 deletion arger/parser/__init__.py

This file was deleted.

47 changes: 0 additions & 47 deletions arger/parser/actions.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

from arger import Arger, Argument
from arger.parser.funcs import Param, create_argument
from arger.funcs import Param, create_argument
from arger.typing_utils import VarArg


Expand Down
2 changes: 1 addition & 1 deletion tests/test_docstrings_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from arger.parser.docstring import ParamDocTp, parse_docstring
from arger.docstring import ParamDocTp, parse_docstring


def func_numpy():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from arger import Arger, Option
from arger.parser.funcs import FlagsGenerator, Param, create_option
from arger.funcs import FlagsGenerator, Param, create_option
from arger.typing_utils import UNDEFINED


Expand Down
2 changes: 1 addition & 1 deletion tests/test_parser_funcs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from arger.funcs import TypeAction
from arger.parser import parse_function
from arger.parser.actions import TypeAction
from arger.typing_utils import VarArg

from .utils import _reprint
Expand Down

0 comments on commit 73e4f08

Please sign in to comment.