Skip to content

Commit

Permalink
feat: add has_attribute conjecture
Browse files Browse the repository at this point in the history
  • Loading branch information
danielknell authored Jun 22, 2022
1 parent 384c562 commit 2122347
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 1 deletion.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ Matching values.
>>> assert "abc" == conjecture.equal_to("abc")
```

Matching attributes.

```pycon
>>> import conjecture
>>> assert 1 == conjecture.has_attribute("__class__")
>>> assert 1 == conjecture.has_attribute("__class__", of=int)
>>> assert 1 == conjecture.has_attribute("__class__", of=conjecture.instance_of(type))
```

#### Rich ordering

Matching lesser values.
Expand Down
3 changes: 2 additions & 1 deletion src/conjecture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from conjecture.base import AllOfConjecture, AnyOfConjecture, Conjecture
from conjecture.general import all_of, any_of, anything, has, none
from conjecture.object import equal_to, instance_of
from conjecture.object import equal_to, has_attribute, instance_of
from conjecture.rich import (
greater_than,
greater_than_or_equal_to,
Expand All @@ -28,6 +28,7 @@
"greater_than_or_equal_to",
"greater_than",
"has",
"has_attribute",
"instance_of",
"length_of",
"less_than_or_equal_to",
Expand Down
26 changes: 26 additions & 0 deletions src/conjecture/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import conjecture.base

sentinel = object()


def instance_of(
value: typing.Union[tuple[type, ...], type]
Expand Down Expand Up @@ -33,3 +35,27 @@ def equal_to(value: object) -> conjecture.base.Conjecture:
:return: a conjecture object
"""
return conjecture.base.Conjecture(lambda x: x == value)


def has_attribute(value: str, of: object = sentinel) -> conjecture.base.Conjecture:
"""
Has attribute.
Propose that the value has the given attribute
>>> assert value == conjecture.has_attribute("foo")
>>> assert value == conjecture.has_attribute("foo", of=5)
>>> assert value == conjecture.has_attribute("foo", of=conjecture.less_than(10))
:param value: the name of the attribute
:param of: an optional value or conjecture to compare the attribute value against
:return: a conjecture object
"""
# pylint: disable=invalid-name
# of is a perfectly valid name.

if of == sentinel:
return conjecture.base.Conjecture(lambda x: hasattr(x, value))

return conjecture.base.Conjecture(lambda x: getattr(x, value, sentinel) == of)
116 changes: 116 additions & 0 deletions tests/unit/test_has_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Tests for :meth:`conjecture.has_attribute`.
"""

import dataclasses
import keyword
import string

import hypothesis
import hypothesis.strategies as st

import conjecture


def _not_keyword(value: str) -> bool:
return not keyword.iskeyword(value)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
other=st.integers(),
)
def test_should_match_when_attribute_exists(value: str, other: int) -> None:
"""
has_attribute() should match when attribute exists.
"""
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})

assert conjecture.has_attribute(value).resolve(obj)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
key=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
other=st.integers(),
)
def test_should_not_match_when_attribute_doesnt_exists(
value: str,
key: str,
other: int,
) -> None:
"""
has_attribute() should not match when attribute doesn't exist.
"""
hypothesis.assume(value != key)

obj = dataclasses.make_dataclass("mock", [(key, int)])(**{key: other})

assert not conjecture.has_attribute(value).resolve(obj)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
other=st.integers(),
)
def test_should_match_when_attribute_value_matches(value: str, other: int) -> None:
"""
has_attribute() should match when attribute value matches.
"""
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})

assert conjecture.has_attribute(value, of=other).resolve(obj)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
wrong=st.integers(),
other=st.integers(),
)
def test_should_not_match_when_attribute_value_doesnt_match(
value: str,
wrong: int,
other: int,
) -> None:
"""
has_attribute() should not match when attribute value doesn't match.
"""
hypothesis.assume(wrong != other)

obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: wrong})

assert not conjecture.has_attribute(value, of=other).resolve(obj)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
other=st.integers(),
)
def test_should_match_when_attribute_value_matches_conjecture(
value: str, other: int
) -> None:
"""
has_attribute() should match when attribute value matches conjecture.
"""
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})

always = conjecture.has(lambda x: True)

assert conjecture.has_attribute(value, of=always).resolve(obj)


@hypothesis.given(
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
other=st.integers(),
)
def test_should_not_match_when_attribute_value_doesnt_match_conjecture(
value: str, other: int
) -> None:
"""
has_attribute() should not match when attribute value doesn't match conjecture.
"""
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})

never = conjecture.has(lambda x: False)

assert not conjecture.has_attribute(value, of=never).resolve(obj)

0 comments on commit 2122347

Please sign in to comment.