From c9ed44aafa8561a0774fb570dd04cf76064842a7 Mon Sep 17 00:00:00 2001 From: drinob Date: Mon, 12 Oct 2020 09:37:59 +0300 Subject: [PATCH 01/24] Added kw_only support for py2 --- src/attr/_make.py | 25 ++++++++++++++++++------ tests/test_make.py | 47 +++++++++++++++++----------------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 4c9e074f8..4b34642dd 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2159,14 +2159,27 @@ def fmt_setter_with_converter( args = ", ".join(args) if kw_only_args: if PY2: - raise PythonTooOldError( - "Keyword-only arguments only work on Python 3 and later." + kw_only_args.sort(key=lambda arg: "=" in arg) + kw_only_arg_tuple = ", ".join( + arg.split("=", 1)[0] for arg in kw_only_args ) - args += "{leading_comma}*, {kw_only_args}".format( - leading_comma=", " if args else "", - kw_only_args=", ".join(kw_only_args), - ) + parse_kw_only_args_lines = [ + "def __attrs_unpack_kw_only__(%s):" % ", ".join(kw_only_args), + " return %s" % kw_only_arg_tuple, + "%s = __attrs_unpack_kw_only__(**_kwargs)" % kw_only_arg_tuple, + " ", + ] + lines = parse_kw_only_args_lines + lines + + args += "{leading_comma} **_kwargs".format( + leading_comma=", " if args else "" + ) + else: + args += "{leading_comma}*, {kw_only_args}".format( + leading_comma=", " if args else "", + kw_only_args=", ".join(kw_only_args), + ) return ( """\ def __init__(self, {args}): diff --git a/tests/test_make.py b/tests/test_make.py index d3f4a279e..47d0cc323 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -712,7 +712,6 @@ class SubClass(BaseClass): assert hash(ba) == hash(sa) -@pytest.mark.skipif(PY2, reason="keyword-only arguments are PY3-only.") class TestKeywordOnlyAttributes(object): """ Tests for keyword-only attributes. @@ -724,7 +723,7 @@ def test_adds_keyword_only_arguments(self): """ @attr.s - class C: + class C(object): a = attr.ib() b = attr.ib(default=2, kw_only=True) c = attr.ib(kw_only=True) @@ -743,7 +742,7 @@ def test_ignores_kw_only_when_init_is_false(self): """ @attr.s - class C: + class C(object): x = attr.ib(init=False, default=0, kw_only=True) y = attr.ib() @@ -759,15 +758,20 @@ def test_keyword_only_attributes_presence(self): """ @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 ( + "__attrs_unpack_kw_only__() takes exactly 1 argument (0 given)" + ) in e.value.args[0] + else: + assert ( + "missing 1 required keyword-only argument: 'x'" + ) in e.value.args[0] def test_keyword_only_attributes_can_come_in_any_order(self): """ @@ -778,7 +782,7 @@ def test_keyword_only_attributes_can_come_in_any_order(self): """ @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) @@ -807,7 +811,7 @@ def test_keyword_only_attributes_allow_subclassing(self): """ @attr.s - class Base: + class Base(object): x = attr.ib(default=0) @attr.s @@ -826,7 +830,7 @@ def test_keyword_only_class_level(self): """ @attr.s(kw_only=True) - class C: + class C(object): x = attr.ib() y = attr.ib(kw_only=True) @@ -845,7 +849,7 @@ def test_keyword_only_class_level_subclassing(self): """ @attr.s - class Base: + class Base(object): x = attr.ib(default=0) @attr.s(kw_only=True) @@ -868,7 +872,7 @@ def test_init_false_attribute_after_keyword_attribute(self): """ @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( @@ -896,7 +900,7 @@ def test_init_false_attribute_after_keyword_attribute_with_inheritance( """ @attr.s - class KwArgBeforeInitFalseParent: + class KwArgBeforeInitFalseParent(object): kwarg = attr.ib(kw_only=True) @attr.s @@ -923,23 +927,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``. From c938678ab1dd47a8a34ee731d21487710b5ae07c Mon Sep 17 00:00:00 2001 From: drinob Date: Mon, 12 Oct 2020 10:33:00 +0300 Subject: [PATCH 02/24] Docs update --- docs/examples.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index 508aa9f6e..b4023bf36 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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 `_ attributes: +You can also add `keyword-only `_ attributes: .. doctest:: @@ -160,6 +160,8 @@ When using ``attrs`` on Python 3, you can also add `keyword-only >> A(a=1) A(a=1) +``attrs`` supports ``kw_only`` attributes even on Python 2, though error messages are default python error messages and may be a bit confusing. + ``kw_only`` may also be specified at via ``attr.s``, and will apply to all attributes: .. doctest:: From ca337e9628db28769f7d2aa9b873025987d82fb5 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 11:31:37 +0300 Subject: [PATCH 03/24] Added changelog --- changelog.d/700.changing.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/700.changing.rst diff --git a/changelog.d/700.changing.rst b/changelog.d/700.changing.rst new file mode 100644 index 000000000..292a4d8d9 --- /dev/null +++ b/changelog.d/700.changing.rst @@ -0,0 +1 @@ +Added support for `kw_only=True` on Python 2. From f636e147c09ec7684ac702ade0a8c97195f348e0 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 11:32:34 +0300 Subject: [PATCH 04/24] Better exception message, moved code to function --- src/attr/_make.py | 48 +++++++++++++++++++++++++++++----------------- tests/test_make.py | 2 +- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 4b34642dd..9939705d0 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1905,6 +1905,31 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr): ) +def _unpack_kw_only_py2(attr_name, default=None): + """ + Unpack *attr_name* for _kw_only dict. Used to support kw_only arguments in py2. + """ + 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): + lines = ["try:"] + lines += [ + " " + _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)))", + ] + return lines + + def _attrs_to_init_script( attrs, frozen, @@ -2159,26 +2184,13 @@ def fmt_setter_with_converter( args = ", ".join(args) if kw_only_args: if PY2: - kw_only_args.sort(key=lambda arg: "=" in arg) - kw_only_arg_tuple = ", ".join( - arg.split("=", 1)[0] for arg in kw_only_args - ) + lines = _unpack_kw_only_lines_py2(kw_only_args) + lines - parse_kw_only_args_lines = [ - "def __attrs_unpack_kw_only__(%s):" % ", ".join(kw_only_args), - " return %s" % kw_only_arg_tuple, - "%s = __attrs_unpack_kw_only__(**_kwargs)" % kw_only_arg_tuple, - " ", - ] - lines = parse_kw_only_args_lines + lines - - args += "{leading_comma} **_kwargs".format( - leading_comma=", " if args else "" - ) + args += "%s**_kw_only" % (", " if args else "",) # leading comma else: - args += "{leading_comma}*, {kw_only_args}".format( - leading_comma=", " if args else "", - kw_only_args=", ".join(kw_only_args), + args += "%s*, %s" % ( + ", " if args else "", # leading comma + ", ".join(kw_only_args), # kw_only args ) return ( """\ diff --git a/tests/test_make.py b/tests/test_make.py index 47d0cc323..5fe79eefc 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -766,7 +766,7 @@ class C(object): if PY2: assert ( - "__attrs_unpack_kw_only__() takes exactly 1 argument (0 given)" + "missing required keyword-only argument: 'x'" ) in e.value.args[0] else: assert ( From e12ad7549e3871cb5797cb714bb5c79cddd8c707 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 11:40:15 +0300 Subject: [PATCH 05/24] Moved py2-only functions under if PY2: --- src/attr/_make.py | 48 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 9939705d0..d4bc2c479 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1905,29 +1905,35 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr): ) -def _unpack_kw_only_py2(attr_name, default=None): - """ - Unpack *attr_name* for _kw_only dict. Used to support kw_only arguments in py2. - """ - 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) +if PY2: + def _unpack_kw_only_py2(attr_name, default=None): + """ + Unpack *attr_name* for _kw_only dict. Used to support kw_only arguments in py2. + """ + 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): - lines = ["try:"] - lines += [ - " " + _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)))", - ] - return lines + def _unpack_kw_only_lines_py2(kw_only_args): + lines = ["try:"] + lines += [ + " " + _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)))", + ] + return lines def _attrs_to_init_script( From c6e58aa066b9eb175cde254497ce4c50c062174e Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 11:50:57 +0300 Subject: [PATCH 06/24] Tested fancy error message for unexpected kw-only argument --- tests/test_make.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_make.py b/tests/test_make.py index 5fe79eefc..c78af7203 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -773,6 +773,23 @@ class C(object): "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): """ Mandatory vs non-mandatory attr order only matters when they are part From 0a453404bbc0e9e4571673412eaa187ca96d6e02 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 11:55:10 +0300 Subject: [PATCH 07/24] Tested fancy error message for unexpected kw-only argument --- tests/test_make.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index c78af7203..fb6148777 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -785,10 +785,7 @@ class C(object): with pytest.raises(TypeError) as e: C(x=5, y=10) - assert( - "got an unexpected keyword argument 'y'" - ) in e.value.args[0] - + assert ("got an unexpected keyword argument 'y'") in e.value.args[0] def test_keyword_only_attributes_can_come_in_any_order(self): """ From d052808a650fa4d29ec1d3f8c719285e411a4443 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:00:13 +0300 Subject: [PATCH 08/24] Fixed line length --- src/attr/_make.py | 58 +++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index d4bc2c479..12ca15312 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1905,35 +1905,39 @@ 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_py2(attr_name, default=None): - """ - Unpack *attr_name* for _kw_only dict. Used to support kw_only arguments in py2. - """ - 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): - lines = ["try:"] - lines += [ - " " + _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)))", - ] - return lines +def _unpack_kw_only_lines_py2(kw_only_args): + """ + Unpack all *kw_only_args* from _kw_only dict and handle errors. + """ + lines = ["try:"] + lines += [ + " " + _unpack_kw_only_py2(*arg.split("=")) + for arg in kw_only_args + ] + lines += """\ +except KeyError as _key_error: + _msg = '__init__() missing required keyword-only argument: %s' + raise TypeError(_msg % _key_error) +if _kw_only: + _msg = '__init__() got an unexpected keyword argument %r' + raise TypeError(_msg % next(iter(_kw_only))) +""".split('\n') + return lines def _attrs_to_init_script( From 356a25603970f9f1285b590d9addb550d2c4945e Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:03:11 +0300 Subject: [PATCH 09/24] Added versionchanged --- src/attr/_make.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index 12ca15312..e3a636e40 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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* supported on python 2 """ eq, order = _determine_eq_order(cmp, eq, order, True) From 430ea7324c29aa4518057ee81f8a154953f6eadb Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:10:45 +0300 Subject: [PATCH 10/24] Updated docs --- docs/examples.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index b4023bf36..8497734fc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -146,7 +146,7 @@ On Python 3 it overrides the implicit detection. Keyword-only Attributes ~~~~~~~~~~~~~~~~~~~~~~~ -You can also add `keyword-only `_ attributes: +You can also add `keyword-only `_ attributes (even on Python 2): .. doctest:: @@ -160,8 +160,6 @@ You can also add `keyword-only >> A(a=1) A(a=1) -``attrs`` supports ``kw_only`` attributes even on Python 2, though error messages are default python error messages and may be a bit confusing. - ``kw_only`` may also be specified at via ``attr.s``, and will apply to all attributes: .. doctest:: From c228a64333def6168d6b5bf6a2e21101e7e6eb2a Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:11:13 +0300 Subject: [PATCH 11/24] Moved functions back under if PY2 - seems codecov doesn't like them in module scope --- src/attr/_make.py | 51 ++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index e3a636e40..86d14485c 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -223,7 +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* supported on python 2 + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 """ eq, order = _determine_eq_order(cmp, eq, order, True) @@ -1906,31 +1906,32 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr): ) -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, - ) +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. - """ - lines = ["try:"] - lines += [ - " " + _unpack_kw_only_py2(*arg.split("=")) - for arg in kw_only_args - ] - lines += """\ + def _unpack_kw_only_lines_py2(kw_only_args): + """ + Unpack all *kw_only_args* from _kw_only dict and handle errors. + """ + lines = ["try:"] + lines += [ + " " + _unpack_kw_only_py2(*arg.split("=")) + for arg in kw_only_args + ] + lines += """\ except KeyError as _key_error: _msg = '__init__() missing required keyword-only argument: %s' raise TypeError(_msg % _key_error) @@ -1938,7 +1939,7 @@ def _unpack_kw_only_lines_py2(kw_only_args): _msg = '__init__() got an unexpected keyword argument %r' raise TypeError(_msg % next(iter(_kw_only))) """.split('\n') - return lines + return lines def _attrs_to_init_script( From ca9600d12db6de99c1d9850b048aea8f9c66facb Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:19:20 +0300 Subject: [PATCH 12/24] Blacked --- src/attr/_make.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 86d14485c..cfbb302b1 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1907,6 +1907,7 @@ 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. @@ -1921,7 +1922,6 @@ def _unpack_kw_only_py2(attr_name, default=None): arg_default, ) - def _unpack_kw_only_lines_py2(kw_only_args): """ Unpack all *kw_only_args* from _kw_only dict and handle errors. @@ -1938,7 +1938,9 @@ def _unpack_kw_only_lines_py2(kw_only_args): if _kw_only: _msg = '__init__() got an unexpected keyword argument %r' raise TypeError(_msg % next(iter(_kw_only))) -""".split('\n') +""".split( + "\n" + ) return lines From 2dd691f776643373e11751c2c8b56c9600d2a11e Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 12:58:19 +0300 Subject: [PATCH 13/24] Fixed changelog.d --- changelog.d/700.change.rst | 1 + changelog.d/700.changing.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/700.change.rst delete mode 100644 changelog.d/700.changing.rst diff --git a/changelog.d/700.change.rst b/changelog.d/700.change.rst new file mode 100644 index 000000000..d4dc2dfd0 --- /dev/null +++ b/changelog.d/700.change.rst @@ -0,0 +1 @@ +``kw_only=True`` now works on Python 2. diff --git a/changelog.d/700.changing.rst b/changelog.d/700.changing.rst deleted file mode 100644 index 292a4d8d9..000000000 --- a/changelog.d/700.changing.rst +++ /dev/null @@ -1 +0,0 @@ -Added support for `kw_only=True` on Python 2. From edd79f21aeb7bf71b6ea4af13af66992e04e42e2 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 13:08:03 +0300 Subject: [PATCH 14/24] Removed redundant brackets in test --- tests/test_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_make.py b/tests/test_make.py index 534f5b8de..fad4ec7e2 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -789,7 +789,7 @@ class C(object): with pytest.raises(TypeError) as e: C(x=5, y=10) - assert ("got an unexpected keyword argument 'y'") in e.value.args[0] + assert "got an unexpected keyword argument 'y'" in e.value.args[0] def test_keyword_only_attributes_can_come_in_any_order(self): """ From b03f015f0a0cf37090035c2720b3b3b20d88e75e Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 23:21:36 +0300 Subject: [PATCH 15/24] Added assertion to the _unpack_kw_only_lines_py2 - hope it will increase code coverage --- src/attr/_make.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index d269e3753..b4168a7f9 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1945,6 +1945,7 @@ def _unpack_kw_only_lines_py2(kw_only_args): """ Unpack all *kw_only_args* from _kw_only dict and handle errors. """ + assert kw_only_args lines = ["try:"] lines += [ " " + _unpack_kw_only_py2(*arg.split("=")) From 6e15d380f0c1dc0afdd755c758fb56353c75b274 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 23:29:01 +0300 Subject: [PATCH 16/24] List comprehension -> for loop --- src/attr/_make.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index b4168a7f9..0c30448f3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1947,10 +1947,8 @@ def _unpack_kw_only_lines_py2(kw_only_args): """ assert kw_only_args lines = ["try:"] - lines += [ - " " + _unpack_kw_only_py2(*arg.split("=")) - for arg in kw_only_args - ] + for arg in kw_only_args: + lines.append(" " + _unpack_kw_only_py2(*arg.split("="))) lines += """\ except KeyError as _key_error: _msg = '__init__() missing required keyword-only argument: %s' From 42d5e427bbda57098473f7c7a3a370fbf0008592 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 23:42:31 +0300 Subject: [PATCH 17/24] lines.extend? I do not like for-loops --- src/attr/_make.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 0c30448f3..b5794bb58 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1947,8 +1947,10 @@ def _unpack_kw_only_lines_py2(kw_only_args): """ assert kw_only_args lines = ["try:"] - for arg in kw_only_args: + lines.extend( lines.append(" " + _unpack_kw_only_py2(*arg.split("="))) + for arg in kw_only_args + ) lines += """\ except KeyError as _key_error: _msg = '__init__() missing required keyword-only argument: %s' From 69c6de61b333d5a0369abac5bfd9577b2fb367a9 Mon Sep 17 00:00:00 2001 From: drinob Date: Thu, 15 Oct 2020 23:47:17 +0300 Subject: [PATCH 18/24] Fix code --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index b5794bb58..c6f6cbb44 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1948,7 +1948,7 @@ def _unpack_kw_only_lines_py2(kw_only_args): assert kw_only_args lines = ["try:"] lines.extend( - lines.append(" " + _unpack_kw_only_py2(*arg.split("="))) + " " + _unpack_kw_only_py2(*arg.split("=")) for arg in kw_only_args ) lines += """\ From c0e0e06b5a77e7923aafd85916519b2013e35b41 Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 11:22:26 +0300 Subject: [PATCH 19/24] Fixed style/added comment --- src/attr/_make.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index c6f6cbb44..172ca83cd 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1944,8 +1944,13 @@ def _unpack_kw_only_py2(attr_name, default=None): 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}={attr_default}" + generate a list of lines "{attr_name} = _kw_only.pop('{attr_name}')" + and "{attr_name} = _kw_only.pop('{attr_name}', {attr_default}). + If required attr is missing in _kw_only dict or extra key is passed + - generated code raises TypeError with TypeError similar to builtins. """ - assert kw_only_args lines = ["try:"] lines.extend( " " + _unpack_kw_only_py2(*arg.split("=")) @@ -1953,11 +1958,14 @@ def _unpack_kw_only_lines_py2(kw_only_args): ) lines += """\ except KeyError as _key_error: - _msg = '__init__() missing required keyword-only argument: %s' - raise TypeError(_msg % _key_error) + raise TypeError( + '__init__() missing required keyword-only argument: %s' % _key_error + ) if _kw_only: - _msg = '__init__() got an unexpected keyword argument %r' - raise TypeError(_msg % next(iter(_kw_only))) + raise TypeError( + '__init__() got an unexpected keyword argument %r' + % next(iter(_kw_only)) + ) """.split( "\n" ) From f775c7af708def3cdffb3798f4caca2961bc500d Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 11:22:40 +0300 Subject: [PATCH 20/24] Fixed docs (removed python2 mention) --- docs/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index 8497734fc..0fac312a0 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -146,7 +146,7 @@ On Python 3 it overrides the implicit detection. Keyword-only Attributes ~~~~~~~~~~~~~~~~~~~~~~~ -You can also add `keyword-only `_ attributes (even on Python 2): +You can also add `keyword-only `_ attributes: .. doctest:: From b5d04efab949291315506218b56608fa1383a33a Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 11:26:19 +0300 Subject: [PATCH 21/24] Fix lint --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 172ca83cd..b2edfcae4 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1963,7 +1963,7 @@ def _unpack_kw_only_lines_py2(kw_only_args): ) if _kw_only: raise TypeError( - '__init__() got an unexpected keyword argument %r' + '__init__() got an unexpected keyword argument %r' % next(iter(_kw_only)) ) """.split( From 80cbb003437b06da6c871000745e7baaecaf5335 Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 12:02:20 +0300 Subject: [PATCH 22/24] Better docstring --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index b2edfcae4..6735e4678 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1949,7 +1949,7 @@ def _unpack_kw_only_lines_py2(kw_only_args): generate a list of lines "{attr_name} = _kw_only.pop('{attr_name}')" and "{attr_name} = _kw_only.pop('{attr_name}', {attr_default}). If required attr is missing in _kw_only dict or extra key is passed - - generated code raises TypeError with TypeError similar to builtins. + - generated code raises TypeError similar to python builtin. """ lines = ["try:"] lines.extend( From e2ad8afb8a63e3fa10cc6c7da3d50e209ba6972a Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 12:46:03 +0300 Subject: [PATCH 23/24] Rewritten docstring and added example code --- src/attr/_make.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 6735e4678..68ad381a9 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1945,11 +1945,21 @@ 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}={attr_default}" - generate a list of lines "{attr_name} = _kw_only.pop('{attr_name}')" - and "{attr_name} = _kw_only.pop('{attr_name}', {attr_default}). - If required attr is missing in _kw_only dict or extra key is passed - - generated code raises TypeError similar to python builtin. + 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( From 89cfd5fcaaaed74b286c32e9da69a5ee9ddb4932 Mon Sep 17 00:00:00 2001 From: drinob Date: Fri, 16 Oct 2020 12:52:42 +0300 Subject: [PATCH 24/24] Added quotes --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 68ad381a9..35d6e864d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1945,7 +1945,7 @@ 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} + 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.