Skip to content

Commit 0aa7346

Browse files
authored
Factor out functions for parsers for built-in collections (#101)
Creates public functions for the four built-in collection types handled by _get_parser: list, set, tuple, and dict.
1 parent cdfbc53 commit 0aa7346

File tree

2 files changed

+199
-109
lines changed

2 files changed

+199
-109
lines changed

fgpyo/util/inspect.py

+160-109
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,162 @@ def split_at_given_level(
7070
NoneType = type(None)
7171

7272

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

fgpyo/util/tests/test_inspect.py

+39
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 dict_parser
14+
from fgpyo.util.inspect import list_parser
15+
from fgpyo.util.inspect import set_parser
16+
from fgpyo.util.inspect import tuple_parser
917

1018

1119
@attr.s(auto_attribs=True, frozen=True)
@@ -66,3 +74,34 @@ 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("") == []
82+
assert parser("1,2,3") == [1, 2, 3]
83+
84+
85+
def test_set_parser() -> None:
86+
parser = set_parser(Foo, Set[int], {})
87+
assert parser("{}") == set()
88+
assert parser("{1,2,3}") == {1, 2, 3}
89+
assert parser("{1,1,2,3}") == {1, 2, 3}
90+
91+
92+
def test_tuple_parser() -> None:
93+
parser = tuple_parser(Foo, Tuple[int, str], {})
94+
assert parser("()") == ()
95+
assert parser("(1,a)") == (1, "a")
96+
97+
98+
def test_dict_parser() -> None:
99+
parser = dict_parser(Foo, Dict[int, str], {})
100+
assert parser("{}") == {}
101+
assert parser("{123;a}") == {123: "a"}
102+
103+
104+
def test_dict_parser_with_duplicate_keys() -> None:
105+
parser = dict_parser(Foo, Dict[int, str], {})
106+
with pytest.raises(ValueError):
107+
parser("{123;a,123;b}")

0 commit comments

Comments
 (0)