Skip to content

Commit 8f691aa

Browse files
committed
Update StringField
1 parent c101918 commit 8f691aa

2 files changed

Lines changed: 75 additions & 112 deletions

File tree

src/flowmapper/string_field.py

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,23 @@
1-
from typing import Any, Generic, TypeVar, Self
1+
from typing import Any, Self
2+
from collections import UserString
23

34
from flowmapper.utils import normalize_str
45

5-
SF = TypeVar("SF")
66

7-
8-
class StringField(Generic[SF]):
9-
def __init__(
10-
self,
11-
value: str,
12-
use_lowercase: bool = True,
13-
):
14-
self.value = value
15-
self.use_lowercase = use_lowercase
16-
17-
def normalize(self) -> Self:
18-
value = normalize_str(self.value)
19-
if self.use_lowercase:
7+
class StringField(UserString):
8+
def normalize(self, lowercase: bool = True) -> Self:
9+
value = normalize_str(self.data)
10+
if lowercase:
2011
value = value.lower()
21-
return StringField(value)
12+
return type(self)(value)
2213

2314
def __eq__(self, other: Any) -> bool:
24-
if self.value == "":
15+
if not self.data:
16+
# Empty strings aren't equal for our use case
2517
return False
2618
elif isinstance(other, StringField):
27-
return (
28-
self.value == other.value
29-
)
19+
return self.data == other.data
3020
elif isinstance(other, str):
31-
if self.use_lowercase:
32-
return self.value == normalize_str(other).lower()
33-
else:
34-
return self.value == normalize_str(other)
21+
return self.data == other or self.data == normalize_str(other)
3522
else:
3623
return False
37-
38-
def __bool__(self) -> bool:
39-
return bool(self.value)
40-
41-
def __repr__(self) -> str:
42-
if not self.value:
43-
return "StringField with missing value"
44-
else:
45-
return f"StringField: '{self.value}'"

tests/unit/test_string_field.py

Lines changed: 64 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Unit tests for StringField class."""
22

3-
import pytest
43

54
from flowmapper.string_field import StringField
65

@@ -11,24 +10,33 @@ class TestStringFieldInitialization:
1110
def test_init_with_value(self):
1211
"""Test initialization with a value."""
1312
sf = StringField("test")
14-
assert sf.value == "test", f"Expected sf.value to be 'test', but got {sf.value!r}"
15-
assert sf.use_lowercase is True, f"Expected sf.use_lowercase to be True, but got {sf.use_lowercase}"
16-
17-
def test_init_with_value_and_use_lowercase_false(self):
18-
"""Test initialization with use_lowercase=False."""
19-
sf = StringField("TEST", use_lowercase=False)
20-
assert sf.value == "TEST", f"Expected sf.value to be 'TEST', but got {sf.value!r}"
21-
assert sf.use_lowercase is False, f"Expected sf.use_lowercase to be False, but got {sf.use_lowercase}"
13+
assert sf == "test", f"Expected sf to equal 'test', but got {sf!r}"
14+
from collections import UserString
15+
assert isinstance(sf, UserString), f"Expected sf to be an instance of UserString, but got {type(sf)}"
16+
assert not isinstance(sf, str), f"Expected sf to not be an instance of str (UserString is not a subclass), but got {type(sf)}"
2217

2318
def test_init_with_empty_string(self):
2419
"""Test initialization with empty string."""
2520
sf = StringField("")
26-
assert sf.value == "", f"Expected sf.value to be '', but got {sf.value!r}"
21+
# Empty StringField doesn't equal empty string due to __eq__ implementation
22+
assert sf != "", f"Expected sf to not equal '', but they are equal (sf={sf!r})"
23+
assert sf.data == "", f"Expected sf.data to be '', but got {sf.data!r}"
2724

2825
def test_init_with_whitespace(self):
2926
"""Test initialization with whitespace."""
3027
sf = StringField(" test ")
31-
assert sf.value == " test ", f"Expected sf.value to be ' test ', but got {sf.value!r}"
28+
# Equality normalizes the other string, so " test " becomes "test"
29+
assert sf == " test ", f"Expected sf to equal ' test ', but got {sf!r}"
30+
assert sf.data == " test ", f"Expected sf.data to be ' test ', but got {sf.data!r}"
31+
32+
def test_inherits_from_userstring(self):
33+
"""Test that StringField inherits from UserString."""
34+
sf = StringField("test")
35+
from collections import UserString
36+
assert isinstance(sf, UserString), f"Expected sf to be an instance of UserString, but got {type(sf)}"
37+
assert issubclass(StringField, UserString), "Expected StringField to be a subclass of UserString, but it is not"
38+
# UserString is not a subclass of str
39+
assert not isinstance(sf, str), f"Expected sf to not be an instance of str (UserString is not a subclass), but got {type(sf)}"
3240

3341

3442
class TestStringFieldNormalize:
@@ -38,28 +46,27 @@ def test_normalize_with_lowercase_default(self):
3846
"""Test normalize with default lowercase=True."""
3947
sf = StringField("TEST")
4048
normalized = sf.normalize()
41-
assert normalized.value == "test", f"Expected normalized.value to be 'test', but got {normalized.value!r}"
42-
assert normalized.use_lowercase is True, f"Expected normalized.use_lowercase to be True, but got {normalized.use_lowercase}"
49+
assert normalized == "test", f"Expected normalized to equal 'test', but got {normalized!r}"
50+
assert isinstance(normalized, StringField), f"Expected normalized to be a StringField instance, but got {type(normalized)}"
4351

4452
def test_normalize_with_lowercase_false(self):
45-
"""Test normalize with use_lowercase=False."""
46-
sf = StringField("TEST", use_lowercase=False)
47-
normalized = sf.normalize()
48-
assert normalized.value == "TEST", f"Expected normalized.value to be 'TEST', but got {normalized.value!r}"
49-
assert normalized.use_lowercase is False, f"Expected normalized.use_lowercase to be False, but got {normalized.use_lowercase}"
53+
"""Test normalize with lowercase=False."""
54+
sf = StringField("TEST")
55+
normalized = sf.normalize(lowercase=False)
56+
assert normalized == "TEST", f"Expected normalized to equal 'TEST', but got {normalized!r}"
5057

5158
def test_normalize_with_whitespace(self):
5259
"""Test normalize with whitespace."""
5360
sf = StringField(" test ")
5461
normalized = sf.normalize()
55-
assert normalized.value == "test", f"Expected normalized.value to be 'test', but got {normalized.value!r}"
62+
assert normalized == "test", f"Expected normalized to equal 'test', but got {normalized!r}"
5663

5764
def test_normalize_returns_new_instance(self):
5865
"""Test that normalize returns a new instance."""
5966
sf = StringField("TEST")
6067
normalized = sf.normalize()
6168
assert normalized is not sf, "Expected normalize() to return a new instance, but it returned the same instance"
62-
assert sf.value == "TEST", f"Expected original sf.value to remain 'TEST', but got {sf.value!r}"
69+
assert sf == "TEST", f"Expected original sf to remain 'TEST', but got {sf!r}"
6370

6471

6572
class TestStringFieldEq:
@@ -77,17 +84,11 @@ def test_eq_with_different_stringfield(self):
7784
sf2 = StringField("other")
7885
assert sf1 != sf2, f"Expected sf1 to not equal sf2, but they are equal (sf1={sf1!r}, sf2={sf2!r})"
7986

80-
def test_eq_with_string_lowercase(self):
81-
"""Test equality with string when use_lowercase=True."""
82-
sf = StringField("TEST", use_lowercase=True)
87+
def test_eq_with_string(self):
88+
"""Test equality with string."""
89+
sf = StringField("test")
8390
assert sf == "test", f"Expected sf to equal 'test', but they are not equal (sf={sf!r})"
84-
assert sf == "TEST", f"Expected sf to equal 'TEST', but they are not equal (sf={sf!r})"
85-
86-
def test_eq_with_string_no_lowercase(self):
87-
"""Test equality with string when use_lowercase=False."""
88-
sf = StringField("TEST", use_lowercase=False)
89-
assert sf == "TEST", f"Expected sf to equal 'TEST', but they are not equal (sf={sf!r})"
90-
assert sf != "test", f"Expected sf to not equal 'test', but they are equal (sf={sf!r})"
91+
assert sf != "other", f"Expected sf to not equal 'other', but they are equal (sf={sf!r})"
9192

9293
def test_eq_with_empty_stringfield(self):
9394
"""Test equality with empty StringField."""
@@ -102,78 +103,51 @@ def test_eq_with_other_type(self):
102103
assert sf != None, f"Expected sf to not equal None, but they are equal (sf={sf!r})"
103104
assert sf != [], f"Expected sf to not equal [], but they are equal (sf={sf!r})"
104105

105-
def test_eq_with_stringfield_different_lowercase_setting(self):
106-
"""Test equality between StringFields with different use_lowercase settings."""
107-
sf1 = StringField("TEST", use_lowercase=True)
108-
sf2 = StringField("TEST", use_lowercase=False)
109-
# They should be equal because they have the same value
110-
assert sf1 == sf2, f"Expected sf1 to equal sf2, but they are not equal (sf1={sf1!r}, sf2={sf2!r})"
111106

107+
class TestStringFieldStrBehavior:
108+
"""Test StringField string behavior (inherited from str)."""
112109

113-
class TestStringFieldBool:
114-
"""Test StringField __bool__ method."""
110+
def test_str_operations(self):
111+
"""Test that StringField behaves like a string."""
112+
sf = StringField("test")
113+
assert len(sf) == 4, f"Expected len(sf) to be 4, but got {len(sf)}"
114+
assert sf.upper() == "TEST", f"Expected sf.upper() to be 'TEST', but got {sf.upper()!r}"
115+
assert sf.lower() == "test", f"Expected sf.lower() to be 'test', but got {sf.lower()!r}"
116+
assert sf.startswith("te"), f"Expected sf.startswith('te') to be True, but got {sf.startswith('te')}"
115117

116118
def test_bool_with_non_empty_string(self):
117-
"""Test __bool__ with non-empty string."""
119+
"""Test __bool__ with non-empty string (inherited from str)."""
118120
sf = StringField("test")
119121
assert bool(sf) is True, f"Expected bool(sf) to be True, but got {bool(sf)}"
120122

121123
def test_bool_with_empty_string(self):
122-
"""Test __bool__ with empty string."""
124+
"""Test __bool__ with empty string (inherited from str)."""
123125
sf = StringField("")
124126
assert bool(sf) is False, f"Expected bool(sf) to be False, but got {bool(sf)}"
125127

126128
def test_bool_with_whitespace(self):
127-
"""Test __bool__ with whitespace-only string."""
129+
"""Test __bool__ with whitespace-only string (inherited from str)."""
128130
sf = StringField(" ")
129131
assert bool(sf) is True, f"Expected bool(sf) to be True for whitespace, but got {bool(sf)}"
130132

131133

132-
class TestStringFieldRepr:
133-
"""Test StringField __repr__ method."""
134-
135-
def test_repr_with_value(self):
136-
"""Test __repr__ with a value."""
137-
sf = StringField("test")
138-
expected = "StringField: 'test'"
139-
assert repr(sf) == expected, f"Expected repr(sf) to be {expected!r}, but got {repr(sf)!r}"
140-
141-
def test_repr_with_empty_string(self):
142-
"""Test __repr__ with empty string."""
143-
sf = StringField("")
144-
expected = "StringField with missing value"
145-
assert repr(sf) == expected, f"Expected repr(sf) to be {expected!r}, but got {repr(sf)!r}"
146-
147-
def test_repr_with_special_characters(self):
148-
"""Test __repr__ with special characters."""
149-
sf = StringField("test 'value'")
150-
expected = "StringField: 'test 'value''"
151-
assert repr(sf) == expected, f"Expected repr(sf) to be {expected!r}, but got {repr(sf)!r}"
152-
153-
def test_repr_with_unicode(self):
154-
"""Test __repr__ with unicode characters."""
155-
sf = StringField("café")
156-
expected = "StringField: 'café'"
157-
assert repr(sf) == expected, f"Expected repr(sf) to be {expected!r}, but got {repr(sf)!r}"
158-
159-
160134
class TestStringFieldEdgeCases:
161135
"""Test StringField edge cases."""
162136

163137
def test_value_preserved_after_normalize(self):
164138
"""Test that original value is preserved after normalize."""
165139
sf = StringField("ORIGINAL")
166140
normalized = sf.normalize()
167-
assert sf.value == "ORIGINAL", f"Expected original sf.value to remain 'ORIGINAL', but got {sf.value!r}"
168-
assert normalized.value == "original", f"Expected normalized.value to be 'original', but got {normalized.value!r}"
141+
assert sf == "ORIGINAL", f"Expected original sf to remain 'ORIGINAL', but got {sf!r}"
142+
assert normalized == "original", f"Expected normalized to be 'original', but got {normalized!r}"
169143

170144
def test_multiple_normalize_calls(self):
171145
"""Test multiple normalize calls."""
172146
sf = StringField(" TEST ")
173147
norm1 = sf.normalize()
174148
norm2 = norm1.normalize()
175-
assert norm1.value == "test", f"Expected norm1.value to be 'test', but got {norm1.value!r}"
176-
assert norm2.value == "test", f"Expected norm2.value to be 'test', but got {norm2.value!r}"
149+
assert norm1 == "test", f"Expected norm1 to be 'test', but got {norm1!r}"
150+
assert norm2 == "test", f"Expected norm2 to be 'test', but got {norm2!r}"
177151

178152
def test_equality_chain(self):
179153
"""Test equality chain with multiple StringFields."""
@@ -182,10 +156,21 @@ def test_equality_chain(self):
182156
sf3 = StringField("test")
183157
assert sf1 == sf2 == sf3, f"Expected all StringFields to be equal, but they are not (sf1={sf1!r}, sf2={sf2!r}, sf3={sf3!r})"
184158

185-
def test_equality_with_normalized(self):
186-
"""Test equality between original and normalized StringField."""
187-
sf1 = StringField("TEST")
188-
sf2 = sf1.normalize()
189-
# They should be equal because they have the same value after normalization
190-
assert sf1 == sf2, f"Expected sf1 to equal normalized sf2, but they are not equal (sf1={sf1!r}, sf2={sf2!r})"
159+
def test_normalize_with_different_lowercase_settings(self):
160+
"""Test normalize with different lowercase settings."""
161+
sf = StringField("TEST")
162+
norm1 = sf.normalize(lowercase=True)
163+
norm2 = sf.normalize(lowercase=False)
164+
assert norm1 == "test", f"Expected norm1 to be 'test', but got {norm1!r}"
165+
assert norm2 == "TEST", f"Expected norm2 to be 'TEST', but got {norm2!r}"
166+
167+
def test_string_concatenation(self):
168+
"""Test that StringField can be concatenated like a string."""
169+
sf1 = StringField("hello")
170+
sf2 = StringField("world")
171+
result = sf1 + " " + sf2
172+
assert result == "hello world", f"Expected result to be 'hello world', but got {result!r}"
173+
# UserString concatenation returns a new instance of the same class
174+
assert isinstance(result, StringField), f"Expected result to be a StringField instance, but got {type(result)}"
175+
assert result.data == "hello world", f"Expected result.data to be 'hello world', but got {result.data!r}"
191176

0 commit comments

Comments
 (0)