hamcrest / PyHamcrest

Hamcrest matchers for Python
http://hamcrest.org/
Other
766 stars 111 forks source link

object comparison override #216

Closed priamai closed 1 year ago

priamai commented 2 years ago

Hi there, I am new to Hamcrest and would like to know if there is an example to override the standard object comparison equal operator. I basically have to change the list comparison for the properties of the object otherwise the default one is order dependent. Cheers.

brunns commented 2 years ago

You question isn't really clear, I'm afraid. Most matchers can take nested matchers in addition to literal values, so I expect what you want is possible, but I'd need a little more information to help. Do you have an object whose property is a list, or is list of objects with properties?

This might be better asked on Stack Overflow.

priamai commented 2 years ago

Hello @brunns, yes apologies, let me do a concrete example to explain. I have an object type that has an inner attribute of type dict.

I just want to be able to compare the internal representation (it's a dict in a property called _inner) and nothing else to assume that two objects are equal.

Right now I do this:

assert_that(stix_obj, equal_to(return_obj))

The STIX object looks like this

image

The test fails with this two pairs of objects:

{"type": "intrusion-set", "spec_version": "2.1", "id": "intrusion-set--ed69450a-f067-4b51-9ba2-c4616b9a6713", "created": "2016-08-08T15:50:10.983Z", "modified": "2016-08-08T15:50:10.983Z", "name": "APT BPP", "description": "An advanced persistent threat that seeks to disrupt Branistan's election with multiple attacks.", "aliases": ["Bran-teaser"], "first_seen": "2016-01-08T12:50:40.123Z", "goals": ["Disrupt the BPP", "Influence the Branistan election"], "resource_level": "government", "primary_motivation": "ideology", "secondary_motivations": ["dominance"]}

and:

{"type": "intrusion-set", "spec_version": "2.1", "id": "intrusion-set--ed69450a-f067-4b51-9ba2-c4616b9a6713", "created": "2016-08-08T15:50:10.983Z", "modified": "2016-08-08T15:50:10.983Z", "name": "APT BPP", "description": "An advanced persistent threat that seeks to disrupt Branistan's election with multiple attacks.", "aliases": ["Bran-teaser"], "first_seen": "2016-01-08T12:50:40.123Z", "goals": ["Influence the Branistan election", "Disrupt the BPP"], "resource_level": "government", "primary_motivation": "ideology", "secondary_motivations": ["dominance"]}

This is because the dictionary property called goals can have a different order of strings.

So ideally I would like to create my own comparator that only considers the dict internal representation (_inner field) and then I guess based on the value types (like the list example) ignore the order.

I hope this makes sense now.

brunns commented 1 year ago

You can compose a custom matcher for this fairly easily:

import collections
from dataclasses import dataclass
from typing import Any, Dict

from hamcrest import assert_that, contains_inanyorder, has_entries, has_property
from hamcrest.core.matcher import Matcher

@dataclass
class Stix:
    _inner: Dict

def is_nonstring_sequence(candidate: Any) -> bool:
    return not isinstance(candidate, str) and isinstance(candidate, collections.abc.Sequence)

def equal_to_stix(expected: Stix) -> Matcher[Stix]:
    return has_property(
        "_inner",
        has_entries(
            **{
                k: (contains_inanyorder(*v) if is_nonstring_sequence(v) else v)
                for k, v in expected._inner.items()
            }
        ),
    )

def test_stix_comp():
    actual = Stix(
        {
            "type": "intrusion-set",
            "spec_version": "2.1",
            "id": "intrusion-set--ed69450a-f067-4b51-9ba2-c4616b9a6713",
            "created": "2016-08-08T15:50:10.983Z",
            "modified": "2016-08-08T15:50:10.983Z",
            "name": "APT BPP",
            "description": "An advanced persistent threat that seeks to disrupt Branistan's election with multiple attacks.",
            "aliases": ["Bran-teaser"],
            "first_seen": "2016-01-08T12:50:40.123Z",
            "goals": ["Disrupt the BPP", "Influence the Branistan election"],
            "resource_level": "government",
            "primary_motivation": "ideology",
            "secondary_motivations": ["dominance"],
        }
    )
    expected = Stix(
        {
            "type": "intrusion-set",
            "spec_version": "2.1",
            "id": "intrusion-set--ed69450a-f067-4b51-9ba2-c4616b9a6713",
            "created": "2016-08-08T15:50:10.983Z",
            "modified": "2016-08-08T15:50:10.983Z",
            "name": "APT BPP",
            "description": "An advanced persistent threat that seeks to disrupt Branistan's election with multiple attacks.",
            "aliases": ["Bran-teaser"],
            "first_seen": "2016-01-08T12:50:40.123Z",
            "goals": ["Influence the Branistan election", "Disrupt the BPP"],
            "resource_level": "government",
            "primary_motivation": "ideology",
            "secondary_motivations": ["dominance"],
        }
    )

    assert_that(actual, equal_to_stix(expected))
brunns commented 1 year ago

Assuming this does the trick.