Skip to content

Commit cfa609c

Browse files
committed
update tests based on Schema superclass UserList
1 parent ef2b95d commit cfa609c

File tree

2 files changed

+73
-58
lines changed

2 files changed

+73
-58
lines changed

google/cloud/bigquery/schema.py

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from __future__ import annotations
1818

1919
import collections
20+
2021
import copy
2122
import enum
22-
from typing import Any, Dict, Iterable, Optional, Union, cast
23+
from typing import Any, Dict, Iterable, Optional, Union, cast, List, Mapping
2324

2425
from google.cloud.bigquery import standard_sql
2526
from google.cloud.bigquery._helpers import (
@@ -524,7 +525,7 @@ def __repr__(self):
524525

525526

526527
def _parse_schema_resource(info):
527-
"""Parse a resource fragment into a schema field.
528+
"""Parse a resource fragment into a sequence of schema fields.
528529
529530
Args:
530531
info: (Mapping[str, Dict]): should contain a "fields" key to be parsed
@@ -548,12 +549,35 @@ def _build_schema_resource(fields):
548549
return [field.to_api_repr() for field in fields]
549550

550551

551-
def _to_schema_fields(schema):
552-
"""TODO docstring
553-
CAST a list of elements to either:
554-
* a Schema object with SchemaFields and an attribute
555-
* a list of SchemaFields but no attribute
552+
def _to_schema_fields(
553+
schema: Union[Schema, List[Union[SchemaField, Mapping[str, Any]]]]
554+
) -> Union[Schema, List[SchemaField]]:
555+
"""Convert the input to either a Schema object OR a list of SchemaField objects.
556+
557+
This helper method ensures that the fields in the schema are SchemaField objects.
558+
It accepts:
559+
560+
* A :class:`~google.cloud.bigquery.schema.Schema` instance: It will
561+
convert items that are mappings to
562+
:class:`~google.cloud.bigquery.schema.SchemaField` instances and
563+
preserve foreign_type_info.
564+
565+
* A list of
566+
:class:`~google.cloud.bigquery.schema.SchemaField` instances.
567+
568+
* A list of mappings: It will convert each of the mapping items to
569+
a :class:`~google.cloud.bigquery.schema.SchemaField` instance.
570+
571+
Args:
572+
schema: The schema to convert.
573+
574+
Returns:
575+
The schema as a list of SchemaField objects or a Schema object.
576+
577+
Raises:
578+
ValueError: If the items in ``schema`` are not valid.
556579
"""
580+
557581
for field in schema:
558582
if not isinstance(field, (SchemaField, collections.abc.Mapping)):
559583
raise ValueError(
@@ -937,57 +961,45 @@ def foreign_type_info(self, value: str) -> None:
937961
@property
938962
def _fields(self) -> Any:
939963
"""TODO: docstring"""
940-
return self._properties.get("_fields")
964+
return self._properties.get("fields")
941965

942966
@_fields.setter
943967
def _fields(self, value: list) -> None:
944968
value = _isinstance_or_raise(value, list, none_allowed=True)
945-
self._properties["_fields"] = value
969+
value = _build_schema_resource(value)
970+
self._properties["fields"] = value
946971

947972
@property
948973
def data(self):
949-
return self._properties.get("_fields")
974+
return self._properties.get("fields")
950975

951976
@data.setter
952977
def data(self, value: list):
953978
# for simplicity, no validation in this proof of concept
954-
self._properties["_fields"] = value
955-
956-
def __len__(self):
957-
return len(self._fields)
958-
959-
def __getitem__(self, index):
960-
return self._fields[index]
961-
962-
def __setitem__(self, index, value):
963-
self._fields[index] = value
964-
965-
def __delitem__(self, index):
966-
del self._fields[index]
967-
968-
def __iter__(self):
969-
return iter(self._fields)
979+
value = _isinstance_or_raise(value, list, none_allowed=True)
980+
value = _build_schema_resource(value)
981+
self._properties["fields"] = value
970982

971983
def __str__(self):
972984
return f"Schema({self._fields}, {self.foreign_type_info})"
973985

974986
def __repr__(self):
975987
return f"Schema({self._fields!r}, {self.foreign_type_info!r})"
976988

977-
def append(self, item):
978-
self._fields.append(item)
979-
980-
def extend(self, iterable):
981-
self._fields.extend(iterable)
982-
983989
def to_api_repr(self) -> dict:
984990
"""Build an API representation of this object.
985991
986992
Returns:
987993
Dict[str, Any]:
988994
A dictionary in the format used by the BigQuery API.
989995
"""
990-
return copy.deepcopy(self._properties)
996+
# If this is a RECORD type, then sub-fields are also included,
997+
# add this to the serialized representation.
998+
answer = self._properties.copy()
999+
schemafields = any([isinstance(f, SchemaField) for f in self._fields])
1000+
if schemafields:
1001+
answer["fields"] = [f.to_api_repr() for f in self._fields]
1002+
return answer
9911003

9921004
@classmethod
9931005
def from_api_repr(cls, resource: dict) -> Schema:
@@ -1002,5 +1014,5 @@ def from_api_repr(cls, resource: dict) -> Schema:
10021014
An instance of the class initialized with data from 'resource'.
10031015
"""
10041016
config = cls("")
1005-
config._properties = copy.deepcopy(resource)
1017+
config._properties = copy.copy(resource)
10061018
return config

tests/unit/test_schema.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,6 @@ def test_from_api_repr_none(self):
681681
self.assertEqual(None, self._get_target_class().from_api_repr(None))
682682

683683

684-
# BEGIN PYTEST BASED SCHEMA TESTS ====================
685684
@pytest.fixture
686685
def basic_resource():
687686
return {
@@ -826,6 +825,7 @@ def test_build_schema_resource(self, fields, expected_resource):
826825

827826
class TestToSchemaFields: # Test class for _to_schema_fields
828827
def test_invalid_type(self):
828+
"""Invalid list of tuples instead of list of mappings"""
829829
schema = [
830830
("full_name", "STRING", "REQUIRED"),
831831
("address", "STRING", "REQUIRED"),
@@ -846,7 +846,7 @@ def test_schema_fields_sequence(self):
846846
def test_invalid_mapping_representation(self):
847847
schema = [
848848
{"name": "full_name", "type": "STRING", "mode": "REQUIRED"},
849-
{"name": "address", "typeooo": "STRING", "mode": "REQUIRED"},
849+
{"name": "address", "invalid_key": "STRING", "mode": "REQUIRED"},
850850
]
851851
with pytest.raises(Exception): # Or a more specific exception if known
852852
_to_schema_fields(schema)
@@ -889,7 +889,7 @@ def test_valid_mapping_representation(self, schema, expected_schema):
889889

890890
def test_valid_schema_object(self):
891891
schema = Schema(
892-
fields=[SchemaField("name", "STRING")],
892+
fields=[SchemaField("name", "STRING", description=None, policy_tags=None)],
893893
foreign_type_info="TestInfo",
894894
)
895895
result = _to_schema_fields(schema)
@@ -900,7 +900,6 @@ def test_valid_schema_object(self):
900900
assert result.to_api_repr() == expected.to_api_repr()
901901

902902

903-
# Testing the new Schema Class =================
904903
class TestSchemaObject: # New test class for Schema object interactions
905904
def test_schema_object_field_access(self):
906905
schema = Schema(
@@ -909,9 +908,10 @@ def test_schema_object_field_access(self):
909908
SchemaField("age", "INTEGER"),
910909
]
911910
)
911+
912912
assert len(schema) == 2
913-
assert schema[0].name == "name" # Access fields using indexing
914-
assert schema[1].field_type == "INTEGER"
913+
assert schema[0]["name"] == "name" # Access fields using indexing
914+
assert schema[1]["type"] == "INTEGER"
915915

916916
def test_schema_object_foreign_type_info(self):
917917
schema = Schema(foreign_type_info="External")
@@ -930,7 +930,7 @@ def test_str(self):
930930
)
931931
assert (
932932
str(schema)
933-
== "Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], TestInfo)"
933+
== "Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], TestInfo)"
934934
)
935935

936936
@pytest.mark.parametrize(
@@ -941,12 +941,12 @@ def test_str(self):
941941
fields=[SchemaField("name", "STRING")],
942942
foreign_type_info="TestInfo",
943943
),
944-
"Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], 'TestInfo')",
944+
"Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], 'TestInfo')",
945945
id="repr with foreign type info",
946946
),
947947
pytest.param(
948948
Schema(fields=[SchemaField("name", "STRING")]),
949-
"Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], None)",
949+
"Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], None)",
950950
id="repr without foreign type info",
951951
),
952952
],
@@ -958,8 +958,7 @@ def test_schema_iteration(self):
958958
schema = Schema(
959959
fields=[SchemaField("name", "STRING"), SchemaField("age", "INTEGER")]
960960
)
961-
962-
field_names = [field.name for field in schema]
961+
field_names = [field["name"] for field in schema]
963962
assert field_names == ["name", "age"]
964963

965964
def test_schema_object_mutability(self): # Tests __setitem__ and __delitem__
@@ -1002,19 +1001,15 @@ def test_schema_extend(self):
10021001
foreign_type_info="TestInfo",
10031002
),
10041003
{
1005-
"_fields": [
1006-
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
1007-
],
1004+
"fields": [{"name": "name", "mode": "NULLABLE", "type": "STRING"}],
10081005
"foreignTypeInfo": "TestInfo",
10091006
},
10101007
id="repr with foreign type info",
10111008
),
10121009
pytest.param(
10131010
Schema(fields=[SchemaField("name", "STRING")]),
10141011
{
1015-
"_fields": [
1016-
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
1017-
],
1012+
"fields": [{"name": "name", "mode": "NULLABLE", "type": "STRING"}],
10181013
"foreignTypeInfo": None,
10191014
},
10201015
id="repr without foreign type info",
@@ -1029,25 +1024,35 @@ def test_to_api_repr(self, schema, expected_api_repr):
10291024
[
10301025
pytest.param(
10311026
{
1032-
"_fields": [
1027+
"fields": [
10331028
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
10341029
],
10351030
"foreignTypeInfo": "TestInfo",
10361031
},
10371032
Schema(
1038-
fields=[SchemaField("name", "STRING")],
1033+
fields=[
1034+
SchemaField(
1035+
"name", "STRING", description=None, policy_tags=None
1036+
)
1037+
],
10391038
foreign_type_info="TestInfo",
10401039
),
10411040
id="repr with foreign type info",
10421041
),
10431042
pytest.param(
10441043
{
1045-
"_fields": [
1044+
"fields": [
10461045
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
10471046
],
10481047
"foreignTypeInfo": None,
10491048
},
1050-
Schema(fields=[SchemaField("name", "STRING")]),
1049+
Schema(
1050+
fields=[
1051+
SchemaField(
1052+
"name", "STRING", description=None, policy_tags=None
1053+
)
1054+
]
1055+
),
10511056
id="repr without foreign type info",
10521057
),
10531058
],
@@ -1059,13 +1064,11 @@ def test_from_api_repr(self, api_repr, expected):
10591064
THEN it will have the same representation a Schema object created
10601065
directly and displayed as a dict.
10611066
"""
1067+
10621068
result = Schema.from_api_repr(api_repr)
10631069
assert result.to_api_repr() == expected.to_api_repr()
10641070

10651071

1066-
# END PYTEST BASED SCHEMA TESTS ====================
1067-
1068-
10691072
class TestPolicyTags(unittest.TestCase):
10701073
@staticmethod
10711074
def _get_target_class():

0 commit comments

Comments
 (0)