diff --git a/solvebio/resource/object.py b/solvebio/resource/object.py index 76b6e4ec..c4a21691 100644 --- a/solvebio/resource/object.py +++ b/solvebio/resource/object.py @@ -1,4 +1,5 @@ """Solvebio Object API resource""" +import collections import os import re import base64 @@ -6,6 +7,7 @@ import mimetypes import requests +import six from requests.packages.urllib3.util.retry import Retry from solvebio.errors import SolveError @@ -484,16 +486,27 @@ def is_file(self): return self.object_type == 'file' def has_tag(self, tag): - """Return True if object contains tags""" + """Return True if object contains tag""" def lowercase(x): return x.lower() - return lowercase(tag) in map(lowercase, self.tags) + return lowercase(str(tag)) in map(lowercase, self.tags) - def tag(self, tags, remove=False, dry_run=False, apply_save=False): + def tag(self, tags, remove=False, dry_run=False, apply_save=True): """Add or remove tags on an object""" + def is_iterable_non_string(arg): + """python2/python3 compatible way to check if arg is an iterable but not string""" + + return (isinstance(arg, collections.Iterable) and + not isinstance(arg, six.string_types)) + + if not is_iterable_non_string(tags): + tags = [str(tags)] + else: + tags = [str(tag) for tag in tags] + if remove: removal_tags = [tag for tag in tags if self.has_tag(tag)] if removal_tags: @@ -501,7 +514,8 @@ def tag(self, tags, remove=False, dry_run=False, apply_save=False): .format('[Dry Run] ' if dry_run else '', ', '.join(removal_tags), self.full_path)) - updated_tags = [tag for tag in tags if not self.has_tag(tag)] + tags_for_removal = [tag for tag in tags if self.has_tag(tag)] + updated_tags = [tag for tag in self.tags if tag not in tags_for_removal] else: print('{}Notice: Object {} does not contain any of the ' 'following tags: {}'.format( @@ -526,3 +540,8 @@ def tag(self, tags, remove=False, dry_run=False, apply_save=False): self.save() return True + + def untag(self, tags, dry_run=False, apply_save=True): + """Remove tags on an object""" + + return self.tag(tags=tags, remove=True, dry_run=dry_run, apply_save=apply_save) diff --git a/solvebio/test/client_mocks.py b/solvebio/test/client_mocks.py index 538f1b28..c122bfca 100644 --- a/solvebio/test/client_mocks.py +++ b/solvebio/test/client_mocks.py @@ -10,7 +10,7 @@ class Fake201Response(object): def __init__(self, data): self.object = { - 'id': 100, + 'id': data.get('id', 100), 'class_name': self.class_name } @@ -20,6 +20,9 @@ def json(self): def create(self, *args, **kwargs): return convert_to_solve_object(self.object) + def save(self, *args, **kwargs): + return self.create(*args, **kwargs) + def retrieve(self, r_id, *args, **kwargs): obj = self.create() obj.id = r_id @@ -249,6 +252,10 @@ def fake_object_create(*args, **kwargs): return FakeObjectResponse(kwargs).create() +def fake_object_save(*args, **kwargs): + return FakeObjectResponse(kwargs).save() + + def fake_object_retrieve(*args, **kwargs): return FakeObjectResponse(kwargs)._retrieve_helper( 'LogicalObject', *args, **kwargs) diff --git a/solvebio/test/test_object.py b/solvebio/test/test_object.py index 8deed432..cb2dfb19 100644 --- a/solvebio/test/test_object.py +++ b/solvebio/test/test_object.py @@ -2,7 +2,7 @@ import mock from .helper import SolveBioTestCase -from solvebio.test.client_mocks import fake_object_create +from solvebio.test.client_mocks import fake_object_create, fake_object_save from solvebio.test.client_mocks import fake_dataset_create @@ -166,3 +166,73 @@ def test_object_dataset_getattr(self, ObjectCreate, DatasetCreate): for obj in [file_, folder_, ds, ds_obj]: with self.assertRaises(AttributeError): getattr(obj, fake_attr) + + @mock.patch('solvebio.resource.Object.create') + @mock.patch('solvebio.resource.Object.save') + def test_object_remove_tag(self, ObjectCreate, ObjectSave): + ObjectCreate.side_effect = fake_object_create + ObjectSave.side_effect = fake_object_save + + tags = ['tag1', 'tag2', 'tag3'] + file_ = self.client.Object.create(filename='foo_file', + object_type='file', + tags=tags) + for tag in tags: + self.assertTrue(file_.has_tag(tag)) + + tags_for_removal = ['tag1', 'tag2'] + file_.tag(tags=tags_for_removal, remove=True, apply_save=True) + + # test that given tags are removed + for tag in tags_for_removal: + self.assertFalse(file_.has_tag(tag)) + + updated_tags = [tag for tag in tags if tag not in tags_for_removal] + + # test that tags which have not been removed are still there + for tag in updated_tags: + self.assertTrue(file_.has_tag(tag)) + + @mock.patch('solvebio.resource.Object.create') + @mock.patch('solvebio.resource.Object.save') + def test_object_untag(self, ObjectCreate, ObjectSave): + ObjectCreate.side_effect = fake_object_create + ObjectSave.side_effect = fake_object_save + + tags = ['tag1', 'tag2', 'tag3'] + file_ = self.client.Object.create(filename='foo_file', + object_type='file', + tags=tags) + for tag in tags: + self.assertTrue(file_.has_tag(tag)) + + tags_for_untagging = ['tag1', 'tag2'] + file_.untag(tags=tags_for_untagging, apply_save=True) + + # test that given tags are untagged + for tag in tags_for_untagging: + self.assertFalse(file_.has_tag(tag)) + + updated_tags = [tag for tag in tags if tag not in tags_for_untagging] + + # test that tags which have not been untagged are still there + for tag in updated_tags: + self.assertTrue(file_.has_tag(tag)) + + @mock.patch('solvebio.resource.Object.create') + @mock.patch('solvebio.resource.Object.save') + def test_object_add_tag_non_iterable_or_string(self, ObjectCreate, ObjectSave): + ObjectCreate.side_effect = fake_object_create + ObjectSave.side_effect = fake_object_save + + # test that a string is added propery + tags = 'tag1' + file_ = self.client.Object.create(filename='foo_file', + object_type='file') + file_.tag(tags) + self.assertTrue(file_.has_tag(tags)) + + # test that non iterable (e.g. integer) added properly + tags = 1 + file_.tag(tags) + self.assertTrue(file_.has_tag(tags))