Skip to content

Commit 17620c9

Browse files
SG-37203 Apply mockgun improvements (#376)
* Apply #217 * Fix dict * Apply #376 * Apply #364
1 parent 418d72e commit 17620c9

File tree

2 files changed

+266
-11
lines changed

2 files changed

+266
-11
lines changed

shotgun_api3/lib/mockgun/mockgun.py

+43-3
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,25 @@ def find(
293293
# handle the ordering of the recordset
294294
if order:
295295
# order: [{"field_name": "code", "direction": "asc"}, ... ]
296+
def sort_none(k, order_field):
297+
"""
298+
Handle sorting of None consistently.
299+
Note: Doesn't handle [checkbox, serializable, url].
300+
"""
301+
field_type = self._get_field_type(k["type"], order_field)
302+
value = k[order_field]
303+
if value is not None:
304+
return value
305+
elif field_type in ("number", "percent", "duration"):
306+
return 0
307+
elif field_type == "float":
308+
return 0.0
309+
elif field_type in ("text", "entity_type", "date", "list", "status_list"):
310+
return ""
311+
elif field_type == "date_time":
312+
return datetime.datetime(datetime.MINYEAR, 1, 1)
313+
return None
314+
296315
for order_entry in order:
297316
if "field_name" not in order_entry:
298317
raise ValueError("Order clauses must be list of dicts with keys 'field_name' and 'direction'!")
@@ -305,7 +324,11 @@ def find(
305324
else:
306325
raise ValueError("Unknown ordering direction")
307326

308-
results = sorted(results, key=lambda k: k[order_field], reverse=desc_order)
327+
results = sorted(
328+
results,
329+
key=lambda k: sort_none(k, order_field),
330+
reverse=desc_order,
331+
)
309332

310333
if fields is None:
311334
fields = set(["type", "id"])
@@ -608,6 +631,20 @@ def _compare(self, field_type, lval, operator, rval):
608631
if operator == "is":
609632
return lval == rval
610633
elif field_type == "text":
634+
# Some operations expect a list but can deal with a single value
635+
if operator in ("in", "not_in") and not isinstance(rval, list):
636+
rval = [rval]
637+
# Some operation expect a string but can deal with None
638+
elif operator in ("starts_with", "ends_with", "contains", "not_contains"):
639+
lval = lval or ''
640+
rval = rval or ''
641+
# Shotgun string comparison is case insensitive
642+
lval = lval.lower() if lval is not None else None
643+
if isinstance(rval, list):
644+
rval = [val.lower() if val is not None else None for val in rval]
645+
else:
646+
rval = rval.lower() if rval is not None else None
647+
611648
if operator == "is":
612649
return lval == rval
613650
elif operator == "is_not":
@@ -617,7 +654,7 @@ def _compare(self, field_type, lval, operator, rval):
617654
elif operator == "contains":
618655
return rval in lval
619656
elif operator == "not_contains":
620-
return lval not in rval
657+
return rval not in lval
621658
elif operator == "starts_with":
622659
return lval.startswith(rval)
623660
elif operator == "ends_with":
@@ -831,7 +868,10 @@ def _update_row(self, entity_type, row, data, multi_entity_update_modes=None):
831868
update_mode = multi_entity_update_modes.get(field, "set") if multi_entity_update_modes else "set"
832869

833870
if update_mode == "add":
834-
row[field] += [{"type": item["type"], "id": item["id"]} for item in data[field]]
871+
for item in data[field]:
872+
new_item = {"type": item["type"], "id": item["id"]}
873+
if new_item not in row[field]:
874+
row[field].append(new_item)
835875
elif update_mode == "remove":
836876
row[field] = [
837877
item

tests/test_mockgun.py

+223-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
and can be run on their own by typing "python test_mockgun.py".
3636
"""
3737

38+
import datetime
3839
import re
3940
import os
4041
import unittest
@@ -188,14 +189,171 @@ def setUp(self):
188189
self._mockgun = Mockgun(
189190
"https://test.shotgunstudio.com", login="user", password="1234"
190191
)
191-
self._user = self._mockgun.create("HumanUser", {"login": "user"})
192+
self._user1 = self._mockgun.create("HumanUser", {"login": "user"})
193+
self._user2 = self._mockgun.create("HumanUser", {"login": None})
194+
195+
def test_operator_is(self):
196+
"""
197+
Ensure is operator work.
198+
"""
199+
actual = self._mockgun.find("HumanUser", [["login", "is", "user"]])
200+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
201+
self.assertEqual(expected, actual)
202+
203+
def test_operator_is_none(self):
204+
"""
205+
Ensure is operator work when used with None.
206+
"""
207+
actual = self._mockgun.find("HumanUser", [["login", "is", None]])
208+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
209+
self.assertEqual(expected, actual)
210+
211+
def test_operator_is_case_sensitivity(self):
212+
"""
213+
Ensure is operator is case insensitive.
214+
"""
215+
actual = self._mockgun.find("HumanUser", [["login", "is", "USER"]])
216+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
217+
self.assertEqual(expected, actual)
218+
219+
def test_operator_is_not(self):
220+
"""
221+
Ensure the is_not operator works.
222+
"""
223+
actual = self._mockgun.find("HumanUser", [["login", "is_not", "user"]])
224+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
225+
self.assertEqual(expected, actual)
226+
227+
def test_operator_is_not_none(self):
228+
"""
229+
Ensure the is_not operator works when used with None.
230+
"""
231+
actual = self._mockgun.find("HumanUser", [["login", "is_not", None]])
232+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
233+
self.assertEqual(expected, actual)
234+
235+
def test_operator_is_not_case_sensitivity(self):
236+
"""
237+
Ensure the is_not operator is case insensitive.
238+
"""
239+
actual = self._mockgun.find("HumanUser", [["login", "is_not", "USER"]])
240+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
241+
self.assertEqual(expected, actual)
242+
243+
def test_operator_in(self):
244+
"""
245+
Ensure the in operator works.
246+
"""
247+
actual = self._mockgun.find("HumanUser", [["login", "in", ["user"]]])
248+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
249+
self.assertEqual(expected, actual)
250+
251+
def test_operator_in_none(self):
252+
"""
253+
Ensure the in operator works with a list containing None.
254+
"""
255+
actual = self._mockgun.find("HumanUser", [["login", "in", [None]]])
256+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
257+
self.assertEqual(expected, actual)
258+
259+
def test_operator_in_case_sensitivity(self):
260+
"""
261+
Ensure the in operator is case insensitive.
262+
"""
263+
actual = self._mockgun.find("HumanUser", [["login", "in", ["USER"]]])
264+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
265+
self.assertEqual(expected, actual)
266+
267+
def test_operator_not_in(self):
268+
"""
269+
Ensure the not_in operator works.
270+
"""
271+
actual = self._mockgun.find("HumanUser", [["login", "not_in", ["foo"]]])
272+
expected = [
273+
{"type": "HumanUser", "id": self._user1["id"]},
274+
{"type": "HumanUser", "id": self._user2["id"]},
275+
]
276+
self.assertEqual(expected, actual)
277+
278+
def test_operator_not_in_none(self):
279+
"""
280+
Ensure the not_not operator works with a list containing None.
281+
"""
282+
actual = self._mockgun.find("HumanUser", [["login", "not_in", [None]]])
283+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
284+
self.assertEqual(expected, actual)
285+
286+
def test_operator_not_in_case_sensitivity(self):
287+
"""
288+
Ensure the not_in operator is case insensitive.
289+
"""
290+
actual = self._mockgun.find("HumanUser", [["login", "not_in", ["USER"]]])
291+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
292+
self.assertEqual(expected, actual)
192293

193294
def test_operator_contains(self):
194295
"""
195-
Ensures contains operator works.
296+
Ensures the contains operator works.
196297
"""
197-
item = self._mockgun.find_one("HumanUser", [["login", "contains", "se"]])
198-
self.assertTrue(item)
298+
actual = self._mockgun.find("HumanUser", [["login", "contains", "se"]])
299+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
300+
self.assertEqual(expected, actual)
301+
302+
def test_operator_contains_case_sensitivity(self):
303+
"""
304+
Ensure the contains operator is case insensitive.
305+
"""
306+
actual = self._mockgun.find("HumanUser", [["login", "contains", "SE"]])
307+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
308+
self.assertEqual(expected, actual)
309+
310+
def test_operator_not_contains(self):
311+
"""
312+
Ensure the not_contains operator works.
313+
"""
314+
actual = self._mockgun.find("HumanUser", [["login", "not_contains", "user"]])
315+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
316+
self.assertEqual(expected, actual)
317+
318+
def test_operator_not_contains_case_sensitivity(self):
319+
"""
320+
Ensure the not_contains operator is case insensitive.
321+
"""
322+
actual = self._mockgun.find("HumanUser", [["login", "not_contains", "USER"]])
323+
expected = [{"type": "HumanUser", "id": self._user2["id"]}]
324+
self.assertEqual(expected, actual)
325+
326+
def test_operator_starts_with(self):
327+
"""
328+
Ensure the starts_with operator works.
329+
"""
330+
actual = self._mockgun.find("HumanUser", [["login", "starts_with", "us"]])
331+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
332+
self.assertEqual(expected, actual)
333+
334+
def test_operator_starts_with_case_sensitivity(self):
335+
"""
336+
Ensure the starts_with operator is case insensitive.
337+
"""
338+
actual = self._mockgun.find("HumanUser", [["login", "starts_with", "US"]])
339+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
340+
self.assertEqual(expected, actual)
341+
342+
def test_operator_ends_with(self):
343+
"""
344+
Ensure the ends_with operator works.
345+
"""
346+
actual = self._mockgun.find("HumanUser", [["login", "ends_with", "er"]])
347+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
348+
self.assertEqual(expected, actual)
349+
350+
def test_operator_ends_with_case_sensitivity(self):
351+
"""
352+
Ensure the starts_with operator is case insensitive.
353+
"""
354+
actual = self._mockgun.find("HumanUser", [["login", "ends_with", "ER"]])
355+
expected = [{"type": "HumanUser", "id": self._user1["id"]}]
356+
self.assertEqual(expected, actual)
199357

200358

201359
class TestMultiEntityFieldComparison(unittest.TestCase):
@@ -345,10 +503,12 @@ def test_update_add(self):
345503
"""
346504
Ensures that "add" multi_entity_update_mode works.
347505
"""
506+
# Attempts to add _version2
507+
# It already exists on the playlist and should not be duplicated
348508
self._mockgun.update(
349509
"Playlist",
350510
self._add_playlist["id"],
351-
{"versions": [self._version3]},
511+
{"versions": [self._version2, self._version3]},
352512
multi_entity_update_modes={"versions": "add"},
353513
)
354514

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

431591
self._shot1 = self._mockgun.create(
432-
"Shot", {"code": "shot1", "project": self._prj1_link}
592+
"Shot",
593+
{
594+
"code": "shot1",
595+
"project": self._prj1_link,
596+
"description": "a",
597+
"sg_cut_order": 2,
598+
},
433599
)
434600

435601
self._shot2 = self._mockgun.create(
436-
"Shot", {"code": "shot2", "project": self._prj1_link}
602+
"Shot", {"code": "shot2", "project": self._prj1_link, "sg_cut_order": 1}
437603
)
438604

439605
self._shot3 = self._mockgun.create(
440-
"Shot", {"code": "shot3", "project": self._prj2_link}
606+
"Shot", {"code": "shot3", "project": self._prj2_link, "description": "b"}
607+
)
608+
609+
self._user1 = self._mockgun.create(
610+
"HumanUser", {"login": "user1", "password_strength": 0.2}
611+
)
612+
613+
self._user2 = self._mockgun.create(
614+
"HumanUser", {"login": "user2", "created_at": datetime.datetime(2025, 1, 1)}
441615
)
442616

443617
def test_simple_filter_operators(self):
@@ -468,6 +642,47 @@ def test_simple_filter_operators(self):
468642

469643
self.assertEqual(len(shots), 0)
470644

645+
def test_ordered_filter_operator(self):
646+
"""
647+
Test use of the order feature of filter_operator on supported data types.
648+
"""
649+
find_args = ["Shot", [], ["code"]]
650+
651+
# str field
652+
shots = self._mockgun.find(
653+
*find_args, order=[{"field_name": "description", "direction": "asc"}]
654+
)
655+
self.assertEqual([s["code"] for s in shots], ["shot2", "shot1", "shot3"])
656+
657+
shots = self._mockgun.find(
658+
*find_args, order=[{"field_name": "description", "direction": "desc"}]
659+
)
660+
self.assertEqual([s["code"] for s in shots], ["shot3", "shot1", "shot2"])
661+
662+
# int field
663+
shots = self._mockgun.find(
664+
*find_args, order=[{"field_name": "sg_cut_order", "direction": "asc"}]
665+
)
666+
self.assertEqual([s["code"] for s in shots], ["shot3", "shot2", "shot1"])
667+
668+
# float field
669+
users = self._mockgun.find(
670+
"HumanUser",
671+
[],
672+
["login"],
673+
order=[{"field_name": "password_strength", "direction": "asc"}],
674+
)
675+
self.assertEqual([u["login"] for u in users], ["user2", "user1"])
676+
677+
# date_time field
678+
users = self._mockgun.find(
679+
"HumanUser",
680+
[],
681+
["login"],
682+
order=[{"field_name": "created_at", "direction": "asc"}],
683+
)
684+
self.assertEqual([u["login"] for u in users], ["user1", "user2"])
685+
471686
def test_nested_filter_operators(self):
472687
"""
473688
Tests a the use of the filter_operator nested

0 commit comments

Comments
 (0)