Skip to content

Commit 3169bee

Browse files
azymnislandrito
authored andcommitted
Add a __hash__ implementation to SchemaField (googleapis#3601)
* Add a __hash__ implementation to SchemaField * Modify default list of subfields to be the empty tuple * Making SchemaField immutable. * Adding SchemaField.__ne__.
1 parent 3a4d031 commit 3169bee

File tree

4 files changed

+186
-52
lines changed

4 files changed

+186
-52
lines changed

bigquery/google/cloud/bigquery/schema.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,89 @@ class SchemaField(object):
2626
'FLOAT', 'BOOLEAN', 'TIMESTAMP' or 'RECORD').
2727
2828
:type mode: str
29-
:param mode: the type of the field (one of 'NULLABLE', 'REQUIRED',
29+
:param mode: the mode of the field (one of 'NULLABLE', 'REQUIRED',
3030
or 'REPEATED').
3131
3232
:type description: str
3333
:param description: optional description for the field.
3434
35-
:type fields: list of :class:`SchemaField`, or None
35+
:type fields: tuple of :class:`SchemaField`
3636
:param fields: subfields (requires ``field_type`` of 'RECORD').
3737
"""
38-
def __init__(self, name, field_type, mode='NULLABLE', description=None,
39-
fields=None):
40-
self.name = name
41-
self.field_type = field_type
42-
self.mode = mode
43-
self.description = description
44-
self.fields = fields
38+
def __init__(self, name, field_type, mode='NULLABLE',
39+
description=None, fields=()):
40+
self._name = name
41+
self._field_type = field_type
42+
self._mode = mode
43+
self._description = description
44+
self._fields = tuple(fields)
4545

46-
def __eq__(self, other):
46+
@property
47+
def name(self):
48+
"""str: The name of the field."""
49+
return self._name
50+
51+
@property
52+
def field_type(self):
53+
"""str: The type of the field.
54+
55+
Will be one of 'STRING', 'INTEGER', 'FLOAT', 'BOOLEAN',
56+
'TIMESTAMP' or 'RECORD'.
57+
"""
58+
return self._field_type
59+
60+
@property
61+
def mode(self):
62+
"""str: The mode of the field.
63+
64+
Will be one of 'NULLABLE', 'REQUIRED', or 'REPEATED'.
65+
"""
66+
return self._mode
67+
68+
@property
69+
def description(self):
70+
"""Optional[str]: Description for the field."""
71+
return self._description
72+
73+
@property
74+
def fields(self):
75+
"""tuple: Subfields contained in this field.
76+
77+
If ``field_type`` is not 'RECORD', this property must be
78+
empty / unset.
79+
"""
80+
return self._fields
81+
82+
def _key(self):
83+
"""A tuple key that unique-ly describes this field.
84+
85+
Used to compute this instance's hashcode and evaluate equality.
86+
87+
Returns:
88+
tuple: The contents of this :class:`SchemaField`.
89+
"""
4790
return (
48-
self.name == other.name and
49-
self.field_type.lower() == other.field_type.lower() and
50-
self.mode == other.mode and
51-
self.description == other.description and
52-
self.fields == other.fields)
91+
self._name,
92+
self._field_type.lower(),
93+
self._mode,
94+
self._description,
95+
self._fields,
96+
)
97+
98+
def __eq__(self, other):
99+
if isinstance(other, SchemaField):
100+
return self._key() == other._key()
101+
else:
102+
return NotImplemented
103+
104+
def __ne__(self, other):
105+
if isinstance(other, SchemaField):
106+
return self._key() != other._key()
107+
else:
108+
return NotImplemented
109+
110+
def __hash__(self):
111+
return hash(self._key())
112+
113+
def __repr__(self):
114+
return 'SchemaField{}'.format(self._key())

bigquery/google/cloud/bigquery/table.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ def _parse_schema_resource(info):
10791079
present in ``info``.
10801080
"""
10811081
if 'fields' not in info:
1082-
return None
1082+
return ()
10831083

10841084
schema = []
10851085
for r_field in info['fields']:
@@ -1109,7 +1109,7 @@ def _build_schema_resource(fields):
11091109
'mode': field.mode}
11101110
if field.description is not None:
11111111
info['description'] = field.description
1112-
if field.fields is not None:
1112+
if field.fields:
11131113
info['fields'] = _build_schema_resource(field.fields)
11141114
infos.append(info)
11151115
return infos

bigquery/tests/unit/test_query.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ def _verifySchema(self, query, resource):
8888
self.assertEqual(found.mode, expected['mode'])
8989
self.assertEqual(found.description,
9090
expected.get('description'))
91-
self.assertEqual(found.fields, expected.get('fields'))
91+
self.assertEqual(found.fields, expected.get('fields', ()))
9292
else:
93-
self.assertIsNone(query.schema)
93+
self.assertEqual(query.schema, ())
9494

9595
def _verifyRows(self, query, resource):
9696
expected = resource.get('rows')
@@ -166,7 +166,7 @@ def test_ctor_defaults(self):
166166
self.assertIsNone(query.page_token)
167167
self.assertEqual(query.query_parameters, [])
168168
self.assertEqual(query.rows, [])
169-
self.assertIsNone(query.schema)
169+
self.assertEqual(query.schema, ())
170170
self.assertIsNone(query.total_rows)
171171
self.assertIsNone(query.total_bytes_processed)
172172
self.assertEqual(query.udf_resources, [])

bigquery/tests/unit/test_schema.py

Lines changed: 104 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,72 @@ def _get_target_class():
2626
def _make_one(self, *args, **kw):
2727
return self._get_target_class()(*args, **kw)
2828

29-
def test_ctor_defaults(self):
29+
def test_constructor_defaults(self):
3030
field = self._make_one('test', 'STRING')
31-
self.assertEqual(field.name, 'test')
32-
self.assertEqual(field.field_type, 'STRING')
33-
self.assertEqual(field.mode, 'NULLABLE')
34-
self.assertIsNone(field.description)
35-
self.assertIsNone(field.fields)
31+
self.assertEqual(field._name, 'test')
32+
self.assertEqual(field._field_type, 'STRING')
33+
self.assertEqual(field._mode, 'NULLABLE')
34+
self.assertIsNone(field._description)
35+
self.assertEqual(field._fields, ())
3636

37-
def test_ctor_explicit(self):
37+
def test_constructor_explicit(self):
3838
field = self._make_one('test', 'STRING', mode='REQUIRED',
3939
description='Testing')
40-
self.assertEqual(field.name, 'test')
41-
self.assertEqual(field.field_type, 'STRING')
42-
self.assertEqual(field.mode, 'REQUIRED')
43-
self.assertEqual(field.description, 'Testing')
44-
self.assertIsNone(field.fields)
45-
46-
def test_ctor_subfields(self):
40+
self.assertEqual(field._name, 'test')
41+
self.assertEqual(field._field_type, 'STRING')
42+
self.assertEqual(field._mode, 'REQUIRED')
43+
self.assertEqual(field._description, 'Testing')
44+
self.assertEqual(field._fields, ())
45+
46+
def test_constructor_subfields(self):
47+
sub_field1 = self._make_one('area_code', 'STRING')
48+
sub_field2 = self._make_one('local_number', 'STRING')
4749
field = self._make_one(
48-
'phone_number', 'RECORD',
49-
fields=[self._make_one('area_code', 'STRING'),
50-
self._make_one('local_number', 'STRING')])
51-
self.assertEqual(field.name, 'phone_number')
52-
self.assertEqual(field.field_type, 'RECORD')
53-
self.assertEqual(field.mode, 'NULLABLE')
54-
self.assertIsNone(field.description)
55-
self.assertEqual(len(field.fields), 2)
56-
self.assertEqual(field.fields[0].name, 'area_code')
57-
self.assertEqual(field.fields[0].field_type, 'STRING')
58-
self.assertEqual(field.fields[0].mode, 'NULLABLE')
59-
self.assertIsNone(field.fields[0].description)
60-
self.assertIsNone(field.fields[0].fields)
61-
self.assertEqual(field.fields[1].name, 'local_number')
62-
self.assertEqual(field.fields[1].field_type, 'STRING')
63-
self.assertEqual(field.fields[1].mode, 'NULLABLE')
64-
self.assertIsNone(field.fields[1].description)
65-
self.assertIsNone(field.fields[1].fields)
50+
'phone_number',
51+
'RECORD',
52+
fields=[sub_field1, sub_field2],
53+
)
54+
self.assertEqual(field._name, 'phone_number')
55+
self.assertEqual(field._field_type, 'RECORD')
56+
self.assertEqual(field._mode, 'NULLABLE')
57+
self.assertIsNone(field._description)
58+
self.assertEqual(len(field._fields), 2)
59+
self.assertIs(field._fields[0], sub_field1)
60+
self.assertIs(field._fields[1], sub_field2)
61+
62+
def test_name_property(self):
63+
name = 'lemon-ness'
64+
schema_field = self._make_one(name, 'INTEGER')
65+
self.assertIs(schema_field.name, name)
66+
67+
def test_field_type_property(self):
68+
field_type = 'BOOLEAN'
69+
schema_field = self._make_one('whether', field_type)
70+
self.assertIs(schema_field.field_type, field_type)
71+
72+
def test_mode_property(self):
73+
mode = 'REPEATED'
74+
schema_field = self._make_one('again', 'FLOAT', mode=mode)
75+
self.assertIs(schema_field.mode, mode)
76+
77+
def test_description_property(self):
78+
description = 'It holds some data.'
79+
schema_field = self._make_one(
80+
'do', 'TIMESTAMP', description=description)
81+
self.assertIs(schema_field.description, description)
82+
83+
def test_fields_property(self):
84+
sub_field1 = self._make_one('one', 'STRING')
85+
sub_field2 = self._make_one('fish', 'INTEGER')
86+
fields = (sub_field1, sub_field2)
87+
schema_field = self._make_one('boat', 'RECORD', fields=fields)
88+
self.assertIs(schema_field.fields, fields)
89+
90+
def test___eq___wrong_type(self):
91+
field = self._make_one('test', 'STRING')
92+
other = object()
93+
self.assertNotEqual(field, other)
94+
self.assertIs(field.__eq__(other), NotImplemented)
6695

6796
def test___eq___name_mismatch(self):
6897
field = self._make_one('test', 'STRING')
@@ -111,3 +140,46 @@ def test___eq___hit_w_fields(self):
111140
field = self._make_one('test', 'RECORD', fields=[sub1, sub2])
112141
other = self._make_one('test', 'RECORD', fields=[sub1, sub2])
113142
self.assertEqual(field, other)
143+
144+
def test___ne___wrong_type(self):
145+
field = self._make_one('toast', 'INTEGER')
146+
other = object()
147+
self.assertNotEqual(field, other)
148+
self.assertIs(field.__ne__(other), NotImplemented)
149+
150+
def test___ne___same_value(self):
151+
field1 = self._make_one('test', 'TIMESTAMP', mode='REPEATED')
152+
field2 = self._make_one('test', 'TIMESTAMP', mode='REPEATED')
153+
# unittest ``assertEqual`` uses ``==`` not ``!=``.
154+
comparison_val = (field1 != field2)
155+
self.assertFalse(comparison_val)
156+
157+
def test___ne___different_values(self):
158+
field1 = self._make_one(
159+
'test1', 'FLOAT', mode='REPEATED', description='Not same')
160+
field2 = self._make_one(
161+
'test2', 'FLOAT', mode='NULLABLE', description='Knot saym')
162+
self.assertNotEqual(field1, field2)
163+
164+
def test___hash__set_equality(self):
165+
sub1 = self._make_one('sub1', 'STRING')
166+
sub2 = self._make_one('sub2', 'STRING')
167+
field1 = self._make_one('test', 'RECORD', fields=[sub1])
168+
field2 = self._make_one('test', 'RECORD', fields=[sub2])
169+
set_one = {field1, field2}
170+
set_two = {field1, field2}
171+
self.assertEqual(set_one, set_two)
172+
173+
def test___hash__not_equals(self):
174+
sub1 = self._make_one('sub1', 'STRING')
175+
sub2 = self._make_one('sub2', 'STRING')
176+
field1 = self._make_one('test', 'RECORD', fields=[sub1])
177+
field2 = self._make_one('test', 'RECORD', fields=[sub2])
178+
set_one = {field1}
179+
set_two = {field2}
180+
self.assertNotEqual(set_one, set_two)
181+
182+
def test___repr__(self):
183+
field1 = self._make_one('field1', 'STRING')
184+
expected = "SchemaField('field1', 'string', 'NULLABLE', None, ())"
185+
self.assertEqual(repr(field1), expected)

0 commit comments

Comments
 (0)