Skip to content

Commit

Permalink
update tests based on Schema superclass UserList
Browse files Browse the repository at this point in the history
  • Loading branch information
chalmerlowe committed Dec 27, 2024
1 parent ef2b95d commit cfa609c
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 58 deletions.
80 changes: 46 additions & 34 deletions google/cloud/bigquery/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
from __future__ import annotations

import collections

import copy
import enum
from typing import Any, Dict, Iterable, Optional, Union, cast
from typing import Any, Dict, Iterable, Optional, Union, cast, List, Mapping

from google.cloud.bigquery import standard_sql
from google.cloud.bigquery._helpers import (
Expand Down Expand Up @@ -524,7 +525,7 @@ def __repr__(self):


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


def _to_schema_fields(schema):
"""TODO docstring
CAST a list of elements to either:
* a Schema object with SchemaFields and an attribute
* a list of SchemaFields but no attribute
def _to_schema_fields(
schema: Union[Schema, List[Union[SchemaField, Mapping[str, Any]]]]
) -> Union[Schema, List[SchemaField]]:
"""Convert the input to either a Schema object OR a list of SchemaField objects.
This helper method ensures that the fields in the schema are SchemaField objects.
It accepts:
* A :class:`~google.cloud.bigquery.schema.Schema` instance: It will
convert items that are mappings to
:class:`~google.cloud.bigquery.schema.SchemaField` instances and
preserve foreign_type_info.
* A list of
:class:`~google.cloud.bigquery.schema.SchemaField` instances.
* A list of mappings: It will convert each of the mapping items to
a :class:`~google.cloud.bigquery.schema.SchemaField` instance.
Args:
schema: The schema to convert.
Returns:
The schema as a list of SchemaField objects or a Schema object.
Raises:
ValueError: If the items in ``schema`` are not valid.
"""

for field in schema:
if not isinstance(field, (SchemaField, collections.abc.Mapping)):
raise ValueError(
Expand Down Expand Up @@ -937,57 +961,45 @@ def foreign_type_info(self, value: str) -> None:
@property
def _fields(self) -> Any:
"""TODO: docstring"""
return self._properties.get("_fields")
return self._properties.get("fields")

@_fields.setter
def _fields(self, value: list) -> None:
value = _isinstance_or_raise(value, list, none_allowed=True)
self._properties["_fields"] = value
value = _build_schema_resource(value)
self._properties["fields"] = value

@property
def data(self):
return self._properties.get("_fields")
return self._properties.get("fields")

@data.setter
def data(self, value: list):
# for simplicity, no validation in this proof of concept
self._properties["_fields"] = value

def __len__(self):
return len(self._fields)

def __getitem__(self, index):
return self._fields[index]

def __setitem__(self, index, value):
self._fields[index] = value

def __delitem__(self, index):
del self._fields[index]

def __iter__(self):
return iter(self._fields)
value = _isinstance_or_raise(value, list, none_allowed=True)
value = _build_schema_resource(value)
self._properties["fields"] = value

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

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

def append(self, item):
self._fields.append(item)

def extend(self, iterable):
self._fields.extend(iterable)

def to_api_repr(self) -> dict:
"""Build an API representation of this object.
Returns:
Dict[str, Any]:
A dictionary in the format used by the BigQuery API.
"""
return copy.deepcopy(self._properties)
# If this is a RECORD type, then sub-fields are also included,
# add this to the serialized representation.
answer = self._properties.copy()
schemafields = any([isinstance(f, SchemaField) for f in self._fields])
if schemafields:
answer["fields"] = [f.to_api_repr() for f in self._fields]
return answer

@classmethod
def from_api_repr(cls, resource: dict) -> Schema:
Expand All @@ -1002,5 +1014,5 @@ def from_api_repr(cls, resource: dict) -> Schema:
An instance of the class initialized with data from 'resource'.
"""
config = cls("")
config._properties = copy.deepcopy(resource)
config._properties = copy.copy(resource)
return config
51 changes: 27 additions & 24 deletions tests/unit/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,6 @@ def test_from_api_repr_none(self):
self.assertEqual(None, self._get_target_class().from_api_repr(None))


# BEGIN PYTEST BASED SCHEMA TESTS ====================
@pytest.fixture
def basic_resource():
return {
Expand Down Expand Up @@ -826,6 +825,7 @@ def test_build_schema_resource(self, fields, expected_resource):

class TestToSchemaFields: # Test class for _to_schema_fields
def test_invalid_type(self):
"""Invalid list of tuples instead of list of mappings"""
schema = [
("full_name", "STRING", "REQUIRED"),
("address", "STRING", "REQUIRED"),
Expand All @@ -846,7 +846,7 @@ def test_schema_fields_sequence(self):
def test_invalid_mapping_representation(self):
schema = [
{"name": "full_name", "type": "STRING", "mode": "REQUIRED"},
{"name": "address", "typeooo": "STRING", "mode": "REQUIRED"},
{"name": "address", "invalid_key": "STRING", "mode": "REQUIRED"},
]
with pytest.raises(Exception): # Or a more specific exception if known
_to_schema_fields(schema)
Expand Down Expand Up @@ -889,7 +889,7 @@ def test_valid_mapping_representation(self, schema, expected_schema):

def test_valid_schema_object(self):
schema = Schema(
fields=[SchemaField("name", "STRING")],
fields=[SchemaField("name", "STRING", description=None, policy_tags=None)],
foreign_type_info="TestInfo",
)
result = _to_schema_fields(schema)
Expand All @@ -900,7 +900,6 @@ def test_valid_schema_object(self):
assert result.to_api_repr() == expected.to_api_repr()


# Testing the new Schema Class =================
class TestSchemaObject: # New test class for Schema object interactions
def test_schema_object_field_access(self):
schema = Schema(
Expand All @@ -909,9 +908,10 @@ def test_schema_object_field_access(self):
SchemaField("age", "INTEGER"),
]
)

assert len(schema) == 2
assert schema[0].name == "name" # Access fields using indexing
assert schema[1].field_type == "INTEGER"
assert schema[0]["name"] == "name" # Access fields using indexing
assert schema[1]["type"] == "INTEGER"

def test_schema_object_foreign_type_info(self):
schema = Schema(foreign_type_info="External")
Expand All @@ -930,7 +930,7 @@ def test_str(self):
)
assert (
str(schema)
== "Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], TestInfo)"
== "Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], TestInfo)"
)

@pytest.mark.parametrize(
Expand All @@ -941,12 +941,12 @@ def test_str(self):
fields=[SchemaField("name", "STRING")],
foreign_type_info="TestInfo",
),
"Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], 'TestInfo')",
"Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], 'TestInfo')",
id="repr with foreign type info",
),
pytest.param(
Schema(fields=[SchemaField("name", "STRING")]),
"Schema([SchemaField('name', 'STRING', 'NULLABLE', None, None, (), None)], None)",
"Schema([{'name': 'name', 'mode': 'NULLABLE', 'type': 'STRING'}], None)",
id="repr without foreign type info",
),
],
Expand All @@ -958,8 +958,7 @@ def test_schema_iteration(self):
schema = Schema(
fields=[SchemaField("name", "STRING"), SchemaField("age", "INTEGER")]
)

field_names = [field.name for field in schema]
field_names = [field["name"] for field in schema]
assert field_names == ["name", "age"]

def test_schema_object_mutability(self): # Tests __setitem__ and __delitem__
Expand Down Expand Up @@ -1002,19 +1001,15 @@ def test_schema_extend(self):
foreign_type_info="TestInfo",
),
{
"_fields": [
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
],
"fields": [{"name": "name", "mode": "NULLABLE", "type": "STRING"}],
"foreignTypeInfo": "TestInfo",
},
id="repr with foreign type info",
),
pytest.param(
Schema(fields=[SchemaField("name", "STRING")]),
{
"_fields": [
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
],
"fields": [{"name": "name", "mode": "NULLABLE", "type": "STRING"}],
"foreignTypeInfo": None,
},
id="repr without foreign type info",
Expand All @@ -1029,25 +1024,35 @@ def test_to_api_repr(self, schema, expected_api_repr):
[
pytest.param(
{
"_fields": [
"fields": [
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
],
"foreignTypeInfo": "TestInfo",
},
Schema(
fields=[SchemaField("name", "STRING")],
fields=[
SchemaField(
"name", "STRING", description=None, policy_tags=None
)
],
foreign_type_info="TestInfo",
),
id="repr with foreign type info",
),
pytest.param(
{
"_fields": [
"fields": [
SchemaField("name", "STRING", "NULLABLE", None, None, (), None)
],
"foreignTypeInfo": None,
},
Schema(fields=[SchemaField("name", "STRING")]),
Schema(
fields=[
SchemaField(
"name", "STRING", description=None, policy_tags=None
)
]
),
id="repr without foreign type info",
),
],
Expand All @@ -1059,13 +1064,11 @@ def test_from_api_repr(self, api_repr, expected):
THEN it will have the same representation a Schema object created
directly and displayed as a dict.
"""

result = Schema.from_api_repr(api_repr)
assert result.to_api_repr() == expected.to_api_repr()


# END PYTEST BASED SCHEMA TESTS ====================


class TestPolicyTags(unittest.TestCase):
@staticmethod
def _get_target_class():
Expand Down

0 comments on commit cfa609c

Please sign in to comment.