Skip to content

Commit afdc1ed

Browse files
authored
refactor: extract common logic to TableBase class (googleapis#956)
* refactor: extract common logic to TableBase class * Adress pytype's missing attribute false warning * Mark TableBase class as private * Simplify TableReference.to_api_repr() logic * Avoid get/set subproperty helper gotcha * Test _TableBase class directly
1 parent 6e785c7 commit afdc1ed

File tree

4 files changed

+293
-314
lines changed

4 files changed

+293
-314
lines changed

google/cloud/bigquery/_helpers.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -672,8 +672,9 @@ def _get_sub_prop(container, keys, default=None):
672672
container (Dict):
673673
A dictionary which may contain other dictionaries as values.
674674
keys (Iterable):
675-
A sequence of keys to attempt to get the value for. Each item in
676-
the sequence represents a deeper nesting. The first key is for
675+
A sequence of keys to attempt to get the value for. If ``keys`` is a
676+
string, it is treated as sequence containing a single string key. Each item
677+
in the sequence represents a deeper nesting. The first key is for
677678
the top level. If there is a dictionary there, the second key
678679
attempts to get the value within that, and so on.
679680
default (Optional[object]):
@@ -700,6 +701,9 @@ def _get_sub_prop(container, keys, default=None):
700701
Returns:
701702
object: The value if present or the default.
702703
"""
704+
if isinstance(keys, str):
705+
keys = [keys]
706+
703707
sub_val = container
704708
for key in keys:
705709
if key not in sub_val:
@@ -715,8 +719,9 @@ def _set_sub_prop(container, keys, value):
715719
container (Dict):
716720
A dictionary which may contain other dictionaries as values.
717721
keys (Iterable):
718-
A sequence of keys to attempt to set the value for. Each item in
719-
the sequence represents a deeper nesting. The first key is for
722+
A sequence of keys to attempt to set the value for. If ``keys`` is a
723+
string, it is treated as sequence containing a single string key. Each item
724+
in the sequence represents a deeper nesting. The first key is for
720725
the top level. If there is a dictionary there, the second key
721726
attempts to get the value within that, and so on.
722727
value (object): Value to set within the container.
@@ -743,6 +748,9 @@ def _set_sub_prop(container, keys, value):
743748
>>> container
744749
{'key': {'subkey': 'new'}}
745750
"""
751+
if isinstance(keys, str):
752+
keys = [keys]
753+
746754
sub_val = container
747755
for key in keys[:-1]:
748756
if key not in sub_val:

google/cloud/bigquery/table.py

+87-149
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import geopandas
7070
import pyarrow
7171
from google.cloud import bigquery_storage
72+
from google.cloud.bigquery.dataset import DatasetReference
7273

7374

7475
_NO_PANDAS_ERROR = (
@@ -126,45 +127,93 @@ def _view_use_legacy_sql_getter(table):
126127
return True
127128

128129

129-
class TableReference(object):
130-
"""TableReferences are pointers to tables.
130+
class _TableBase:
131+
"""Base class for Table-related classes with common functionality."""
131132

132-
See
133-
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#tablereference
134-
135-
Args:
136-
dataset_ref (google.cloud.bigquery.dataset.DatasetReference):
137-
A pointer to the dataset
138-
table_id (str): The ID of the table
139-
"""
133+
_PROPERTY_TO_API_FIELD = {
134+
"dataset_id": ["tableReference", "datasetId"],
135+
"project": ["tableReference", "projectId"],
136+
"table_id": ["tableReference", "tableId"],
137+
}
140138

141-
def __init__(self, dataset_ref, table_id):
142-
self._project = dataset_ref.project
143-
self._dataset_id = dataset_ref.dataset_id
144-
self._table_id = table_id
139+
def __init__(self):
140+
self._properties = {}
145141

146142
@property
147-
def project(self):
148-
"""str: Project bound to the table"""
149-
return self._project
143+
def project(self) -> str:
144+
"""Project bound to the table."""
145+
return _helpers._get_sub_prop(
146+
self._properties, self._PROPERTY_TO_API_FIELD["project"]
147+
)
150148

151149
@property
152-
def dataset_id(self):
153-
"""str: ID of dataset containing the table."""
154-
return self._dataset_id
150+
def dataset_id(self) -> str:
151+
"""ID of dataset containing the table."""
152+
return _helpers._get_sub_prop(
153+
self._properties, self._PROPERTY_TO_API_FIELD["dataset_id"]
154+
)
155155

156156
@property
157-
def table_id(self):
158-
"""str: The table ID."""
159-
return self._table_id
157+
def table_id(self) -> str:
158+
"""The table ID."""
159+
return _helpers._get_sub_prop(
160+
self._properties, self._PROPERTY_TO_API_FIELD["table_id"]
161+
)
160162

161163
@property
162-
def path(self):
163-
"""str: URL path for the table's APIs."""
164-
return "/projects/%s/datasets/%s/tables/%s" % (
165-
self._project,
166-
self._dataset_id,
167-
self._table_id,
164+
def path(self) -> str:
165+
"""URL path for the table's APIs."""
166+
return (
167+
f"/projects/{self.project}/datasets/{self.dataset_id}"
168+
f"/tables/{self.table_id}"
169+
)
170+
171+
def __eq__(self, other):
172+
if isinstance(other, _TableBase):
173+
return (
174+
self.project == other.project
175+
and self.dataset_id == other.dataset_id
176+
and self.table_id == other.table_id
177+
)
178+
else:
179+
return NotImplemented
180+
181+
def __hash__(self):
182+
return hash((self.project, self.dataset_id, self.table_id))
183+
184+
185+
class TableReference(_TableBase):
186+
"""TableReferences are pointers to tables.
187+
188+
See
189+
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#tablereference
190+
191+
Args:
192+
dataset_ref: A pointer to the dataset
193+
table_id: The ID of the table
194+
"""
195+
196+
_PROPERTY_TO_API_FIELD = {
197+
"dataset_id": "datasetId",
198+
"project": "projectId",
199+
"table_id": "tableId",
200+
}
201+
202+
def __init__(self, dataset_ref: "DatasetReference", table_id: str):
203+
self._properties = {}
204+
205+
_helpers._set_sub_prop(
206+
self._properties,
207+
self._PROPERTY_TO_API_FIELD["project"],
208+
dataset_ref.project,
209+
)
210+
_helpers._set_sub_prop(
211+
self._properties,
212+
self._PROPERTY_TO_API_FIELD["dataset_id"],
213+
dataset_ref.dataset_id,
214+
)
215+
_helpers._set_sub_prop(
216+
self._properties, self._PROPERTY_TO_API_FIELD["table_id"], table_id,
168217
)
169218

170219
@classmethod
@@ -233,11 +282,7 @@ def to_api_repr(self) -> dict:
233282
Returns:
234283
Dict[str, object]: Table reference represented as an API resource
235284
"""
236-
return {
237-
"projectId": self._project,
238-
"datasetId": self._dataset_id,
239-
"tableId": self._table_id,
240-
}
285+
return copy.deepcopy(self._properties)
241286

242287
def to_bqstorage(self) -> str:
243288
"""Construct a BigQuery Storage API representation of this table.
@@ -257,54 +302,25 @@ def to_bqstorage(self) -> str:
257302
str: A reference to this table in the BigQuery Storage API.
258303
"""
259304

260-
table_id, _, _ = self._table_id.partition("@")
305+
table_id, _, _ = self.table_id.partition("@")
261306
table_id, _, _ = table_id.partition("$")
262307

263-
table_ref = "projects/{}/datasets/{}/tables/{}".format(
264-
self._project, self._dataset_id, table_id,
308+
table_ref = (
309+
f"projects/{self.project}/datasets/{self.dataset_id}/tables/{table_id}"
265310
)
266-
267311
return table_ref
268312

269-
def _key(self):
270-
"""A tuple key that uniquely describes this field.
271-
272-
Used to compute this instance's hashcode and evaluate equality.
273-
274-
Returns:
275-
Tuple[str]: The contents of this :class:`DatasetReference`.
276-
"""
277-
return (self._project, self._dataset_id, self._table_id)
278-
279-
def __eq__(self, other):
280-
if isinstance(other, (Table, TableListItem)):
281-
return (
282-
self.project == other.project
283-
and self.dataset_id == other.dataset_id
284-
and self.table_id == other.table_id
285-
)
286-
elif isinstance(other, TableReference):
287-
return self._key() == other._key()
288-
else:
289-
return NotImplemented
290-
291-
def __ne__(self, other):
292-
return not self == other
293-
294-
def __hash__(self):
295-
return hash(self._key())
296-
297313
def __str__(self):
298314
return f"{self.project}.{self.dataset_id}.{self.table_id}"
299315

300316
def __repr__(self):
301317
from google.cloud.bigquery.dataset import DatasetReference
302318

303-
dataset_ref = DatasetReference(self._project, self._dataset_id)
304-
return "TableReference({}, '{}')".format(repr(dataset_ref), self._table_id)
319+
dataset_ref = DatasetReference(self.project, self.dataset_id)
320+
return f"TableReference({dataset_ref!r}, '{self.table_id}')"
305321

306322

307-
class Table(object):
323+
class Table(_TableBase):
308324
"""Tables represent a set of rows whose values correspond to a schema.
309325
310326
See
@@ -325,9 +341,9 @@ class Table(object):
325341
"""
326342

327343
_PROPERTY_TO_API_FIELD = {
344+
**_TableBase._PROPERTY_TO_API_FIELD,
328345
"clustering_fields": "clustering",
329346
"created": "creationTime",
330-
"dataset_id": ["tableReference", "datasetId"],
331347
"description": "description",
332348
"encryption_configuration": "encryptionConfiguration",
333349
"etag": "etag",
@@ -346,14 +362,12 @@ class Table(object):
346362
"num_rows": "numRows",
347363
"partition_expiration": "timePartitioning",
348364
"partitioning_type": "timePartitioning",
349-
"project": ["tableReference", "projectId"],
350365
"range_partitioning": "rangePartitioning",
351366
"time_partitioning": "timePartitioning",
352367
"schema": "schema",
353368
"snapshot_definition": "snapshotDefinition",
354369
"streaming_buffer": "streamingBuffer",
355370
"self_link": "selfLink",
356-
"table_id": ["tableReference", "tableId"],
357371
"time_partitioning": "timePartitioning",
358372
"type": "type",
359373
"view_use_legacy_sql": "view",
@@ -368,38 +382,8 @@ def __init__(self, table_ref, schema=None):
368382
if schema is not None:
369383
self.schema = schema
370384

371-
@property
372-
def project(self):
373-
"""str: Project bound to the table."""
374-
return _helpers._get_sub_prop(
375-
self._properties, self._PROPERTY_TO_API_FIELD["project"]
376-
)
377-
378-
@property
379-
def dataset_id(self):
380-
"""str: ID of dataset containing the table."""
381-
return _helpers._get_sub_prop(
382-
self._properties, self._PROPERTY_TO_API_FIELD["dataset_id"]
383-
)
384-
385-
@property
386-
def table_id(self):
387-
"""str: ID of the table."""
388-
return _helpers._get_sub_prop(
389-
self._properties, self._PROPERTY_TO_API_FIELD["table_id"]
390-
)
391-
392385
reference = property(_reference_getter)
393386

394-
@property
395-
def path(self):
396-
"""str: URL path for the table's APIs."""
397-
return "/projects/%s/datasets/%s/tables/%s" % (
398-
self.project,
399-
self.dataset_id,
400-
self.table_id,
401-
)
402-
403387
@property
404388
def require_partition_filter(self):
405389
"""bool: If set to true, queries over the partitioned table require a
@@ -1040,29 +1024,11 @@ def _build_resource(self, filter_fields):
10401024
"""Generate a resource for ``update``."""
10411025
return _helpers._build_resource_from_properties(self, filter_fields)
10421026

1043-
def __eq__(self, other):
1044-
if isinstance(other, Table):
1045-
return (
1046-
self._properties["tableReference"]
1047-
== other._properties["tableReference"]
1048-
)
1049-
elif isinstance(other, (TableReference, TableListItem)):
1050-
return (
1051-
self.project == other.project
1052-
and self.dataset_id == other.dataset_id
1053-
and self.table_id == other.table_id
1054-
)
1055-
else:
1056-
return NotImplemented
1057-
1058-
def __hash__(self):
1059-
return hash((self.project, self.dataset_id, self.table_id))
1060-
10611027
def __repr__(self):
10621028
return "Table({})".format(repr(self.reference))
10631029

10641030

1065-
class TableListItem(object):
1031+
class TableListItem(_TableBase):
10661032
"""A read-only table resource from a list operation.
10671033
10681034
For performance reasons, the BigQuery API only includes some of the table
@@ -1126,21 +1092,6 @@ def expires(self):
11261092
1000.0 * float(expiration_time)
11271093
)
11281094

1129-
@property
1130-
def project(self):
1131-
"""str: Project bound to the table."""
1132-
return self._properties["tableReference"]["projectId"]
1133-
1134-
@property
1135-
def dataset_id(self):
1136-
"""str: ID of dataset containing the table."""
1137-
return self._properties["tableReference"]["datasetId"]
1138-
1139-
@property
1140-
def table_id(self):
1141-
"""str: ID of the table."""
1142-
return self._properties["tableReference"]["tableId"]
1143-
11441095
reference = property(_reference_getter)
11451096

11461097
@property
@@ -1276,19 +1227,6 @@ def to_api_repr(self) -> dict:
12761227
"""
12771228
return copy.deepcopy(self._properties)
12781229

1279-
def __eq__(self, other):
1280-
if isinstance(other, (Table, TableReference, TableListItem)):
1281-
return (
1282-
self.project == other.project
1283-
and self.dataset_id == other.dataset_id
1284-
and self.table_id == other.table_id
1285-
)
1286-
else:
1287-
return NotImplemented
1288-
1289-
def __hash__(self):
1290-
return hash((self.project, self.dataset_id, self.table_id))
1291-
12921230

12931231
def _row_from_mapping(mapping, schema):
12941232
"""Convert a mapping to a row tuple using the schema.

0 commit comments

Comments
 (0)