Skip to content

Commit

Permalink
Inline distutils.util.strtobool in tests (closes #813) (#830)
Browse files Browse the repository at this point in the history
* Inline distutils.util.strtobool in tests (#813)

`distutils` is deprecated in Python 3.10 and slated for removal in
Python 3.12. Fortunately, `attrs` only uses `distutils` once and it's
trivial to remove.

As suggested by @sscherfke, add the `to_bool` converter to
`converters.py`.

Closes #813

Co-authored-by: Stefan Scherfke <stefan@sofa-rockers.org>

* Use :raises: directive in docstring

* Remove f-strings for Py2.7 and 3.5 support

* Add to_bool tests

Co-authored-by: Stefan Scherfke <stefan@sofa-rockers.org>
Co-authored-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
3 people authored Aug 10, 2021
1 parent 2ca7aad commit e84b57e
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 7 deletions.
22 changes: 22 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,28 @@ Converters
C(x='')


.. autofunction:: attr.converters.to_bool

For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(
... converter=attr.converters.to_bool
... )
>>> C("yes")
C(x=True)
>>> C(0)
C(x=False)
>>> C("foo")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Cannot convert value to bool: foo



.. _api_setters:

Setters
Expand Down
41 changes: 41 additions & 0 deletions src/attr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,44 @@ def default_if_none_converter(val):
return default

return default_if_none_converter


def to_bool(val):
"""
Convert "boolean" strings (e.g., from env. vars.) to real booleans.
Values mapping to :code:`True`:
- :code:`True`
- :code:`"true"` / :code:`"t"`
- :code:`"yes"` / :code:`"y"`
- :code:`"on"`
- :code:`"1"`
- :code:`1`
Values mapping to :code:`False`:
- :code:`False`
- :code:`"false"` / :code:`"f"`
- :code:`"no"` / :code:`"n"`
- :code:`"off"`
- :code:`"0"`
- :code:`0`
:raises ValueError: for any other value.
.. versionadded:: 21.3.0
"""
if isinstance(val, str):
val = val.lower()
truthy = {True, "true", "t", "yes", "y", "on", "1", 1}
falsy = {False, "false", "f", "no", "n", "off", "0", 0}
try:
if val in truthy:
return True
if val in falsy:
return False
except TypeError:
# Raised when "val" is not hashable (e.g., lists)
pass
raise ValueError("Cannot convert value to bool: {}".format(val))
1 change: 1 addition & 0 deletions src/attr/converters.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ def optional(converter: _ConverterType) -> _ConverterType: ...
def default_if_none(default: _T) -> _ConverterType: ...
@overload
def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...
def to_bool(val: str) -> bool: ...
37 changes: 30 additions & 7 deletions tests/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

from __future__ import absolute_import

from distutils.util import strtobool

import pytest

import attr

from attr import Factory, attrib
from attr.converters import default_if_none, optional, pipe
from attr.converters import default_if_none, optional, pipe, to_bool


class TestOptional(object):
Expand Down Expand Up @@ -106,15 +104,15 @@ def test_success(self):
"""
Succeeds if all wrapped converters succeed.
"""
c = pipe(str, strtobool, bool)
c = pipe(str, to_bool, bool)

assert True is c("True") is c(True)

def test_fail(self):
"""
Fails if any wrapped converter fails.
"""
c = pipe(str, strtobool)
c = pipe(str, to_bool)

# First wrapped converter fails:
with pytest.raises(ValueError):
Expand All @@ -131,8 +129,33 @@ def test_sugar(self):

@attr.s
class C(object):
a1 = attrib(default="True", converter=pipe(str, strtobool, bool))
a2 = attrib(default=True, converter=[str, strtobool, bool])
a1 = attrib(default="True", converter=pipe(str, to_bool, bool))
a2 = attrib(default=True, converter=[str, to_bool, bool])

c = C()
assert True is c.a1 is c.a2


class TestToBool(object):
def test_unhashable(self):
"""
Fails if value is unhashable.
"""
with pytest.raises(ValueError, match="Cannot convert value to bool"):
to_bool([])

def test_truthy(self):
"""
Fails if truthy values are incorrectly converted.
"""
assert to_bool("t")
assert to_bool("yes")
assert to_bool("on")

def test_falsy(self):
"""
Fails if falsy values are incorrectly converted.
"""
assert not to_bool("f")
assert not to_bool("no")
assert not to_bool("off")
14 changes: 14 additions & 0 deletions tests/typing_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ class Error(Exception):
# ConvCDefaultIfNone(None)


# @attr.s
# class ConvCToBool:
# x: int = attr.ib(converter=attr.converters.to_bool)


# ConvCToBool(1)
# ConvCToBool(True)
# ConvCToBool("on")
# ConvCToBool("yes")
# ConvCToBool(0)
# ConvCToBool(False)
# ConvCToBool("n")


# Validators
@attr.s
class Validated:
Expand Down

0 comments on commit e84b57e

Please sign in to comment.