diff --git a/shotgun_api3/lib/mockgun/mockgun.py b/shotgun_api3/lib/mockgun/mockgun.py index 1122c413..d3c73c86 100644 --- a/shotgun_api3/lib/mockgun/mockgun.py +++ b/shotgun_api3/lib/mockgun/mockgun.py @@ -116,7 +116,7 @@ import datetime -from ... import sg_timezone, ShotgunError +from ... import ShotgunError from ...shotgun import _Config from .errors import MockgunError from .schema import SchemaFactory @@ -220,7 +220,6 @@ def __init__(self, ################################################################################################### # public API methods - def get_session_token(self): return "bogus_session_token" @@ -245,15 +244,16 @@ def schema_field_read(self, entity_type, field_name=None): else: return dict((k, v) for k, v in self._schema[entity_type].items() if k == field_name) - - def find(self, entity_type, filters, fields=None, order=None, filter_operator=None, limit=0, retired_only=False, page=0): - + def find( + self, entity_type, filters, fields=None, order=None, filter_operator=None, + limit=0, retired_only=False, page=0 + ): self.finds += 1 self._validate_entity_type(entity_type) # do not validate custom fields - this makes it hard to mock up a field quickly - #self._validate_entity_fields(entity_type, fields) + # self._validate_entity_fields(entity_type, fields) # FIXME: This should be refactored so that we can use the complex filer # style in nested filter operations. @@ -271,10 +271,10 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No if len(f["values"]) != 1: # {'path': 'id', 'relation': 'in', 'values': [1,2,3]} --> ["id", "in", [1,2,3]] - resolved_filters.append([ f["path"], f["relation"], f["values"] ]) + resolved_filters.append([f["path"], f["relation"], f["values"]]) else: # {'path': 'id', 'relation': 'is', 'values': [3]} --> ["id", "is", 3] - resolved_filters.append([ f["path"], f["relation"], f["values"][0] ]) + resolved_filters.append([f["path"], f["relation"], f["values"][0]]) else: # traditiona style sg filters @@ -315,9 +315,14 @@ def find(self, entity_type, filters, fields=None, order=None, filter_operator=No return val - - def find_one(self, entity_type, filters, fields=None, order=None, filter_operator=None, retired_only=False): - results = self.find(entity_type, filters, fields=fields, order=order, filter_operator=filter_operator, retired_only=retired_only) + def find_one( + self, entity_type, filters, fields=None, order=None, filter_operator=None, + retired_only=False + ): + results = self.find( + entity_type, filters, fields=fields, + order=order, filter_operator=filter_operator, retired_only=retired_only + ) return results[0] if results else None def batch(self, requests): @@ -440,22 +445,44 @@ def _validate_entity_data(self, entity_type, data): if field_info["data_type"]["value"] == "multi_entity": if not isinstance(item, list): - raise ShotgunError("%s.%s is of type multi_entity, but data %s is not a list" % (entity_type, field, item)) + raise ShotgunError( + "%s.%s is of type multi_entity, but data %s is not a list" % + (entity_type, field, item) + ) elif item and any(not isinstance(sub_item, dict) for sub_item in item): - raise ShotgunError("%s.%s is of type multi_entity, but data %s contains a non-dictionary" % (entity_type, field, item)) + raise ShotgunError( + "%s.%s is of type multi_entity, but data %s contains a non-dictionary" % + (entity_type, field, item) + ) elif item and any("id" not in sub_item or "type" not in sub_item for sub_item in item): - raise ShotgunError("%s.%s is of type multi-entity, but an item in data %s does not contain 'type' and 'id'" % (entity_type, field, item)) - elif item and any(sub_item["type"] not in field_info["properties"]["valid_types"]["value"] for sub_item in item): - raise ShotgunError("%s.%s is of multi-type entity, but an item in data %s has an invalid type (expected one of %s)" % (entity_type, field, item, field_info["properties"]["valid_types"]["value"])) - + raise ShotgunError( + "%s.%s is of type multi-entity, but an item in data %s does not contain 'type' and 'id'" % + (entity_type, field, item) + ) + elif item and any( + sub_item["type"] not in field_info["properties"]["valid_types"]["value"] for sub_item in item + ): + raise ShotgunError( + "%s.%s is of multi-type entity, but an item in data %s has an invalid type (expected one of %s)" + % (entity_type, field, item, field_info["properties"]["valid_types"]["value"]) + ) elif field_info["data_type"]["value"] == "entity": if not isinstance(item, dict): - raise ShotgunError("%s.%s is of type entity, but data %s is not a dictionary" % (entity_type, field, item)) + raise ShotgunError( + "%s.%s is of type entity, but data %s is not a dictionary" % + (entity_type, field, item) + ) elif "id" not in item or "type" not in item: - raise ShotgunError("%s.%s is of type entity, but data %s does not contain 'type' and 'id'" % (entity_type, field, item)) - #elif item["type"] not in field_info["properties"]["valid_types"]["value"]: - # raise ShotgunError("%s.%s is of type entity, but data %s has an invalid type (expected one of %s)" % (entity_type, field, item, field_info["properties"]["valid_types"]["value"])) + raise ShotgunError( + "%s.%s is of type entity, but data %s does not contain 'type' and 'id'" + % (entity_type, field, item) + ) + # elif item["type"] not in field_info["properties"]["valid_types"]["value"]: + # raise ShotgunError( + # "%s.%s is of type entity, but data %s has an invalid type (expected one of %s)" % + # (entity_type, field, item, field_info["properties"]["valid_types"]["value"]) + # ) else: try: @@ -472,10 +499,16 @@ def _validate_entity_data(self, entity_type, data): "status_list": basestring, "url": dict}[sg_type] except KeyError: - raise ShotgunError("Field %s.%s: Handling for Shotgun type %s is not implemented" % (entity_type, field, sg_type)) + raise ShotgunError( + "Field %s.%s: Handling for Shotgun type %s is not implemented" % + (entity_type, field, sg_type) + ) if not isinstance(item, python_type): - raise ShotgunError("%s.%s is of type %s, but data %s is not of type %s" % (entity_type, field, type(item), sg_type, python_type)) + raise ShotgunError( + "%s.%s is of type %s, but data %s is not of type %s" % + (entity_type, field, type(item), sg_type, python_type) + ) # TODO: add check for correct timezone @@ -641,6 +674,9 @@ def _get_field_from_row(self, entity_type, row, field): sub_field_value = self._get_field_from_row(entity_type2, entity, field3) values.append(sub_field_value) return values + # The field is not set, so return None. + elif field_value is None: + return None # not multi entity, must be entity. elif not isinstance(field_value, dict): raise ShotgunError("Invalid deep query field %s.%s" % (entity_type, field)) @@ -652,7 +688,7 @@ def _get_field_from_row(self, entity_type, row, field): # ok so looks like the value is an entity link # e.g. db contains: {"sg_sequence": {"type":"Sequence", "id": 123 } } - linked_row = self._db[ field_value["type"] ][ field_value["id"] ] + linked_row = self._db[field_value["type"]][field_value["id"]] return self._get_field_from_row(entity_type2, linked_row, field3) else: @@ -769,7 +805,6 @@ def _row_matches_filters(self, entity_type, row, filters, filter_operator, retir else: raise ShotgunError("%s is not a valid filter operator" % filter_operator) - def _update_row(self, entity_type, row, data): for field in data: field_type = self._get_field_type(entity_type, field) @@ -780,11 +815,6 @@ def _update_row(self, entity_type, row, data): else: row[field] = data[field] - def _validate_entity_exists(self, entity_type, entity_id): if entity_id not in self._db[entity_type]: raise ShotgunError("No entity of type %s exists with id %s" % (entity_type, entity_id)) - - - - diff --git a/tests/test_mockgun.py b/tests/test_mockgun.py index 5a5d3411..2bd4ae10 100644 --- a/tests/test_mockgun.py +++ b/tests/test_mockgun.py @@ -148,12 +148,12 @@ def setUp(self): """ self._mockgun = Mockgun("https://test.shotgunstudio.com", login="user", password="1234") - self._project_link = self._mockgun.create("Project", {"name": "project"}) + self._project_link = self._mockgun.create("Project", {"name": "project", "archived": False}) # This entity will ensure that a populated link field will be comparable. self._mockgun.create( "PipelineConfiguration", - {"code": "with_project", "project": self._project_link} + {"code": "with_project", "project": self._project_link, } ) # This entity will ensure that an unpopulated link field will be comparable. @@ -180,6 +180,16 @@ def test_searching_for_initialized_entity_field(self): items = self._mockgun.find("PipelineConfiguration", [["project", "is_not", self._project_link]]) self.assertEqual(len(items), 1) + def test_find_entity_with_none_link(self): + """ + Make sure that we can search for sub entity fields on entities that have the field not set. + """ + # The pipeline configuration without_project doesn't have the project field set, so we're expecting + # it to not be returned here. + items = self._mockgun.find("PipelineConfiguration", [["project.Project.archived", "is", False]]) + self.assertEqual(len(items), 1) + self.assertEqual(items[0]["id"], self._project_link["id"]) + class TestTextFieldOperators(TestBaseWithExceptionTests): """