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

[1.27.0] Tests for custom data types #428

Merged
merged 38 commits into from
Dec 27, 2022
Merged

Conversation

Karrenbelt
Copy link

@Karrenbelt Karrenbelt commented Nov 5, 2022

Proposed changes

Tests that showcase issues with custom data types that implement their own __eq__ and __lt__ methods.

In furtherance of deterministic config serialization - #359

There are several issues with the implementation of __dunder__ methods in our code base. Here we showcase those with respect to comparison operators: __eq__, __ne__, __lt__, __le__, __gt__, __ge__.

def __lt__(self, other: Any) -> bool:
"""Compare with another object."""
enforce(
isinstance(other, PackageVersion),
f"Cannot compare {type(self)} with type {type(other)}.",
)
other = cast(PackageVersion, other)
if self.is_latest or other.is_latest:
return self.is_latest < other.is_latest
return str(self) < str(other)

def __lt__(self, other: Any) -> bool:
"""
Compare two public ids.
>>> public_id_1 = PublicId("author_1", "name_1", "0.1.0")
>>> public_id_2 = PublicId("author_1", "name_1", "0.1.1")
>>> public_id_3 = PublicId("author_1", "name_2", "0.1.0")
>>> public_id_1 > public_id_2
False
>>> public_id_1 < public_id_2
True
>>> public_id_1 < public_id_3
Traceback (most recent call last):
...
ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different.
:param other: the object to compate to
:raises ValueError: if the public ids cannot be confirmed
:return: whether or not the inequality is satisfied
"""
if (
isinstance(other, PublicId)
and self.author == other.author
and self.name == other.name
):
return self.package_version < other.package_version
raise ValueError(
"The public IDs {} and {} cannot be compared. Their author or name attributes are different.".format(
self, other
)
)

def __lt__(self, other: Any) -> bool:
"""Compare two public ids."""
return str(self) < str(other)

  • Expected failures:
    -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
    ============================================================================================= short test summary info =============================================================================================
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[list0-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_self_type_comparison[PackageId] - TypeError: '<=' not supported between instances of 'PackageId' and 'PackageId'
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[tuple-PackageId] - assert False
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[PackageId-PublicId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[str-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_public_id_comparison - TypeError: '<=' not supported between instances of 'PublicId' and 'PublicId'
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[float-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[complex-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[int-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_package_version_comparison - assert False
    FAILED tests/test_configurations/test_data_types.py::test_self_type_comparison[Dependency] - TypeError: '<' not supported between instances of 'Dependency' and 'Dependency'
    FAILED tests/test_configurations/test_data_types.py::test_self_type_comparison[PublicId] - ValueError: The public IDs C/A:0.0.0:Qm00000000000000000000000000000000000000000000 and J/A:0.0.0:Qm00000000000000000...
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[list1-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[PackageId-Dependency] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[zip-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[Dependency-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[PublicId-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    FAILED tests/test_configurations/test_data_types.py::test_package_id_comparison - TypeError: '<=' not supported between instances of 'PackageId' and 'PackageId'
    FAILED tests/test_configurations/test_data_types.py::test_type_comparison[map-PackageId] - Failed: DID NOT RAISE (<class 'TypeError'>, <class 'ValueError'>)
    

Issues

  1. Stricter equality comparison is needed: abolish the use of string casting for comparsion in __lt__.
    Use VersionInfo for everything relating to comparisons in PackageVersion, PublicId and PackageId. It has many useful features implemented for us.
  2. functools.total_ordering missing on PublicId and PackageId.
  3. We should not return False if other is not an instance of type(self) in __eq__ and __lt__ operators. We should opt to raise when comparing apples to oranges.
    • We do this for PackageVersion.__lt__ and PublicId.__lt__, but not in PackageId.__lt__.
    • We don't deflect in __eq__ methods.
  4. PackageId.__lt__ is most problematic. Not only does it compare the string representation of PublicId, containing a string representation of the version, but it also compares against the string representation of any arbitrary object. It should raise an error. We should deal with sorting necessary for serialization in the serialization process.

Suggested changes:

  • Delegate all comparison to semver.VersionInfo. __eq__ and __lt__ must first check for "any" and "latest" in __lt__: if self.version_like == other.version_like: return False else self.version_like < other.version_like
  • PublicId.version should be a property returning an instance of semver.VersionInfo
  • __eq__ should deflect when not isinstance(other, type(self)) by returning NotImplemented (see #383).
  • PackageVersion.__lt__ should be / rely on PackageVersion._version (which should be an instance of semver.VersionInfo)
  • PublicId.__lt__ should raise on incoherent type, author or name, otherwise delegate to semver.VersionInfo
  • PackageId.__lt__ should raise on incoherent type or package_type, otherwise delegate to PublicId
  • add the functools.total_ordering decorator to PublicId and PackageId.
  • Sorting for deterministic serialization can be achieved via json.
    • PackageVersion.json: return self.version_like.to_tuple() # or a nasty "any" / "latest" string
    • PublicId.json: return dict(author=..., name=..., version=self.package_version.json, package_hash=...)
    • PackageId.json: return dict(package_type=..., **self.public_id.json)

This would constitute a breaking change. The usage of "any" and "latest" complicates matters unnecessarily, we may consider changing this prior to next major release.

Types of changes

What types of changes does your code introduce to agents-aea?
Put an x in the boxes that apply

  • Bugfix (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)

Checklist

Put an x in the boxes that apply.

  • I have read the CONTRIBUTING doc
  • I am making a pull request against the develop branch (left side). Also you should start your branch off our develop.
  • Lint and unit tests pass locally with my changes and CI passes too
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that code coverage does not decrease.
  • I have added necessary documentation (if appropriate)
  • Any dependent changes have been merged and published in downstream modules

Further comments

If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...

Comment on lines +303 to +305
[mypy-hypothesis.*]
ignore_missing_imports = True

Copy link
Author

Choose a reason for hiding this comment

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

equal to setup in open-autonomy setup.cfg

@codecov-commenter
Copy link

codecov-commenter commented Nov 5, 2022

Codecov Report

Base: 90.29% // Head: 90.29% // Increases project coverage by +0.00% 🎉

Coverage data is based on head (8ed2410) compared to base (8c684c4).
Patch coverage: 94.23% of modified lines in pull request are covered.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #428   +/-   ##
=======================================
  Coverage   90.29%   90.29%           
=======================================
  Files         352      352           
  Lines       28700    28721   +21     
=======================================
+ Hits        25916    25935   +19     
- Misses       2784     2786    +2     
Flag Coverage Δ
unittests 90.29% <94.23%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
aea/configurations/data_types.py 99.18% <93.02%> (-0.82%) ⬇️
aea/cli/check_packages.py 96.81% <100.00%> (ø)
aea/helpers/base.py 100.00% <100.00%> (ø)
aea/helpers/dependency_tree.py 100.00% <100.00%> (ø)
plugins/aea-cli-ipfs/aea_cli_ipfs/ipfs_utils.py 100.00% <0.00%> (+0.58%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

Comment on lines +73 to +80
s = st.fixed_dictionaries(
dict(
author=st.from_type(SimpleId),
name=st.from_type(SimpleId),
version=st.from_type(semver.VersionInfo),
package_hash=st.from_type(IPFSHash),
)
)
Copy link
Author

@Karrenbelt Karrenbelt Nov 5, 2022

Choose a reason for hiding this comment

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

not all mypy types are covered in the strategies listed here. E.g. in case of SimpleIdOrStr: Union[SimpleId, str], a string should adhere to the pattern of SimpleId or will otherwise be invalid.

Comment on lines +98 to +100
for f in funcs:
with pytest.raises((TypeError, ValueError)):
assert f(self, other)
Copy link
Author

Choose a reason for hiding this comment

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

some raise custom ValueError, I suggest raise TypeError on all instead.

Comment on lines +121 to +128
@given(st.tuples(package_id_strategy, package_id_strategy))
def test_package_id_comparison(package_id_pair):
"""Test package id comparison"""

package_id_pair[0]._public_id = package_id_pair[1]._public_id
package_id_pair[0]._package_type = package_id_pair[1]._package_type
version_pair = tuple(p.public_id.package_version._version for p in package_id_pair)
assert all_comparisons_operations_equal(version_pair, package_id_pair)
Copy link
Author

Choose a reason for hiding this comment

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

this seems to me the desired type of behaviour.

Comment on lines +42 to +57
positive_integer_strategy = st.integers(min_value=0)
user_string_pattern = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]{0,127}")
user_string_strategy = st.from_regex(user_string_pattern, fullmatch=True)
simple_id_strategy = st.from_regex(SimpleId.REGEX, fullmatch=True)
ipfs_hash_strategy = st.from_regex(IPFSHash.REGEX, fullmatch=True)
pypi_package_name_strategy = st.from_regex(PyPIPackageName.REGEX, fullmatch=True)
gitref_strategy = st.from_regex(GitRef.REGEX, fullmatch=True)
specifier_strategy = st.from_regex(Specifier._regex, fullmatch=True)


st.register_type_strategy(collections.UserString, user_string_strategy)
st.register_type_strategy(SimpleId, simple_id_strategy)
st.register_type_strategy(IPFSHash, ipfs_hash_strategy)
st.register_type_strategy(PyPIPackageName, pypi_package_name_strategy)
st.register_type_strategy(GitRef, gitref_strategy)
st.register_type_strategy(Specifier, specifier_strategy)
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

@Adamantios Adamantios left a comment

Choose a reason for hiding this comment

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

LGTM, let's stack a fix PR.

@Karrenbelt
Copy link
Author

TODO: introduce Comparable type annotation, see: python/typing#59

@Karrenbelt Karrenbelt changed the title Tests/test data types [1.25.0] Tests for custom data types Nov 17, 2022
@Karrenbelt Karrenbelt marked this pull request as ready for review December 18, 2022 16:37
0xArdi
0xArdi previously approved these changes Dec 18, 2022
solarw
solarw previously approved these changes Dec 19, 2022
Copy link
Collaborator

@solarw solarw left a comment

Choose a reason for hiding this comment

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

lgtm
coool!

…nLike

Fix/delegate `__lt__` to `PackageVersionLike`
Add deflection to custom `__lt__` and add `functools.total_ordering`
Updates `__eq__` implementation on the data types
@DavidMinarsch DavidMinarsch dismissed stale reviews from solarw and 0xArdi via 4a8fa40 December 19, 2022 20:56
@Karrenbelt Karrenbelt changed the title [WIP] Tests for custom data types [1.27.0] Tests for custom data types Dec 27, 2022
@property
def is_any(self) -> bool:
"""Check whether the version is 'any'."""
return isinstance(self._version, str) and self._version == "any"
Copy link
Collaborator

Choose a reason for hiding this comment

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

check for instance is done cause will fail on Version() == str() ?

Copy link
Collaborator

@solarw solarw left a comment

Choose a reason for hiding this comment

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

lgtm

@DavidMinarsch DavidMinarsch merged commit ec9225a into main Dec 27, 2022
@DavidMinarsch DavidMinarsch deleted the tests/test_data_types branch December 28, 2022 17:36
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.

6 participants