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

Use kw_only=True parameter in dataclass module #673

Merged
merged 28 commits into from
Jun 25, 2024
Merged

Use kw_only=True parameter in dataclass module #673

merged 28 commits into from
Jun 25, 2024

Conversation

ckeshava
Copy link
Collaborator

High Level Overview of Change

Introduces the use of kw_only=True parameter in the dataclass module. It preserves backwards compatibility for older versions of Python, because this feature is enabled in Python v3.10 or later versions

Context of Change

The use of @require_kwargs_on_init decorator prevents IDE's from providing auto-complete suggestions. The newer versions of Python standard library allow developers to solve this issue using kw_only=True in the constructor of dataclass. Reference documentation: https://docs.python.org/3.10/library/dataclasses.html#module-contents

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)
  • Documentation Updates
  • Release

Did you update CHANGELOG.md?

  • Yes
  • No, this change does not impact library users

This change is based on the suggestion in this Stackoverflow page: https://stackoverflow.com/questions/72733998/kw-only-and-slots-dataclass-compatibility-with-older-versions-of-python

Thanks for the tip @mvadari !

…rds compatibility for older versions of Python, this feature is enabled in Python v3.10 or later versions
@mvadari
Copy link
Collaborator

mvadari commented Jan 16, 2024

Does this preserve the error message in 3.7?

@ckeshava
Copy link
Collaborator Author

Yes, developers will get an error like:

----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/ckeshavabs/xrpl-py/tests/unit/models/requests/test_channel_authorize.py", line 13, in test_has_secret_only_is_valid
    request = ChannelAuthorize(
  File "/Users/ckeshavabs/xrpl-py/xrpl/models/utils.py", line 59, in new_init
    raise XRPLModelException(
xrpl.models.exceptions.XRPLModelException: ChannelAuthorize.__init__ only allows keyword arguments. Found the following positional arguments: (20,)

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (errors=1)
➜  xrpl-py git:(kwOnly) ✗ python3 --version
Python 3.7.17
➜  xrpl-py git:(kwOnly) ✗ 

@mvadari
Copy link
Collaborator

mvadari commented Jan 19, 2024

Please add a unit test to confirm that an error is thrown in previous versions.

@ckeshava
Copy link
Collaborator Author

@mvadari 👍
I've added unit tests to Sign and ChannelAuthorize classes: tests/unit/models/requests/test_channel_authorize.py and tests/unit/models/requests/test_sign.py

fixed in commit: b0d0ffa

@ckeshava
Copy link
Collaborator Author

ckeshava commented Feb 8, 2024

@mvadari I have added the unit tests, would you be able to check this PR again?

xrpl/models/utils.py Outdated Show resolved Hide resolved
@JST5000
Copy link
Contributor

JST5000 commented Feb 12, 2024

I would add this to the HISTORY.md since the intent is to make auto-complete work with IDEs (which will impact developers)

@@ -56,3 +56,14 @@ def test_has_multiple_signing_methods_is_invalid(self):
passphrase=_DUMMY_STRING,
seed_hex=_DUMMY_STRING,
)

def test_throws_if_positional_args_mixed_with_non_positional_args(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted a unit test that shows that positional arguments are not permitted in the constructor of ledger objects. that was my intention

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 0cbc07e
let me know if that looks ok

@@ -72,3 +72,13 @@ def test_valid_seed(self):
transaction=_TRANSACTION, seed=_SEED, key_type=CryptoAlgorithm.SECP256K1
)
self.assertTrue(request.is_valid())

def test_ctor_has_positional_args(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what does ctor mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i shortened constructor to ctor. i can use the full name

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in 0cbc07e.
Let me know if that looks ok

CHANGELOG.md Outdated Show resolved Hide resolved
@@ -11,3 +32,24 @@ class TestUtils(TestCase):
def test_kwargs_req(self):
with self.assertRaises(XRPLModelException):
IssuedCurrency(currency, issuer)

def test_throws_if_positional_args_mixed_with_non_positional_args(self):
with self.assertRaises(XRPLModelException):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why this works with >=3.10 - shouldn't it be throwing a different error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thanks for pointing this out. I had forgotten to remove the decorator @required_kw_args. It was falling back on the old code. I'll update this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old decorator should still be used in some form, to keep support for the error for <3.10.


class TestUtils(TestCase):
def test_kwargs_req(self):
with self.assertRaises(XRPLModelException):
IssuedCurrency(currency, issuer)
if sys.version_info.major == 3 and sys.version_info.minor < 10:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvadari I have updated the unit tests to depend on the version of Python

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend writing a helper function to do this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, for consistency (and in case they backport the changes), I'd recommend doing the same check you do in KW_ONLY_DATACLASS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

# noinspection PyTypeHints
cls.__init__ = new_init # type: ignore

# For Python v3.10 and above, the KW_ONLY attribute in data_class
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvadari I came up with this conditional statement to accomodate different versions of python. Does this look good? Do you have any other ideas in mind?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency (and in case they backport the changes), I'd recommend doing the same check you do in KW_ONLY_DATACLASS

def test_positional_args_in_model_constructor_throws(self):
if sys.version_info.major == 3 and sys.version_info.minor < 10:
with self.assertRaises(XRPLModelException):
Sign(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a different RPC for this, since Sign may be deprecated in the library at some point (it's really only used with an admin connection).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 32 to 37
if is_kw_only_attr_defined_in_dataclass():
with self.assertRaises(XRPLModelException):
IssuedCurrency(currency, issuer)
else:
with self.assertRaises(TypeError):
IssuedCurrency(currency, issuer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend putting this if-else block in a reusable context manager to avoid needing it in every test/avoid repetition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by a re-usable context manager? Do you mean something like:

def testWrapper(pythonVersionDiscriminator, oldVersion, newVersion):
    if pythonVersionDiscriminator():
        <some steps in the unit tests>
        <raise XRPLModelException>
    else:
        <some more steps>
        <raise TypeError>

?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the resource. let me know if my usage looks nice.

@@ -73,7 +74,8 @@ def _value_to_json(value: XRPL_VALUE_TYPE) -> XRPL_VALUE_TYPE:
return value


@dataclass(frozen=True)
# @require_kwargs_on_init
@dataclass(frozen=True, **KW_ONLY_DATACLASS)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does BaseModel need this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed it 👍

xrpl/models/utils.py Outdated Show resolved Hide resolved
# Code source for requiring kwargs:
# https://gist.github.com/mikeholler/4be180627d3f8fceb55704b729464adb

_T = TypeVar("_T")
_Self = TypeVar("_Self")


def is_kw_only_attr_defined_in_dataclass() -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a private method. It's fine to still use it in tests.


# Unit tests that validate this behavior can be found at test_channel_authorize.py
# and test_sign.py files.
KW_ONLY_DATACLASS = dict(kw_only=True) if "kw_only" in dataclass.__kwdefaults__ else {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the helper function is_kw_only_attr_defined_in_dataclass.

# When support for older versions of Python (earlier than v3.10) is removed, the
# usage of require_kwargs_on_init decorator on model classes can also be removed.
if is_kw_only_attr_defined_in_dataclass():
# noinspection PyTypeHints
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this comment for? I don't think PyTypeHints is something this library uses.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I was trying to preserve the existing code. #noinspection PyTypeHints is also used in the present code base. I've removed it in the latest tip of this branch 👍

Copy link
Contributor

@JST5000 JST5000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@ckeshava
Copy link
Collaborator Author

@mvadari could you take a look at this PR? do you suggest any other changes?

@ckeshava
Copy link
Collaborator Author

@khancode @justinr1234 could you guys take a look at this PR at your convinience?

@ckeshava ckeshava requested a review from mvadari April 8, 2024 20:08
@ckeshava
Copy link
Collaborator Author

ckeshava commented May 8, 2024

Please let me know if there are any objections. I'd like to merge this PR

Copy link
Collaborator

@khancode khancode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ckeshava ckeshava merged commit 4779d79 into main Jun 25, 2024
21 checks passed
@ckeshava ckeshava deleted the kwOnly branch June 25, 2024 17:51
ckeshava added a commit to ckeshava/xrpl-py that referenced this pull request Jul 1, 2024
use the kw_only=True parameter in the dataclass module. Ensure backwards compatibility for older versions of Python, this feature is enabled in Python v3.10 or later versions

---------

Co-authored-by: Jackson Mills <jmills@ripple.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants