[RFC] kw_only python 2 backport (#700)
* Added kw_only support for py2 * Docs update * Added changelog * Better exception message, moved code to function * Moved py2-only functions under if PY2: * Tested fancy error message for unexpected kw-only argument * Tested fancy error message for unexpected kw-only argument * Fixed line length * Added versionchanged * Updated docs * Moved functions back under if PY2 - seems codecov doesn't like them in module scope * Blacked * Fixed changelog.d * Removed redundant brackets in test * Added assertion to the _unpack_kw_only_lines_py2 - hope it will increase code coverage * List comprehension -> for loop * lines.extend? I do not like for-loops * Fix code * Fixed style/added comment * Fixed docs (removed python2 mention) * Fix lint * Better docstring * Rewritten docstring and added example code * Added quotes Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
parent
0e6c74ac17
commit
bc527b9f29
|
@ -0,0 +1 @@
|
|||
``kw_only=True`` now works on Python 2.
|
|
@ -146,7 +146,7 @@ On Python 3 it overrides the implicit detection.
|
|||
Keyword-only Attributes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using ``attrs`` on Python 3, you can also add `keyword-only <https://docs.python.org/3/glossary.html#keyword-only-parameter>`_ attributes:
|
||||
You can also add `keyword-only <https://docs.python.org/3/glossary.html#keyword-only-parameter>`_ attributes:
|
||||
|
||||
.. doctest::
|
||||
|
||||
|
|
|
@ -223,6 +223,7 @@ def attrib(
|
|||
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
|
||||
.. versionadded:: 19.2.0 *eq* and *order*
|
||||
.. versionadded:: 20.1.0 *on_setattr*
|
||||
.. versionchanged:: 20.3.0 *kw_only* backported to Python 2
|
||||
"""
|
||||
eq, order = _determine_eq_order(cmp, eq, order, True)
|
||||
|
||||
|
@ -1924,6 +1925,63 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr):
|
|||
)
|
||||
|
||||
|
||||
if PY2:
|
||||
|
||||
def _unpack_kw_only_py2(attr_name, default=None):
|
||||
"""
|
||||
Unpack *attr_name* from _kw_only dict.
|
||||
"""
|
||||
if default is not None:
|
||||
arg_default = ", %s" % default
|
||||
else:
|
||||
arg_default = ""
|
||||
return "%s = _kw_only.pop('%s'%s)" % (
|
||||
attr_name,
|
||||
attr_name,
|
||||
arg_default,
|
||||
)
|
||||
|
||||
def _unpack_kw_only_lines_py2(kw_only_args):
|
||||
"""
|
||||
Unpack all *kw_only_args* from _kw_only dict and handle errors.
|
||||
|
||||
Given a list of strings "{attr_name}" and "{attr_name}={default}"
|
||||
generates list of lines of code that pop attrs from _kw_only dict and
|
||||
raise TypeError similar to builtin if required attr is missing or
|
||||
extra key is passed.
|
||||
|
||||
>>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"])))
|
||||
try:
|
||||
a = _kw_only.pop('a')
|
||||
b = _kw_only.pop('b', 42)
|
||||
except KeyError as _key_error:
|
||||
raise TypeError(
|
||||
...
|
||||
if _kw_only:
|
||||
raise TypeError(
|
||||
...
|
||||
"""
|
||||
lines = ["try:"]
|
||||
lines.extend(
|
||||
" " + _unpack_kw_only_py2(*arg.split("="))
|
||||
for arg in kw_only_args
|
||||
)
|
||||
lines += """\
|
||||
except KeyError as _key_error:
|
||||
raise TypeError(
|
||||
'__init__() missing required keyword-only argument: %s' % _key_error
|
||||
)
|
||||
if _kw_only:
|
||||
raise TypeError(
|
||||
'__init__() got an unexpected keyword argument %r'
|
||||
% next(iter(_kw_only))
|
||||
)
|
||||
""".split(
|
||||
"\n"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def _attrs_to_init_script(
|
||||
attrs,
|
||||
frozen,
|
||||
|
@ -2178,14 +2236,14 @@ def _attrs_to_init_script(
|
|||
args = ", ".join(args)
|
||||
if kw_only_args:
|
||||
if PY2:
|
||||
raise PythonTooOldError(
|
||||
"Keyword-only arguments only work on Python 3 and later."
|
||||
)
|
||||
lines = _unpack_kw_only_lines_py2(kw_only_args) + lines
|
||||
|
||||
args += "{leading_comma}*, {kw_only_args}".format(
|
||||
leading_comma=", " if args else "",
|
||||
kw_only_args=", ".join(kw_only_args),
|
||||
)
|
||||
args += "%s**_kw_only" % (", " if args else "",) # leading comma
|
||||
else:
|
||||
args += "%s*, %s" % (
|
||||
", " if args else "", # leading comma
|
||||
", ".join(kw_only_args), # kw_only args
|
||||
)
|
||||
return (
|
||||
"""\
|
||||
def __init__(self, {args}):
|
||||
|
|
|
@ -716,7 +716,6 @@ class TestAttributes(object):
|
|||
assert hash(ba) == hash(sa)
|
||||
|
||||
|
||||
@pytest.mark.skipif(PY2, reason="keyword-only arguments are PY3-only.")
|
||||
class TestKeywordOnlyAttributes(object):
|
||||
"""
|
||||
Tests for keyword-only attributes.
|
||||
|
@ -728,7 +727,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
class C(object):
|
||||
a = attr.ib()
|
||||
b = attr.ib(default=2, kw_only=True)
|
||||
c = attr.ib(kw_only=True)
|
||||
|
@ -747,7 +746,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
class C(object):
|
||||
x = attr.ib(init=False, default=0, kw_only=True)
|
||||
y = attr.ib()
|
||||
|
||||
|
@ -763,15 +762,34 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
class C(object):
|
||||
x = attr.ib(kw_only=True)
|
||||
|
||||
with pytest.raises(TypeError) as e:
|
||||
C()
|
||||
|
||||
assert (
|
||||
"missing 1 required keyword-only argument: 'x'"
|
||||
) in e.value.args[0]
|
||||
if PY2:
|
||||
assert (
|
||||
"missing required keyword-only argument: 'x'"
|
||||
) in e.value.args[0]
|
||||
else:
|
||||
assert (
|
||||
"missing 1 required keyword-only argument: 'x'"
|
||||
) in e.value.args[0]
|
||||
|
||||
def test_keyword_only_attributes_unexpected(self):
|
||||
"""
|
||||
Raises `TypeError` when unexpected keyword argument passed.
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
class C(object):
|
||||
x = attr.ib(kw_only=True)
|
||||
|
||||
with pytest.raises(TypeError) as e:
|
||||
C(x=5, y=10)
|
||||
|
||||
assert "got an unexpected keyword argument 'y'" in e.value.args[0]
|
||||
|
||||
def test_keyword_only_attributes_can_come_in_any_order(self):
|
||||
"""
|
||||
|
@ -782,7 +800,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
class C(object):
|
||||
a = attr.ib(kw_only=True)
|
||||
b = attr.ib(kw_only=True, default="b")
|
||||
c = attr.ib(kw_only=True)
|
||||
|
@ -811,7 +829,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class Base:
|
||||
class Base(object):
|
||||
x = attr.ib(default=0)
|
||||
|
||||
@attr.s
|
||||
|
@ -830,7 +848,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class C:
|
||||
class C(object):
|
||||
x = attr.ib()
|
||||
y = attr.ib(kw_only=True)
|
||||
|
||||
|
@ -849,7 +867,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class Base:
|
||||
class Base(object):
|
||||
x = attr.ib(default=0)
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
|
@ -872,7 +890,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class KwArgBeforeInitFalse:
|
||||
class KwArgBeforeInitFalse(object):
|
||||
kwarg = attr.ib(kw_only=True)
|
||||
non_init_function_default = attr.ib(init=False)
|
||||
non_init_keyword_default = attr.ib(
|
||||
|
@ -900,7 +918,7 @@ class TestKeywordOnlyAttributes(object):
|
|||
"""
|
||||
|
||||
@attr.s
|
||||
class KwArgBeforeInitFalseParent:
|
||||
class KwArgBeforeInitFalseParent(object):
|
||||
kwarg = attr.ib(kw_only=True)
|
||||
|
||||
@attr.s
|
||||
|
@ -927,23 +945,6 @@ class TestKeywordOnlyAttributesOnPy2(object):
|
|||
Tests for keyword-only attribute behavior on py2.
|
||||
"""
|
||||
|
||||
def test_syntax_error(self):
|
||||
"""
|
||||
Keyword-only attributes raise Syntax error on ``__init__`` generation.
|
||||
"""
|
||||
|
||||
with pytest.raises(PythonTooOldError):
|
||||
|
||||
@attr.s(kw_only=True)
|
||||
class ClassLevel(object):
|
||||
a = attr.ib()
|
||||
|
||||
with pytest.raises(PythonTooOldError):
|
||||
|
||||
@attr.s()
|
||||
class AttrLevel(object):
|
||||
a = attr.ib(kw_only=True)
|
||||
|
||||
def test_no_init(self):
|
||||
"""
|
||||
Keyworld-only is a no-op, not any error, if ``init=false``.
|
||||
|
|
Loading…
Reference in New Issue