Skip to content

Commit 2122347

Browse files
authored
feat: add has_attribute conjecture
1 parent 384c562 commit 2122347

File tree

4 files changed

+153
-1
lines changed

4 files changed

+153
-1
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ Matching values.
7070
>>> assert "abc" == conjecture.equal_to("abc")
7171
```
7272

73+
Matching attributes.
74+
75+
```pycon
76+
>>> import conjecture
77+
>>> assert 1 == conjecture.has_attribute("__class__")
78+
>>> assert 1 == conjecture.has_attribute("__class__", of=int)
79+
>>> assert 1 == conjecture.has_attribute("__class__", of=conjecture.instance_of(type))
80+
```
81+
7382
#### Rich ordering
7483

7584
Matching lesser values.

src/conjecture/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from conjecture.base import AllOfConjecture, AnyOfConjecture, Conjecture
88
from conjecture.general import all_of, any_of, anything, has, none
9-
from conjecture.object import equal_to, instance_of
9+
from conjecture.object import equal_to, has_attribute, instance_of
1010
from conjecture.rich import (
1111
greater_than,
1212
greater_than_or_equal_to,
@@ -28,6 +28,7 @@
2828
"greater_than_or_equal_to",
2929
"greater_than",
3030
"has",
31+
"has_attribute",
3132
"instance_of",
3233
"length_of",
3334
"less_than_or_equal_to",

src/conjecture/object.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import conjecture.base
66

7+
sentinel = object()
8+
79

810
def instance_of(
911
value: typing.Union[tuple[type, ...], type]
@@ -33,3 +35,27 @@ def equal_to(value: object) -> conjecture.base.Conjecture:
3335
:return: a conjecture object
3436
"""
3537
return conjecture.base.Conjecture(lambda x: x == value)
38+
39+
40+
def has_attribute(value: str, of: object = sentinel) -> conjecture.base.Conjecture:
41+
"""
42+
Has attribute.
43+
44+
Propose that the value has the given attribute
45+
46+
>>> assert value == conjecture.has_attribute("foo")
47+
>>> assert value == conjecture.has_attribute("foo", of=5)
48+
>>> assert value == conjecture.has_attribute("foo", of=conjecture.less_than(10))
49+
50+
:param value: the name of the attribute
51+
:param of: an optional value or conjecture to compare the attribute value against
52+
53+
:return: a conjecture object
54+
"""
55+
# pylint: disable=invalid-name
56+
# of is a perfectly valid name.
57+
58+
if of == sentinel:
59+
return conjecture.base.Conjecture(lambda x: hasattr(x, value))
60+
61+
return conjecture.base.Conjecture(lambda x: getattr(x, value, sentinel) == of)

tests/unit/test_has_attribute.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
Tests for :meth:`conjecture.has_attribute`.
3+
"""
4+
5+
import dataclasses
6+
import keyword
7+
import string
8+
9+
import hypothesis
10+
import hypothesis.strategies as st
11+
12+
import conjecture
13+
14+
15+
def _not_keyword(value: str) -> bool:
16+
return not keyword.iskeyword(value)
17+
18+
19+
@hypothesis.given(
20+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
21+
other=st.integers(),
22+
)
23+
def test_should_match_when_attribute_exists(value: str, other: int) -> None:
24+
"""
25+
has_attribute() should match when attribute exists.
26+
"""
27+
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})
28+
29+
assert conjecture.has_attribute(value).resolve(obj)
30+
31+
32+
@hypothesis.given(
33+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
34+
key=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
35+
other=st.integers(),
36+
)
37+
def test_should_not_match_when_attribute_doesnt_exists(
38+
value: str,
39+
key: str,
40+
other: int,
41+
) -> None:
42+
"""
43+
has_attribute() should not match when attribute doesn't exist.
44+
"""
45+
hypothesis.assume(value != key)
46+
47+
obj = dataclasses.make_dataclass("mock", [(key, int)])(**{key: other})
48+
49+
assert not conjecture.has_attribute(value).resolve(obj)
50+
51+
52+
@hypothesis.given(
53+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
54+
other=st.integers(),
55+
)
56+
def test_should_match_when_attribute_value_matches(value: str, other: int) -> None:
57+
"""
58+
has_attribute() should match when attribute value matches.
59+
"""
60+
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})
61+
62+
assert conjecture.has_attribute(value, of=other).resolve(obj)
63+
64+
65+
@hypothesis.given(
66+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
67+
wrong=st.integers(),
68+
other=st.integers(),
69+
)
70+
def test_should_not_match_when_attribute_value_doesnt_match(
71+
value: str,
72+
wrong: int,
73+
other: int,
74+
) -> None:
75+
"""
76+
has_attribute() should not match when attribute value doesn't match.
77+
"""
78+
hypothesis.assume(wrong != other)
79+
80+
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: wrong})
81+
82+
assert not conjecture.has_attribute(value, of=other).resolve(obj)
83+
84+
85+
@hypothesis.given(
86+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
87+
other=st.integers(),
88+
)
89+
def test_should_match_when_attribute_value_matches_conjecture(
90+
value: str, other: int
91+
) -> None:
92+
"""
93+
has_attribute() should match when attribute value matches conjecture.
94+
"""
95+
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})
96+
97+
always = conjecture.has(lambda x: True)
98+
99+
assert conjecture.has_attribute(value, of=always).resolve(obj)
100+
101+
102+
@hypothesis.given(
103+
value=st.text(alphabet=string.ascii_letters, min_size=1).filter(_not_keyword),
104+
other=st.integers(),
105+
)
106+
def test_should_not_match_when_attribute_value_doesnt_match_conjecture(
107+
value: str, other: int
108+
) -> None:
109+
"""
110+
has_attribute() should not match when attribute value doesn't match conjecture.
111+
"""
112+
obj = dataclasses.make_dataclass("mock", [(value, int)])(**{value: other})
113+
114+
never = conjecture.has(lambda x: False)
115+
116+
assert not conjecture.has_attribute(value, of=never).resolve(obj)

0 commit comments

Comments
 (0)