Skip to content

Commit 2965b2d

Browse files
committed
factor out functions for parsers for built-in collections
1 parent 111e15b commit 2965b2d

File tree

2 files changed

+186
-109
lines changed

2 files changed

+186
-109
lines changed

fgpyo/util/inspect.py

+158-109
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import List
1212
from typing import Literal
1313
from typing import Optional
14+
from typing import Set
1415
from typing import Tuple
1516
from typing import Type
1617
from typing import Union
@@ -70,6 +71,159 @@ def split_at_given_level(
7071
NoneType = type(None)
7172

7273

74+
def list_parser(
75+
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
76+
) -> Callable[[str], List[Any]]:
77+
"""
78+
Returns a function that parses a stringified list into a `List` of the correct type.
79+
80+
Args:
81+
cls: the type of the class object this is being parsed for (used to get default val for
82+
parsers)
83+
type_: the type of the attribute to be parsed
84+
parsers: an optional mapping from type to the function to use for parsing that type (allows
85+
for parsing of more complex types)
86+
"""
87+
subtypes = typing.get_args(type_)
88+
assert len(subtypes) == 1, "Lists are allowed only one subtype per PEP specification!"
89+
subtype_parser = _get_parser(
90+
cls,
91+
subtypes[0],
92+
parsers,
93+
)
94+
return functools.partial(
95+
lambda s: list(
96+
[]
97+
if s == ""
98+
else [subtype_parser(item) for item in list(split_at_given_level(s, split_delim=","))]
99+
)
100+
)
101+
102+
103+
def set_parser(
104+
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
105+
) -> Callable[[str], Set[Any]]:
106+
"""
107+
Returns a function that parses a stringified set into a `Set` of the correct type.
108+
109+
Args:
110+
cls: the type of the class object this is being parsed for (used to get default val for
111+
parsers)
112+
type_: the type of the attribute to be parsed
113+
parsers: an optional mapping from type to the function to use for parsing that type (allows
114+
for parsing of more complex types)
115+
"""
116+
subtypes = typing.get_args(type_)
117+
assert len(subtypes) == 1, "Sets are allowed only one subtype per PEP specification!"
118+
subtype_parser = _get_parser(
119+
cls,
120+
subtypes[0],
121+
parsers,
122+
)
123+
return functools.partial(
124+
lambda s: set(
125+
set({})
126+
if s == "{}"
127+
else [
128+
subtype_parser(item)
129+
for item in set(split_at_given_level(s[1:-1], split_delim=","))
130+
]
131+
)
132+
)
133+
134+
135+
def tuple_parser(
136+
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
137+
) -> Callable[[str], Tuple[Any, ...]]:
138+
"""
139+
Returns a function that parses a stringified tuple into a `Tuple` of the correct type.
140+
141+
Args:
142+
cls: the type of the class object this is being parsed for (used to get default val for
143+
parsers)
144+
type_: the type of the attribute to be parsed
145+
parsers: an optional mapping from type to the function to use for parsing that type (allows
146+
for parsing of more complex types)
147+
"""
148+
subtype_parsers = [
149+
_get_parser(
150+
cls,
151+
subtype,
152+
parsers,
153+
)
154+
for subtype in typing.get_args(type_)
155+
]
156+
157+
def tuple_parse(tuple_string: str) -> Tuple[Any, ...]:
158+
"""
159+
Parses a dictionary value (can do so recursively)
160+
Note that this tool will fail on tuples containing strings containing
161+
unpaired '{', or '}' characters
162+
"""
163+
assert tuple_string[0] == "(", "Tuple val improperly formatted"
164+
assert tuple_string[-1] == ")", "Tuple val improperly formatted"
165+
tuple_string = tuple_string[1:-1]
166+
if len(tuple_string) == 0:
167+
return ()
168+
else:
169+
val_strings = split_at_given_level(tuple_string, split_delim=",")
170+
return tuple(parser(val_str) for parser, val_str in zip(subtype_parsers, val_strings))
171+
172+
return functools.partial(tuple_parse)
173+
174+
175+
def dict_parser(
176+
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
177+
) -> Callable[[str], Dict[Any, Any]]:
178+
"""
179+
Returns a function that parses a stringified dict into a `Dict` of the correct type.
180+
181+
Args:
182+
cls: the type of the class object this is being parsed for (used to get default val for
183+
parsers)
184+
type_: the type of the attribute to be parsed
185+
parsers: an optional mapping from type to the function to use for parsing that type (allows
186+
for parsing of more complex types)
187+
"""
188+
subtypes = typing.get_args(type_)
189+
assert len(subtypes) == 2, "Dict object must have exactly 2 subtypes per PEP specification!"
190+
(key_parser, val_parser) = (
191+
_get_parser(
192+
cls,
193+
subtypes[0],
194+
parsers,
195+
),
196+
_get_parser(
197+
cls,
198+
subtypes[1],
199+
parsers,
200+
),
201+
)
202+
203+
def dict_parse(dict_string: str) -> Dict[Any, Any]:
204+
"""
205+
Parses a dictionary value (can do so recursively)
206+
"""
207+
assert dict_string[0] == "{", "Dict val improperly formatted"
208+
assert dict_string[-1] == "}", "Dict val improprly formatted"
209+
dict_string = dict_string[1:-1]
210+
if len(dict_string) == 0:
211+
return {}
212+
else:
213+
outer_splits = split_at_given_level(dict_string, split_delim=",")
214+
out_dict = {}
215+
for outer_split in outer_splits:
216+
inner_splits = split_at_given_level(outer_split, split_delim=";")
217+
assert (
218+
len(inner_splits) % 2 == 0
219+
), "Inner splits of dict didn't have matched key val pairs"
220+
for i in range(0, len(inner_splits), 2):
221+
out_dict[key_parser(inner_splits[i])] = val_parser(inner_splits[i + 1])
222+
return out_dict
223+
224+
return functools.partial(dict_parse)
225+
226+
73227
def _get_parser(
74228
cls: Type, type_: TypeAlias, parsers: Optional[Dict[type, Callable[[str], Any]]] = None
75229
) -> partial:
@@ -110,118 +264,13 @@ def get_parser() -> partial:
110264
elif type_ == dict:
111265
raise ValueError("Unable to parse dict (try typing.Mapping[type])")
112266
elif typing.get_origin(type_) == list:
113-
subtypes = typing.get_args(type_)
114-
115-
assert (
116-
len(subtypes) == 1
117-
), "Lists are allowed only one subtype per PEP specification!"
118-
subtype_parser = _get_parser(
119-
cls,
120-
subtypes[0],
121-
parsers,
122-
)
123-
return functools.partial(
124-
lambda s: list(
125-
[]
126-
if s == ""
127-
else [
128-
subtype_parser(item)
129-
for item in list(split_at_given_level(s, split_delim=","))
130-
]
131-
)
132-
)
267+
return list_parser(cls, type_, parsers)
133268
elif typing.get_origin(type_) == set:
134-
subtypes = typing.get_args(type_)
135-
assert (
136-
len(subtypes) == 1
137-
), "Sets are allowed only one subtype per PEP specification!"
138-
subtype_parser = _get_parser(
139-
cls,
140-
subtypes[0],
141-
parsers,
142-
)
143-
return functools.partial(
144-
lambda s: set(
145-
set({})
146-
if s == "{}"
147-
else [
148-
subtype_parser(item)
149-
for item in set(split_at_given_level(s[1:-1], split_delim=","))
150-
]
151-
)
152-
)
269+
return set_parser(cls, type_, parsers)
153270
elif typing.get_origin(type_) == tuple:
154-
subtype_parsers = [
155-
_get_parser(
156-
cls,
157-
subtype,
158-
parsers,
159-
)
160-
for subtype in typing.get_args(type_)
161-
]
162-
163-
def tuple_parse(tuple_string: str) -> Tuple[Any, ...]:
164-
"""
165-
Parses a dictionary value (can do so recursively)
166-
Note that this tool will fail on tuples containing strings containing
167-
unpaired '{', or '}' characters
168-
"""
169-
assert tuple_string[0] == "(", "Tuple val improperly formatted"
170-
assert tuple_string[-1] == ")", "Tuple val improperly formatted"
171-
tuple_string = tuple_string[1:-1]
172-
if len(tuple_string) == 0:
173-
return ()
174-
else:
175-
val_strings = split_at_given_level(tuple_string, split_delim=",")
176-
return tuple(
177-
parser(val_str)
178-
for parser, val_str in zip(subtype_parsers, val_strings)
179-
)
180-
181-
return functools.partial(tuple_parse)
182-
271+
return tuple_parser(cls, type_, parsers)
183272
elif typing.get_origin(type_) == dict:
184-
subtypes = typing.get_args(type_)
185-
assert (
186-
len(subtypes) == 2
187-
), "Dict object must have exactly 2 subtypes per PEP specification!"
188-
(key_parser, val_parser) = (
189-
_get_parser(
190-
cls,
191-
subtypes[0],
192-
parsers,
193-
),
194-
_get_parser(
195-
cls,
196-
subtypes[1],
197-
parsers,
198-
),
199-
)
200-
201-
def dict_parse(dict_string: str) -> Dict[Any, Any]:
202-
"""
203-
Parses a dictionary value (can do so recursively)
204-
"""
205-
assert dict_string[0] == "{", "Dict val improperly formatted"
206-
assert dict_string[-1] == "}", "Dict val improprly formatted"
207-
dict_string = dict_string[1:-1]
208-
if len(dict_string) == 0:
209-
return {}
210-
else:
211-
outer_splits = split_at_given_level(dict_string, split_delim=",")
212-
out_dict = {}
213-
for outer_split in outer_splits:
214-
inner_splits = split_at_given_level(outer_split, split_delim=";")
215-
assert (
216-
len(inner_splits) % 2 == 0
217-
), "Inner splits of dict didn't have matched key val pairs"
218-
for i in range(0, len(inner_splits), 2):
219-
out_dict[key_parser(inner_splits[i])] = val_parser(
220-
inner_splits[i + 1]
221-
)
222-
return out_dict
223-
224-
return functools.partial(dict_parse)
273+
return dict_parser(cls, type_, parsers)
225274
elif isinstance(type_, type) and issubclass(type_, Enum):
226275
return types.make_enum_parser(type_)
227276
elif types.is_constructible_from_str(type_):

fgpyo/util/tests/test_inspect.py

+28
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
from typing import Dict
2+
from typing import List
13
from typing import Optional
4+
from typing import Set
5+
from typing import Tuple
26

37
import attr
48
import pytest
59

610
from fgpyo.util.inspect import attr_from
711
from fgpyo.util.inspect import attribute_has_default
812
from fgpyo.util.inspect import attribute_is_optional
13+
from fgpyo.util.inspect import list_parser
14+
from fgpyo.util.inspect import set_parser
15+
from fgpyo.util.inspect import tuple_parser
16+
from fgpyo.util.inspect import dict_parser
917

1018

1119
@attr.s(auto_attribs=True, frozen=True)
@@ -66,3 +74,23 @@ def test_attr_from_custom_type_without_parser_fails() -> None:
6674
kwargs={"foo": ""},
6775
parsers={},
6876
)
77+
78+
79+
def test_list_parser() -> None:
80+
parser = list_parser(Foo, List[int], {})
81+
assert parser("1,2,3") == [1, 2, 3]
82+
83+
84+
def test_set_parser() -> None:
85+
parser = set_parser(Foo, Set[int], {})
86+
assert parser("{1,2,3}") == {1, 2, 3}
87+
88+
89+
def test_tuple_parser() -> None:
90+
parser = tuple_parser(Foo, Tuple[int, str], {})
91+
assert parser("(1,a)") == (1, "a")
92+
93+
94+
def test_dict_parser() -> None:
95+
parser = dict_parser(Foo, Dict[int, str], {})
96+
assert parser("{123;a}") == {123: "a"}

0 commit comments

Comments
 (0)