Skip to content

Commit 4846564

Browse files
committed
Merge pull request #16 from maxmind/greg/str-method
Add __repr__ and __eq__ methods to model/record classes
2 parents e965217 + 867aa96 commit 4846564

File tree

5 files changed

+104
-9
lines changed

5 files changed

+104
-9
lines changed

geoip2/mixins.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""This package contains utility mixins"""
2+
# pylint: disable=too-few-public-methods
3+
from abc import ABCMeta
4+
5+
6+
class SimpleEquality(object):
7+
8+
"""Naive __dict__ equality mixin"""
9+
10+
__metaclass__ = ABCMeta
11+
12+
def __eq__(self, other):
13+
return (isinstance(other, self.__class__)
14+
and self.__dict__ == other.__dict__)
15+
16+
def __ne__(self, other):
17+
return not self.__eq__(other)

geoip2/models.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
http://dev.maxmind.com/geoip/geoip2/web-services for more details.
1111
1212
"""
13-
# pylint:disable=R0903
13+
# pylint: disable=too-many-instance-attributes,too-few-public-methods
14+
from abc import ABCMeta
15+
1416
import geoip2.records
17+
from geoip2.mixins import SimpleEquality
1518

1619

17-
class Country(object):
20+
class Country(SimpleEquality):
1821

1922
"""Model for the GeoIP2 Precision: Country and the GeoIP2 Country database
2023
@@ -66,6 +69,7 @@ class Country(object):
6669
def __init__(self, raw_response, locales=None):
6770
if locales is None:
6871
locales = ['en']
72+
self._locales = locales
6973
self.continent = \
7074
geoip2.records.Continent(locales,
7175
**raw_response.get('continent', {}))
@@ -76,18 +80,24 @@ def __init__(self, raw_response, locales=None):
7680
geoip2.records.Country(locales,
7781
**raw_response.get('registered_country',
7882
{}))
79-
# pylint:disable=bad-continuation
8083
self.represented_country \
8184
= geoip2.records.RepresentedCountry(locales,
8285
**raw_response.get(
83-
'represented_country', {}))
86+
'represented_country', {}))
8487

8588
self.maxmind = \
8689
geoip2.records.MaxMind(**raw_response.get('maxmind', {}))
8790

8891
self.traits = geoip2.records.Traits(**raw_response.get('traits', {}))
8992
self.raw = raw_response
9093

94+
def __repr__(self):
95+
return '{module}.{class_name}({data}, {locales})'.format(
96+
module=self.__module__,
97+
class_name=self.__class__.__name__,
98+
data=self.raw,
99+
locales=self._locales)
100+
91101

92102
class City(Country):
93103

@@ -230,7 +240,21 @@ class Insights(City):
230240
"""
231241

232242

233-
class ConnectionType(object):
243+
class SimpleModel(SimpleEquality):
244+
245+
"""Provides basic methods for non-location models"""
246+
247+
__metaclass__ = ABCMeta
248+
249+
def __repr__(self):
250+
# pylint: disable=no-member
251+
return '{module}.{class_name}({data})'.format(
252+
module=self.__module__,
253+
class_name=self.__class__.__name__,
254+
data=str(self.raw))
255+
256+
257+
class ConnectionType(SimpleModel):
234258

235259
"""Model class for the GeoIP2 Connection-Type
236260
@@ -262,7 +286,7 @@ def __init__(self, raw):
262286
self.raw = raw
263287

264288

265-
class Domain(object):
289+
class Domain(SimpleModel):
266290

267291
"""Model class for the GeoIP2 Domain
268292
@@ -288,7 +312,7 @@ def __init__(self, raw):
288312
self.raw = raw
289313

290314

291-
class ISP(object):
315+
class ISP(SimpleModel):
292316

293317
"""Model class for the GeoIP2 ISP
294318

geoip2/records.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
# pylint:disable=R0903
88
from abc import ABCMeta
99

10+
from geoip2.mixins import SimpleEquality
1011

11-
class Record(object):
12+
13+
class Record(SimpleEquality):
1214

1315
"""All records are subclasses of the abstract class ``Record``"""
1416
__metaclass__ = ABCMeta
@@ -22,6 +24,13 @@ def __init__(self, **kwargs):
2224
def __setattr__(self, name, value):
2325
raise AttributeError("can't set attribute")
2426

27+
def __repr__(self):
28+
args = ', '.join('%s=%r' % x for x in self.__dict__.items())
29+
return '{module}.{class_name}({data})'.format(
30+
module=self.__module__,
31+
class_name=self.__class__.__name__,
32+
data=args)
33+
2534

2635
class PlaceRecord(Record):
2736

tests/database_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
if sys.version_info[0] == 2:
1717
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
18+
unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches
1819

1920

2021
class TestReader(unittest.TestCase):
@@ -78,6 +79,14 @@ def test_connection_type(self):
7879
record = reader.connection_type(ip_address)
7980
self.assertEqual(record.connection_type, 'Cable/DSL')
8081
self.assertEqual(record.ip_address, ip_address)
82+
83+
self.assertRegex(
84+
str(record), r'ConnectionType\(\{.*Cable/DSL.*\}\)',
85+
'ConnectionType str representation is reasonable')
86+
87+
self.assertEqual(record, eval(repr(record)),
88+
"ConnectionType repr can be eval'd")
89+
8190
reader.close()
8291

8392
def test_domain(self):
@@ -89,6 +98,13 @@ def test_domain(self):
8998
self.assertEqual(record.domain, 'maxmind.com')
9099
self.assertEqual(record.ip_address, ip_address)
91100

101+
self.assertRegex(
102+
str(record), r'Domain\(\{.*maxmind.com.*\}\)',
103+
'Domain str representation is reasonable')
104+
105+
self.assertEqual(record, eval(repr(record)),
106+
"Domain repr can be eval'd")
107+
92108
reader.close()
93109

94110
def test_isp(self):
@@ -104,4 +120,11 @@ def test_isp(self):
104120
self.assertEqual(record.organization, 'Telstra Internet')
105121
self.assertEqual(record.ip_address, ip_address)
106122

123+
self.assertRegex(
124+
str(record), r'ISP\(\{.*Telstra.*\}\)',
125+
'ISP str representation is reasonable')
126+
127+
self.assertEqual(record, eval(repr(record)),
128+
"ISP repr can be eval'd")
129+
107130
reader.close()

tests/models_test.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
if sys.version_info[0] == 2:
1717
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
18+
unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches
1819

1920

2021
class TestModels(unittest.TestCase):
@@ -58,7 +59,7 @@ def test_insights_full(self):
5859
'geoname_id': 123,
5960
'iso_code': 'HP',
6061
'names': {'en': 'Hennepin'},
61-
}
62+
}
6263
],
6364
'registered_country': {
6465
'geoname_id': 2,
@@ -133,6 +134,21 @@ def test_insights_full(self):
133134
self.assertEqual(model.location.metro_code, 765,
134135
'correct metro_code')
135136

137+
self.assertRegex(
138+
str(
139+
model), r'^geoip2.models.Insights\(\{.*geoname_id.*\}, \[.*en.*\]\)',
140+
'Insights str representation looks reasonable')
141+
142+
self.assertEqual(
143+
model, eval(repr(model)), "Insights repr can be eval'd")
144+
145+
self.assertRegex(
146+
str(model.location), r'^geoip2.records.Location\(.*longitude=.*\)',
147+
'Location str representation is reasonable')
148+
149+
self.assertEqual(model.location, eval(repr(model.location)),
150+
"Location repr can be eval'd")
151+
136152
def test_insights_min(self):
137153
model = geoip2.models.Insights({'traits': {'ip_address': '5.6.7.8'}})
138154
self.assertEqual(type(model), geoip2.models.Insights,
@@ -229,6 +245,12 @@ def test_city_full(self):
229245
'traits is_setellite_provider is True')
230246
self.assertEqual(model.raw, raw, 'raw method produces raw output')
231247

248+
self.assertRegex(
249+
str(model), r'^geoip2.models.City\(\{.*geoname_id.*\}, \[.*en.*\]\)')
250+
251+
self.assertFalse(
252+
model == True, '__eq__ does not blow up on weird input')
253+
232254
def test_unknown_keys(self):
233255
model = geoip2.models.City({'traits': {'ip_address': '1.2.3.4',
234256
'invalid': 'blah'},

0 commit comments

Comments
 (0)