Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline distutils.util.strtobool in tests (closes #813) #830

Merged
merged 5 commits into from Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/api.rst
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
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
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
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
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