Skip to content

Commit 1e07f46

Browse files
filbrandenhynek
andauthored
Make attrs.converters.pipe only return a Converter instance if one is passed (#1380)
* Make attrs.converters.pipe only return a Converter instance if one is passed * Add type check overloads to specify that callables will return callables --------- Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 2a76643 commit 1e07f46

File tree

5 files changed

+52
-31
lines changed

5 files changed

+52
-31
lines changed

src/attr/_make.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,11 +2932,25 @@ def pipe(*converters):
29322932
.. versionadded:: 20.1.0
29332933
"""
29342934

2935-
def pipe_converter(val, inst, field):
2936-
for c in converters:
2937-
val = c(val, inst, field) if isinstance(c, Converter) else c(val)
2935+
return_instance = any(isinstance(c, Converter) for c in converters)
29382936

2939-
return val
2937+
if return_instance:
2938+
2939+
def pipe_converter(val, inst, field):
2940+
for c in converters:
2941+
val = (
2942+
c(val, inst, field) if isinstance(c, Converter) else c(val)
2943+
)
2944+
2945+
return val
2946+
2947+
else:
2948+
2949+
def pipe_converter(val):
2950+
for c in converters:
2951+
val = c(val)
2952+
2953+
return val
29402954

29412955
if not converters:
29422956
# If the converter list is empty, pipe_converter is the identity.
@@ -2957,4 +2971,6 @@ def pipe_converter(val, inst, field):
29572971
if rt:
29582972
pipe_converter.__annotations__["return"] = rt
29592973

2960-
return Converter(pipe_converter, takes_self=True, takes_field=True)
2974+
if return_instance:
2975+
return Converter(pipe_converter, takes_self=True, takes_field=True)
2976+
return pipe_converter

src/attr/converters.pyi

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
from typing import Callable, TypeVar, overload
1+
from typing import Callable, Any, overload
22

3-
from attrs import _ConverterType
4-
5-
_T = TypeVar("_T")
3+
from attrs import _ConverterType, _CallableConverterType
64

5+
@overload
6+
def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ...
7+
@overload
78
def pipe(*validators: _ConverterType) -> _ConverterType: ...
9+
@overload
10+
def optional(converter: _CallableConverterType) -> _CallableConverterType: ...
11+
@overload
812
def optional(converter: _ConverterType) -> _ConverterType: ...
913
@overload
10-
def default_if_none(default: _T) -> _ConverterType: ...
14+
def default_if_none(default: Any) -> _CallableConverterType: ...
1115
@overload
12-
def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...
16+
def default_if_none(
17+
*, factory: Callable[[], Any]
18+
) -> _CallableConverterType: ...
1319
def to_bool(val: str | int | bool) -> bool: ...

src/attrs/__init__.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ _C = TypeVar("_C", bound=type)
5151

5252
_EqOrderType = bool | Callable[[Any], Any]
5353
_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any]
54-
_ConverterType = Callable[[Any], Any] | Converter[Any, _T]
54+
_CallableConverterType = Callable[[Any], Any]
55+
_ConverterType = _CallableConverterType | Converter[Any, Any]
5556
_ReprType = Callable[[Any], str]
5657
_ReprArgType = bool | _ReprType
5758
_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any]

tests/test_annotations.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -278,27 +278,25 @@ def strlen(y: str) -> int:
278278
def identity(z):
279279
return z
280280

281-
assert attr.converters.pipe(int2str).converter.__annotations__ == {
281+
assert attr.converters.pipe(int2str).__annotations__ == {
282282
"val": int,
283283
"return": str,
284284
}
285-
assert attr.converters.pipe(
286-
int2str, strlen
287-
).converter.__annotations__ == {
285+
assert attr.converters.pipe(int2str, strlen).__annotations__ == {
288286
"val": int,
289287
"return": int,
290288
}
291-
assert attr.converters.pipe(
292-
identity, strlen
293-
).converter.__annotations__ == {"return": int}
294-
assert attr.converters.pipe(
295-
int2str, identity
296-
).converter.__annotations__ == {"val": int}
289+
assert attr.converters.pipe(identity, strlen).__annotations__ == {
290+
"return": int
291+
}
292+
assert attr.converters.pipe(int2str, identity).__annotations__ == {
293+
"val": int
294+
}
297295

298296
def int2str_(x: int, y: int = 0) -> str:
299297
return str(x)
300298

301-
assert attr.converters.pipe(int2str_).converter.__annotations__ == {
299+
assert attr.converters.pipe(int2str_).__annotations__ == {
302300
"val": int,
303301
"return": str,
304302
}
@@ -310,19 +308,19 @@ def test_pipe_empty(self):
310308

311309
p = attr.converters.pipe()
312310

313-
assert "val" in p.converter.__annotations__
311+
assert "val" in p.__annotations__
314312

315-
t = p.converter.__annotations__["val"]
313+
t = p.__annotations__["val"]
316314

317315
assert isinstance(t, typing.TypeVar)
318-
assert p.converter.__annotations__ == {"val": t, "return": t}
316+
assert p.__annotations__ == {"val": t, "return": t}
319317

320318
def test_pipe_non_introspectable(self):
321319
"""
322320
pipe() doesn't crash when passed a non-introspectable converter.
323321
"""
324322

325-
assert attr.converters.pipe(print).converter.__annotations__ == {}
323+
assert attr.converters.pipe(print).__annotations__ == {}
326324

327325
def test_pipe_nullary(self):
328326
"""
@@ -332,7 +330,7 @@ def test_pipe_nullary(self):
332330
def noop():
333331
pass
334332

335-
assert attr.converters.pipe(noop).converter.__annotations__ == {}
333+
assert attr.converters.pipe(noop).__annotations__ == {}
336334

337335
def test_optional(self):
338336
"""

tests/test_converters.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,11 @@ def test_fail(self):
248248

249249
# First wrapped converter fails:
250250
with pytest.raises(ValueError):
251-
c.converter(33, None, None)
251+
c(33)
252252

253253
# Last wrapped converter fails:
254254
with pytest.raises(ValueError):
255-
c.converter("33", None, None)
255+
c("33")
256256

257257
def test_sugar(self):
258258
"""
@@ -273,7 +273,7 @@ def test_empty(self):
273273
"""
274274
o = object()
275275

276-
assert o is pipe().converter(o, None, None)
276+
assert o is pipe()(o)
277277

278278
def test_wrapped_annotation(self):
279279
"""

0 commit comments

Comments
 (0)