Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions shotgun_api3/lib/mockgun/mockgun.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,25 @@ def find(
# handle the ordering of the recordset
if order:
# order: [{"field_name": "code", "direction": "asc"}, ... ]
def sort_none(k, order_field):
"""
Handle sorting of None consistently.
Note: Doesn't handle [checkbox, serializable, url].
"""
field_type = self._get_field_type(k["type"], order_field)
value = k[order_field]
if value is not None:
return value
elif field_type in ("number", "percent", "duration"):
return 0
elif field_type == "float":
return 0.0
elif field_type in ("text", "entity_type", "date", "list", "status_list"):
return ""
elif field_type == "date_time":
return datetime.datetime(datetime.MINYEAR, 1, 1)
return None

for order_entry in order:
if "field_name" not in order_entry:
raise ValueError("Order clauses must be list of dicts with keys 'field_name' and 'direction'!")
Expand All @@ -305,7 +324,11 @@ def find(
else:
raise ValueError("Unknown ordering direction")

results = sorted(results, key=lambda k: k[order_field], reverse=desc_order)
results = sorted(
results,
key=lambda k: sort_none(k, order_field),
reverse=desc_order,
)

if fields is None:
fields = set(["type", "id"])
Expand Down Expand Up @@ -608,6 +631,20 @@ def _compare(self, field_type, lval, operator, rval):
if operator == "is":
return lval == rval
elif field_type == "text":
# Some operations expect a list but can deal with a single value
if operator in ("in", "not_in") and not isinstance(rval, list):
rval = [rval]
# Some operation expect a string but can deal with None
elif operator in ("starts_with", "ends_with", "contains", "not_contains"):
lval = lval or ''
rval = rval or ''
# Shotgun string comparison is case insensitive
lval = lval.lower() if lval is not None else None
if isinstance(rval, list):
rval = [val.lower() if val is not None else None for val in rval]
else:
rval = rval.lower() if rval is not None else None

if operator == "is":
return lval == rval
elif operator == "is_not":
Expand All @@ -617,7 +654,7 @@ def _compare(self, field_type, lval, operator, rval):
elif operator == "contains":
return rval in lval
elif operator == "not_contains":
return lval not in rval
return rval not in lval
elif operator == "starts_with":
return lval.startswith(rval)
elif operator == "ends_with":
Expand Down Expand Up @@ -831,7 +868,10 @@ def _update_row(self, entity_type, row, data, multi_entity_update_modes=None):
update_mode = multi_entity_update_modes.get(field, "set") if multi_entity_update_modes else "set"

if update_mode == "add":
row[field] += [{"type": item["type"], "id": item["id"]} for item in data[field]]
for item in data[field]:
new_item = {"type": item["type"], "id": item["id"]}
if new_item not in row[field]:
row[field].append(new_item)
elif update_mode == "remove":
row[field] = [
item
Expand Down
231 changes: 223 additions & 8 deletions tests/test_mockgun.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
and can be run on their own by typing "python test_mockgun.py".
"""

import datetime
import re
import os
import unittest
Expand Down Expand Up @@ -188,14 +189,171 @@ def setUp(self):
self._mockgun = Mockgun(
"https://test.shotgunstudio.com", login="user", password="1234"
)
self._user = self._mockgun.create("HumanUser", {"login": "user"})
self._user1 = self._mockgun.create("HumanUser", {"login": "user"})
self._user2 = self._mockgun.create("HumanUser", {"login": None})

def test_operator_is(self):
"""
Ensure is operator work.
"""
actual = self._mockgun.find("HumanUser", [["login", "is", "user"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_is_none(self):
"""
Ensure is operator work when used with None.
"""
actual = self._mockgun.find("HumanUser", [["login", "is", None]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_is_case_sensitivity(self):
"""
Ensure is operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "is", "USER"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_is_not(self):
"""
Ensure the is_not operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "is_not", "user"]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_is_not_none(self):
"""
Ensure the is_not operator works when used with None.
"""
actual = self._mockgun.find("HumanUser", [["login", "is_not", None]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_is_not_case_sensitivity(self):
"""
Ensure the is_not operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "is_not", "USER"]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_in(self):
"""
Ensure the in operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "in", ["user"]]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_in_none(self):
"""
Ensure the in operator works with a list containing None.
"""
actual = self._mockgun.find("HumanUser", [["login", "in", [None]]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_in_case_sensitivity(self):
"""
Ensure the in operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "in", ["USER"]]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_not_in(self):
"""
Ensure the not_in operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "not_in", ["foo"]]])
expected = [
{"type": "HumanUser", "id": self._user1["id"]},
{"type": "HumanUser", "id": self._user2["id"]},
]
self.assertEqual(expected, actual)

def test_operator_not_in_none(self):
"""
Ensure the not_not operator works with a list containing None.
"""
actual = self._mockgun.find("HumanUser", [["login", "not_in", [None]]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_not_in_case_sensitivity(self):
"""
Ensure the not_in operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "not_in", ["USER"]]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_contains(self):
"""
Ensures contains operator works.
Ensures the contains operator works.
"""
item = self._mockgun.find_one("HumanUser", [["login", "contains", "se"]])
self.assertTrue(item)
actual = self._mockgun.find("HumanUser", [["login", "contains", "se"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_contains_case_sensitivity(self):
"""
Ensure the contains operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "contains", "SE"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_not_contains(self):
"""
Ensure the not_contains operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "not_contains", "user"]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_not_contains_case_sensitivity(self):
"""
Ensure the not_contains operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "not_contains", "USER"]])
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
self.assertEqual(expected, actual)

def test_operator_starts_with(self):
"""
Ensure the starts_with operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "starts_with", "us"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_starts_with_case_sensitivity(self):
"""
Ensure the starts_with operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "starts_with", "US"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_ends_with(self):
"""
Ensure the ends_with operator works.
"""
actual = self._mockgun.find("HumanUser", [["login", "ends_with", "er"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)

def test_operator_ends_with_case_sensitivity(self):
"""
Ensure the starts_with operator is case insensitive.
"""
actual = self._mockgun.find("HumanUser", [["login", "ends_with", "ER"]])
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
self.assertEqual(expected, actual)


class TestMultiEntityFieldComparison(unittest.TestCase):
Expand Down Expand Up @@ -345,10 +503,12 @@ def test_update_add(self):
"""
Ensures that "add" multi_entity_update_mode works.
"""
# Attempts to add _version2
# It already exists on the playlist and should not be duplicated
self._mockgun.update(
"Playlist",
self._add_playlist["id"],
{"versions": [self._version3]},
{"versions": [self._version2, self._version3]},
multi_entity_update_modes={"versions": "add"},
)

Expand Down Expand Up @@ -429,15 +589,29 @@ def setUp(self):
self._prj2_link = self._mockgun.create("Project", {"name": "prj2"})

self._shot1 = self._mockgun.create(
"Shot", {"code": "shot1", "project": self._prj1_link}
"Shot",
{
"code": "shot1",
"project": self._prj1_link,
"description": "a",
"sg_cut_order": 2,
},
)

self._shot2 = self._mockgun.create(
"Shot", {"code": "shot2", "project": self._prj1_link}
"Shot", {"code": "shot2", "project": self._prj1_link, "sg_cut_order": 1}
)

self._shot3 = self._mockgun.create(
"Shot", {"code": "shot3", "project": self._prj2_link}
"Shot", {"code": "shot3", "project": self._prj2_link, "description": "b"}
)

self._user1 = self._mockgun.create(
"HumanUser", {"login": "user1", "password_strength": 0.2}
)

self._user2 = self._mockgun.create(
"HumanUser", {"login": "user2", "created_at": datetime.datetime(2025, 1, 1)}
)

def test_simple_filter_operators(self):
Expand Down Expand Up @@ -468,6 +642,47 @@ def test_simple_filter_operators(self):

self.assertEqual(len(shots), 0)

def test_ordered_filter_operator(self):
"""
Test use of the order feature of filter_operator on supported data types.
"""
find_args = ["Shot", [], ["code"]]

# str field
shots = self._mockgun.find(
*find_args, order=[{"field_name": "description", "direction": "asc"}]
)
self.assertEqual([s["code"] for s in shots], ["shot2", "shot1", "shot3"])

shots = self._mockgun.find(
*find_args, order=[{"field_name": "description", "direction": "desc"}]
)
self.assertEqual([s["code"] for s in shots], ["shot3", "shot1", "shot2"])

# int field
shots = self._mockgun.find(
*find_args, order=[{"field_name": "sg_cut_order", "direction": "asc"}]
)
self.assertEqual([s["code"] for s in shots], ["shot3", "shot2", "shot1"])

# float field
users = self._mockgun.find(
"HumanUser",
[],
["login"],
order=[{"field_name": "password_strength", "direction": "asc"}],
)
self.assertEqual([u["login"] for u in users], ["user2", "user1"])

# date_time field
users = self._mockgun.find(
"HumanUser",
[],
["login"],
order=[{"field_name": "created_at", "direction": "asc"}],
)
self.assertEqual([u["login"] for u in users], ["user1", "user2"])

def test_nested_filter_operators(self):
"""
Tests a the use of the filter_operator nested
Expand Down
Loading