Merge lp:~verterok/ubuntuone-client/fix-583412 into lp:ubuntuone-client
- fix-583412
- Merge into trunk
Proposed by
Guillermo Gonzalez
Status: | Merged |
---|---|
Approved by: | Stuart Colville |
Approved revision: | 527 |
Merged at revision: | 528 |
Proposed branch: | lp:~verterok/ubuntuone-client/fix-583412 |
Merge into: | lp:ubuntuone-client |
Diff against target: |
289 lines (+81/-31) 5 files modified
tests/syncdaemon/test_dbus.py (+33/-6) tests/syncdaemon/test_vm.py (+8/-0) ubuntuone/syncdaemon/dbus_interface.py (+16/-11) ubuntuone/syncdaemon/filesystem_manager.py (+2/-1) ubuntuone/syncdaemon/volume_manager.py (+22/-13) |
To merge this branch: | bzr merge lp:~verterok/ubuntuone-client/fix-583412 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Rodrigo Moya (community) | Approve | ||
Rick McBride (community) | Approve | ||
Review via email: mp+26621@code.launchpad.net |
Commit message
Fix dbus_interface error handling in Folders.delete (and Shares.
Description of the change
Fix syncdaemon dbus interface when Folders.delete (or Shares.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'tests/syncdaemon/test_dbus.py' | |||
2 | --- tests/syncdaemon/test_dbus.py 2010-05-19 19:57:54 +0000 | |||
3 | +++ tests/syncdaemon/test_dbus.py 2010-06-02 18:22:20 +0000 | |||
4 | @@ -37,7 +37,12 @@ | |||
5 | 37 | DBUS_IFACE_PUBLIC_FILES_NAME, | 37 | DBUS_IFACE_PUBLIC_FILES_NAME, |
6 | 38 | EventListener, | 38 | EventListener, |
7 | 39 | ) | 39 | ) |
9 | 40 | from ubuntuone.syncdaemon.volume_manager import Share, Shared, UDF | 40 | from ubuntuone.syncdaemon.volume_manager import ( |
10 | 41 | Share, | ||
11 | 42 | Shared, | ||
12 | 43 | UDF, | ||
13 | 44 | VolumeDoesNotExist, | ||
14 | 45 | ) | ||
15 | 41 | from ubuntuone.syncdaemon.tools import DBusClient | 46 | from ubuntuone.syncdaemon.tools import DBusClient |
16 | 42 | from ubuntuone.syncdaemon import event_queue, states, main, config | 47 | from ubuntuone.syncdaemon import event_queue, states, main, config |
17 | 43 | from contrib.testing.testcase import ( | 48 | from contrib.testing.testcase import ( |
18 | @@ -1004,7 +1009,7 @@ | |||
19 | 1004 | n_bytes_read=10, | 1009 | n_bytes_read=10, |
20 | 1005 | deflated_size=20) | 1010 | deflated_size=20) |
21 | 1006 | return d | 1011 | return d |
23 | 1007 | 1012 | ||
24 | 1008 | def test_download_finished(self): | 1013 | def test_download_finished(self): |
25 | 1009 | """ Test the DBus signals in Status """ | 1014 | """ Test the DBus signals in Status """ |
26 | 1010 | a_dir = os.path.join(self.root_dir, u'ñoño'.encode('utf-8')) | 1015 | a_dir = os.path.join(self.root_dir, u'ñoño'.encode('utf-8')) |
27 | @@ -2084,7 +2089,8 @@ | |||
28 | 2084 | """FolderDeleted handler.""" | 2089 | """FolderDeleted handler.""" |
29 | 2085 | self.assertRaises(KeyError, self.main.fs.get_by_path, | 2090 | self.assertRaises(KeyError, self.main.fs.get_by_path, |
30 | 2086 | info['path'].decode('utf-8')) | 2091 | info['path'].decode('utf-8')) |
32 | 2087 | self.assertRaises(KeyError, self.main.vm.get_volume, info['volume_id']) | 2092 | self.assertRaises(VolumeDoesNotExist, |
33 | 2093 | self.main.vm.get_volume, info['volume_id']) | ||
34 | 2088 | d.callback(True) | 2094 | d.callback(True) |
35 | 2089 | match = self.bus.add_signal_receiver(deleted_handler, | 2095 | match = self.bus.add_signal_receiver(deleted_handler, |
36 | 2090 | signal_name='FolderDeleted') | 2096 | signal_name='FolderDeleted') |
37 | @@ -2093,7 +2099,8 @@ | |||
38 | 2093 | """the callback""" | 2099 | """the callback""" |
39 | 2094 | self.assertNotIn(udf.volume_id, self.main.vm.udfs) | 2100 | self.assertNotIn(udf.volume_id, self.main.vm.udfs) |
40 | 2095 | self.assertRaises(KeyError, self.main.fs.get_by_path, udf.path) | 2101 | self.assertRaises(KeyError, self.main.fs.get_by_path, udf.path) |
42 | 2096 | self.assertRaises(KeyError, self.main.vm.get_volume, udf.volume_id) | 2102 | self.assertRaises(VolumeDoesNotExist, |
43 | 2103 | self.main.vm.get_volume, udf.volume_id) | ||
44 | 2097 | self.folders_client.call_method('delete', udf.volume_id, | 2104 | self.folders_client.call_method('delete', udf.volume_id, |
45 | 2098 | reply_handler=check_deleted, | 2105 | reply_handler=check_deleted, |
46 | 2099 | error_handler=self.error_handler) | 2106 | error_handler=self.error_handler) |
47 | @@ -2126,6 +2133,24 @@ | |||
48 | 2126 | return d | 2133 | return d |
49 | 2127 | 2134 | ||
50 | 2128 | @defer.inlineCallbacks | 2135 | @defer.inlineCallbacks |
51 | 2136 | def test_delete_error_signal_folder_id(self): | ||
52 | 2137 | """Test for FolderDeleteError for a volume that doesn't exists.""" | ||
53 | 2138 | udf_id = 'foobar' | ||
54 | 2139 | d = defer.Deferred() | ||
55 | 2140 | def deleted_error_handler(info, error): | ||
56 | 2141 | """FolderDeleteError handler""" | ||
57 | 2142 | d.callback((info, error)) | ||
58 | 2143 | match = self.bus.add_signal_receiver(deleted_error_handler, | ||
59 | 2144 | signal_name='FolderDeleteError') | ||
60 | 2145 | self.signal_receivers.add(match) | ||
61 | 2146 | self.folders_client.call_method('delete', udf_id, | ||
62 | 2147 | reply_handler=lambda *args: None, | ||
63 | 2148 | error_handler=d.errback) | ||
64 | 2149 | info, error = yield d | ||
65 | 2150 | self.assertEquals(info['volume_id'], udf_id) | ||
66 | 2151 | self.assertEquals(error, "DOES_NOT_EXIST") | ||
67 | 2152 | |||
68 | 2153 | @defer.inlineCallbacks | ||
69 | 2129 | def test_subscribe(self): | 2154 | def test_subscribe(self): |
70 | 2130 | """Test for Folders.subscribe and that it fires a dbus signal.""" | 2155 | """Test for Folders.subscribe and that it fires a dbus signal.""" |
71 | 2131 | suggested_path = u'~/ñoño' | 2156 | suggested_path = u'~/ñoño' |
72 | @@ -2230,7 +2255,8 @@ | |||
73 | 2230 | """FolderDeleted handler.""" | 2255 | """FolderDeleted handler.""" |
74 | 2231 | self.assertRaises(KeyError, self.main.fs.get_by_path, | 2256 | self.assertRaises(KeyError, self.main.fs.get_by_path, |
75 | 2232 | info['path'].decode('utf-8')) | 2257 | info['path'].decode('utf-8')) |
77 | 2233 | self.assertRaises(KeyError, self.main.vm.get_volume, info['volume_id']) | 2258 | self.assertRaises(VolumeDoesNotExist, |
78 | 2259 | self.main.vm.get_volume, info['volume_id']) | ||
79 | 2234 | d.callback(True) | 2260 | d.callback(True) |
80 | 2235 | match = self.bus.add_signal_receiver(deleted_handler, | 2261 | match = self.bus.add_signal_receiver(deleted_handler, |
81 | 2236 | signal_name='ShareDeleted') | 2262 | signal_name='ShareDeleted') |
82 | @@ -2239,7 +2265,8 @@ | |||
83 | 2239 | """the callback""" | 2265 | """the callback""" |
84 | 2240 | self.assertNotIn(share.volume_id, self.main.vm.shares) | 2266 | self.assertNotIn(share.volume_id, self.main.vm.shares) |
85 | 2241 | self.assertRaises(KeyError, self.main.fs.get_by_path, share.path) | 2267 | self.assertRaises(KeyError, self.main.fs.get_by_path, share.path) |
87 | 2242 | self.assertRaises(KeyError, self.main.vm.get_volume, share.volume_id) | 2268 | self.assertRaises(VolumeDoesNotExist, |
88 | 2269 | self.main.vm.get_volume, share.volume_id) | ||
89 | 2243 | client = DBusClient(self.bus, '/shares', DBUS_IFACE_SHARES_NAME) | 2270 | client = DBusClient(self.bus, '/shares', DBUS_IFACE_SHARES_NAME) |
90 | 2244 | client.call_method('delete_share', share.volume_id, | 2271 | client.call_method('delete_share', share.volume_id, |
91 | 2245 | reply_handler=check_deleted, | 2272 | reply_handler=check_deleted, |
92 | 2246 | 2273 | ||
93 | === modified file 'tests/syncdaemon/test_vm.py' | |||
94 | --- tests/syncdaemon/test_vm.py 2010-05-20 14:52:09 +0000 | |||
95 | +++ tests/syncdaemon/test_vm.py 2010-06-02 18:22:20 +0000 | |||
96 | @@ -46,6 +46,7 @@ | |||
97 | 46 | LegacyShareFileShelf, | 46 | LegacyShareFileShelf, |
98 | 47 | MetadataUpgrader, | 47 | MetadataUpgrader, |
99 | 48 | VMFileShelf, | 48 | VMFileShelf, |
100 | 49 | VolumeDoesNotExist, | ||
101 | 49 | ) | 50 | ) |
102 | 50 | from twisted.internet import defer, reactor | 51 | from twisted.internet import defer, reactor |
103 | 51 | 52 | ||
104 | @@ -1376,6 +1377,13 @@ | |||
105 | 1376 | self.vm.delete_volume(udf.volume_id) | 1377 | self.vm.delete_volume(udf.volume_id) |
106 | 1377 | yield d | 1378 | yield d |
107 | 1378 | 1379 | ||
108 | 1380 | def test_handle_AQ_DELETE_VOLUME_ERROR_missing_volume(self): | ||
109 | 1381 | """Test for handle_AQ_DELETE_VOLUME_ERROR for a missing volume.""" | ||
110 | 1382 | volume_id = 'foobar' | ||
111 | 1383 | self.assertRaises(VolumeDoesNotExist, | ||
112 | 1384 | self.vm.handle_AQ_DELETE_VOLUME_ERROR, | ||
113 | 1385 | volume_id=volume_id, error="ERROR!") | ||
114 | 1386 | |||
115 | 1379 | def _test_handle_SV_VOLUME_CREATED(self, auto_subscribe): | 1387 | def _test_handle_SV_VOLUME_CREATED(self, auto_subscribe): |
116 | 1380 | """Test for handle_SV_VOLUME_CREATED.""" | 1388 | """Test for handle_SV_VOLUME_CREATED.""" |
117 | 1381 | user_conf = config.get_user_config() | 1389 | user_conf = config.get_user_config() |
118 | 1382 | 1390 | ||
119 | === modified file 'ubuntuone/syncdaemon/dbus_interface.py' | |||
120 | --- ubuntuone/syncdaemon/dbus_interface.py 2010-05-19 19:57:54 +0000 | |||
121 | +++ ubuntuone/syncdaemon/dbus_interface.py 2010-06-02 18:22:20 +0000 | |||
122 | @@ -32,7 +32,7 @@ | |||
123 | 32 | from ubuntuone.syncdaemon.event_queue import EVENTS | 32 | from ubuntuone.syncdaemon.event_queue import EVENTS |
124 | 33 | from ubuntuone.syncdaemon.interfaces import IMarker | 33 | from ubuntuone.syncdaemon.interfaces import IMarker |
125 | 34 | from ubuntuone.syncdaemon import config | 34 | from ubuntuone.syncdaemon import config |
127 | 35 | from ubuntuone.syncdaemon.volume_manager import Share, UDF | 35 | from ubuntuone.syncdaemon.volume_manager import Share, UDF, VolumeDoesNotExist |
128 | 36 | 36 | ||
129 | 37 | 37 | ||
130 | 38 | # Disable the "Invalid Name" check here, as we have lots of DBus style names | 38 | # Disable the "Invalid Name" check here, as we have lots of DBus style names |
131 | @@ -278,7 +278,7 @@ | |||
132 | 278 | signature='sa{ss}') | 278 | signature='sa{ss}') |
133 | 279 | def DownloadFileProgress(self, path, info): | 279 | def DownloadFileProgress(self, path, info): |
134 | 280 | """Fire a D-BUS signal, notifying about a download progress.""" | 280 | """Fire a D-BUS signal, notifying about a download progress.""" |
136 | 281 | pass | 281 | pass |
137 | 282 | 282 | ||
138 | 283 | @dbus.service.signal(DBUS_IFACE_STATUS_NAME, | 283 | @dbus.service.signal(DBUS_IFACE_STATUS_NAME, |
139 | 284 | signature='sa{ss}') | 284 | signature='sa{ss}') |
140 | @@ -295,7 +295,7 @@ | |||
141 | 295 | signature='sa{ss}') | 295 | signature='sa{ss}') |
142 | 296 | def UploadFileProgress(self, path, info): | 296 | def UploadFileProgress(self, path, info): |
143 | 297 | """Fire a D-BUS signal, notifying about an upload progress.""" | 297 | """Fire a D-BUS signal, notifying about an upload progress.""" |
145 | 298 | pass | 298 | pass |
146 | 299 | 299 | ||
147 | 300 | @dbus.service.signal(DBUS_IFACE_STATUS_NAME, | 300 | @dbus.service.signal(DBUS_IFACE_STATUS_NAME, |
148 | 301 | signature='sa{ss}') | 301 | signature='sa{ss}') |
149 | @@ -502,7 +502,7 @@ | |||
150 | 502 | n_bytes_read=n_bytes_read, | 502 | n_bytes_read=n_bytes_read, |
151 | 503 | deflated_size=deflated_size | 503 | deflated_size=deflated_size |
152 | 504 | ) | 504 | ) |
154 | 505 | 505 | ||
155 | 506 | def handle_AQ_DOWNLOAD_FINISHED(self, share_id, node_id, server_hash): | 506 | def handle_AQ_DOWNLOAD_FINISHED(self, share_id, node_id, server_hash): |
156 | 507 | """ handle AQ_DOWNLOAD_FINISHED """ | 507 | """ handle AQ_DOWNLOAD_FINISHED """ |
157 | 508 | self.handle_default('AQ_DOWNLOAD_FINISHED', share_id, | 508 | self.handle_default('AQ_DOWNLOAD_FINISHED', share_id, |
158 | @@ -586,7 +586,7 @@ | |||
159 | 586 | n_bytes_written=n_bytes_written, | 586 | n_bytes_written=n_bytes_written, |
160 | 587 | deflated_size=deflated_size | 587 | deflated_size=deflated_size |
161 | 588 | ) | 588 | ) |
163 | 589 | 589 | ||
164 | 590 | def handle_AQ_UPLOAD_FINISHED(self, share_id, node_id, hash): | 590 | def handle_AQ_UPLOAD_FINISHED(self, share_id, node_id, hash): |
165 | 591 | """ handle AQ_UPLOAD_FINISHED """ | 591 | """ handle AQ_UPLOAD_FINISHED """ |
166 | 592 | self.handle_default('AQ_UPLOAD_FINISHED', share_id, node_id, hash) | 592 | self.handle_default('AQ_UPLOAD_FINISHED', share_id, node_id, hash) |
167 | @@ -737,9 +737,9 @@ | |||
168 | 737 | self.handle_default('VM_VOLUME_DELETE_ERROR', volume_id, error) | 737 | self.handle_default('VM_VOLUME_DELETE_ERROR', volume_id, error) |
169 | 738 | try: | 738 | try: |
170 | 739 | volume = self.dbus_iface.volume_manager.get_volume(volume_id) | 739 | volume = self.dbus_iface.volume_manager.get_volume(volume_id) |
172 | 740 | except KeyError: | 740 | except VolumeDoesNotExist: |
173 | 741 | logger.error("Unable to handle VM_VOLUME_DELETE_ERROR for " | 741 | logger.error("Unable to handle VM_VOLUME_DELETE_ERROR for " |
175 | 742 | "volume_id=%r", volume_id) | 742 | "volume_id=%r, no such volume.", volume_id) |
176 | 743 | else: | 743 | else: |
177 | 744 | if isinstance(volume, Share): | 744 | if isinstance(volume, Share): |
178 | 745 | self.dbus_iface.shares.emit_share_delete_error(volume, error) | 745 | self.dbus_iface.shares.emit_share_delete_error(volume, error) |
179 | @@ -954,9 +954,12 @@ | |||
180 | 954 | try: | 954 | try: |
181 | 955 | self.vm.delete_volume(str(share_id)) | 955 | self.vm.delete_volume(str(share_id)) |
182 | 956 | reply_handler() | 956 | reply_handler() |
186 | 957 | except KeyError: | 957 | except VolumeDoesNotExist, e: |
187 | 958 | error_handler(ValueError("The volume with id: %s don't exists" % \ | 958 | self.ShareDeleteError({'volume_id':share_id}, str(e)) |
188 | 959 | str(share_id))) | 959 | except Exception, e: |
189 | 960 | error_handler(e) | ||
190 | 961 | logger.exception('Error while deleting share: %r', share_id) | ||
191 | 962 | self.ShareDeleteError({'volume_id':share_id}, str(e)) | ||
192 | 960 | 963 | ||
193 | 961 | 964 | ||
194 | 962 | @dbus.service.signal(DBUS_IFACE_SHARES_NAME, | 965 | @dbus.service.signal(DBUS_IFACE_SHARES_NAME, |
195 | @@ -1337,9 +1340,11 @@ | |||
196 | 1337 | logger.debug('Folders.delete: %r', folder_id) | 1340 | logger.debug('Folders.delete: %r', folder_id) |
197 | 1338 | try: | 1341 | try: |
198 | 1339 | self.vm.delete_volume(folder_id) | 1342 | self.vm.delete_volume(folder_id) |
199 | 1343 | except VolumeDoesNotExist, e: | ||
200 | 1344 | self.FolderDeleteError({'volume_id':folder_id}, str(e)) | ||
201 | 1340 | except Exception, e: | 1345 | except Exception, e: |
202 | 1341 | logger.exception('Error while deleting volume: %r', folder_id) | 1346 | logger.exception('Error while deleting volume: %r', folder_id) |
204 | 1342 | self.emit_folder_delete_error({'id':folder_id}, str(e)) | 1347 | self.FolderDeleteError({'volume_id':folder_id}, str(e)) |
205 | 1343 | 1348 | ||
206 | 1344 | @dbus.service.method(DBUS_IFACE_FOLDERS_NAME, out_signature='aa{ss}') | 1349 | @dbus.service.method(DBUS_IFACE_FOLDERS_NAME, out_signature='aa{ss}') |
207 | 1345 | def get_folders(self): | 1350 | def get_folders(self): |
208 | 1346 | 1351 | ||
209 | === modified file 'ubuntuone/syncdaemon/filesystem_manager.py' | |||
210 | --- ubuntuone/syncdaemon/filesystem_manager.py 2010-03-26 20:24:15 +0000 | |||
211 | +++ ubuntuone/syncdaemon/filesystem_manager.py 2010-06-02 18:22:20 +0000 | |||
212 | @@ -29,6 +29,7 @@ | |||
213 | 29 | import stat | 29 | import stat |
214 | 30 | 30 | ||
215 | 31 | from ubuntuone.syncdaemon import file_shelf | 31 | from ubuntuone.syncdaemon import file_shelf |
216 | 32 | from ubuntuone.syncdaemon.volume_manager import VolumeDoesNotExist | ||
217 | 32 | import uuid | 33 | import uuid |
218 | 33 | 34 | ||
219 | 34 | METADATA_VERSION = "4" | 35 | METADATA_VERSION = "4" |
220 | @@ -277,7 +278,7 @@ | |||
221 | 277 | # check if the share exists | 278 | # check if the share exists |
222 | 278 | try: | 279 | try: |
223 | 279 | self._get_share(mdobj["share_id"]) | 280 | self._get_share(mdobj["share_id"]) |
225 | 280 | except KeyError: | 281 | except VolumeDoesNotExist: |
226 | 281 | # oops, the share is gone!, invalidate this mdid | 282 | # oops, the share is gone!, invalidate this mdid |
227 | 282 | log_warning('Share %r disappeared! deleting mdid: %s', mdobj['share_id'], mdid) | 283 | log_warning('Share %r disappeared! deleting mdid: %s', mdobj['share_id'], mdid) |
228 | 283 | del self.fs[mdid] | 284 | del self.fs[mdid] |
229 | 284 | 285 | ||
230 | === modified file 'ubuntuone/syncdaemon/volume_manager.py' | |||
231 | --- ubuntuone/syncdaemon/volume_manager.py 2010-05-21 14:30:25 +0000 | |||
232 | +++ ubuntuone/syncdaemon/volume_manager.py 2010-06-02 18:22:20 +0000 | |||
233 | @@ -276,6 +276,20 @@ | |||
234 | 276 | return result | 276 | return result |
235 | 277 | 277 | ||
236 | 278 | 278 | ||
237 | 279 | class VolumeDoesNotExist(Exception): | ||
238 | 280 | """Exception for non existing volumes.""" | ||
239 | 281 | |||
240 | 282 | msg = 'DOES_NOT_EXIST' | ||
241 | 283 | |||
242 | 284 | def __init__(self, volume_id): | ||
243 | 285 | """Create the instance.""" | ||
244 | 286 | super(VolumeDoesNotExist, self).__init__(self.msg, volume_id) | ||
245 | 287 | |||
246 | 288 | def __str__(self): | ||
247 | 289 | """The error message.""" | ||
248 | 290 | return self.msg | ||
249 | 291 | |||
250 | 292 | |||
251 | 279 | class VolumeManager(object): | 293 | class VolumeManager(object): |
252 | 280 | """Manages shares and mount points.""" | 294 | """Manages shares and mount points.""" |
253 | 281 | 295 | ||
254 | @@ -807,13 +821,13 @@ | |||
255 | 807 | del self.udfs[udf_id] | 821 | del self.udfs[udf_id] |
256 | 808 | self.m.event_q.push('VM_VOLUME_DELETED', udf) | 822 | self.m.event_q.push('VM_VOLUME_DELETED', udf) |
257 | 809 | 823 | ||
259 | 810 | def get_volume(self, id): | 824 | def get_volume(self, volume_id): |
260 | 811 | """Returns the Share or UDF with the matching id.""" | 825 | """Returns the Share or UDF with the matching id.""" |
266 | 812 | volume = self.shares.get(id, None) | 826 | volume = self.shares.get(volume_id, None) |
267 | 813 | if volume is None: | 827 | if volume is None: |
268 | 814 | volume = self.udfs.get(id, None) | 828 | volume = self.udfs.get(volume_id, None) |
269 | 815 | if volume is None: | 829 | if volume is None: |
270 | 816 | raise KeyError(id) | 830 | raise VolumeDoesNotExist(volume_id) |
271 | 817 | return volume | 831 | return volume |
272 | 818 | 832 | ||
273 | 819 | def get_volumes(self, all_volumes=False): | 833 | def get_volumes(self, all_volumes=False): |
274 | @@ -920,13 +934,8 @@ | |||
275 | 920 | 934 | ||
276 | 921 | """ | 935 | """ |
277 | 922 | self.log.info('delete_volume: %r', volume_id) | 936 | self.log.info('delete_volume: %r', volume_id) |
285 | 923 | try: | 937 | volume = self.get_volume(volume_id) |
286 | 924 | volume = self.get_volume(volume_id) | 938 | self.m.action_q.delete_volume(volume.id) |
280 | 925 | except KeyError: | ||
281 | 926 | self.m.event_q.push('VM_VOLUME_DELETE_ERROR', volume_id, | ||
282 | 927 | "DOES_NOT_EXIST") | ||
283 | 928 | else: | ||
284 | 929 | self.m.action_q.delete_volume(volume.id) | ||
287 | 930 | 939 | ||
288 | 931 | def subscribe_udf(self, udf_id): | 940 | def subscribe_udf(self, udf_id): |
289 | 932 | """Mark the UDF with id as subscribed. | 941 | """Mark the UDF with id as subscribed. |
Cool!