Ticket #625: objects.py

File objects.py, 30.5 kB (added by ttressieres, 2 years ago)

objects.py file of PyPerforce? patch

Line 
1 """Object-oriented interfaces for Perforce entities."""
2
3 __all__ = ['CommandError',
4            'Client', 'User', 'Branch', 'Label', 'Change', 'Job']
5
6 import re
7 import perforce.api
8
9 class CommandError(perforce.api.PerforceError):
10     """Exception raised when a Perforce command fails."""
11    
12     def __init__(self, command, errors):
13         self.command = command
14         self.errors = errors
15
16     def __str__(self):
17         return "\n".join((x.format() for x in self.errors))
18
19 class FormObject(object):
20     """A Perforce form-based object.
21
22     Provides dictionary-style index access to form fields and to 'Options'
23     field values.
24
25     For example::
26       | >>> c = FormObject(clientForm)
27       | >>> print c['Options']
28       | noallwrite noclobber nocompress unlocked nomodtime normdir
29       | >>> print c['compress']
30       | False
31       | >>> c['compress'] = True
32       | >>> print c['Options']
33       | noallwrite noclobber compress unlocked nomodtime normdir
34       | >>> print c['compress']
35       | True
36     """
37
38     __slots__ = ['_form']
39
40     def __init__(self, form):
41         self._form = form
42
43     def __getitem__(self, key):
44         if key in self._form:
45             return self._form[key]           
46         elif 'Options' in self._form:
47             field = self._form.Options
48             if field.isSingle() and field.values is not None:
49                 fieldValues = field.values
50                 if not isinstance(fieldValues, list):
51                     fieldValues = [fieldValues]
52                 vals = self._form['Options'].split()
53                 for opts in fieldValues:
54                     if key in opts:
55                         return key in vals
56         raise KeyError("No such field or option '%s'" % key)         
57
58     def __setitem__(self, key, value):
59         if key in self._form:
60             self._form[key] = value
61             return
62         elif 'Options' in self._form:
63             field = self._form.Options
64             if field.isSingle() and field.values is not None:
65                 fieldValues = field.values
66                 if not isinstance(fieldValues, list):
67                     fieldValues = [fieldValues]
68                 vals = self._form['Options'].split()
69                 for opts in fieldValues:
70                     if key in opts:
71                         others = [opt for opt in opts if opt != key]
72                         if value:
73                             # Set to true by replacing one of the other values
74                             # with this value or adding it to the list.
75                             for other in others:
76                                 if other in vals:
77                                     vals[vals.index(other)] = key
78                                     break
79                             else:
80                                 if key not in vals:
81                                     vals.append(key)
82                         else:
83                             # Set to false by replacing this one with the other
84                             # value or appending the other value to the list.
85                             if len(others) != 1:
86                                 raise KeyError(
87                                     ("Ambiguous operation setting option '%s'"+
88                                      "to False") % key)
89                            
90                             if key in vals:
91                                 vals[vals.index(key)] = others[0]
92                             elif others[0] not in vals:
93                                 vals.append(others[0])
94                         self._form['Options'] = ' '.join(vals)
95                         return
96         raise KeyError("No such field or option '%s'" % key)
97
98     def __delitem__(self, key):
99         del self._form[key]
100
101 class Client(FormObject):
102     """A Perforce client/workspace object."""
103
104     __slots__ = ['__connection', '__name']
105
106     def __init__(self, connection, clientName):
107         """Initialises the Client based on the form retrieved on the
108         connection.
109
110         @param connection: The connection to the Perforce server to use to
111         query the client details.
112         @type connection: L{perforce.connection.Connection}
113
114         @param clientName: The name of the Perforce client to query.
115         @type clientName: C{str} or C{unicode}
116
117         @raise CommandError: If there was a Perforce error querying the client.
118         """
119         results = connection.run('client', '-o', clientName)
120         if results.errors or not results.forms:
121             raise CommandError('p4 client -o %s' % clientName,
122                                results.messages)
123        
124         form = results.forms[0]
125        
126         FormObject.__init__(self, form)
127         self.__connection = connection
128         self.__name = clientName
129
130     def save(self):
131         """Save any changes made to the client on the Perforce server.
132
133         @raise CommandError: If there was a Perforce error saving the client.
134         """
135         results = self.__connection.run('client', '-i', input=self._form)
136         if results.errors:
137             raise CommandError('p4 client -i', results.errors)
138
139     def sync(self, *fileRevisions, **options):
140         """Sync the client workspace to the specified file revisions.
141
142         If no file revisions are specified, then sync the entire workspace
143         to the latest revision.
144
145         @param fileRevisions: The file revisions to sync in this workspace.
146         If no fileRevisions were specified then all files in the workspace are
147         synced to the #head revision.
148         @type fileRevisions: C{tuple} of C{str} or C{unicode}
149
150         @keyword force: Pass as C{True} to force all named file revisions to
151         be retransmitted rather than just those that are out of date.
152         @type force: C{boolean}
153
154         @return: The results of performing the sync operation
155         @rtype: L{perforce.results.Results}
156
157         @raise CommandError: If there was a Perforce error syncing the client.
158         """
159         force = False
160         if 'force' in options:
161             force = options['force']
162
163         overrides = {'client' : self.__name}
164         if force:
165             results = self.__connection.run('sync', '-f',
166                                             *fileRevisions,
167                                             **overrides)
168         else:
169             results = self.__connection.run('sync',
170                                             *fileRevisions,
171                                             **overrides)
172         return results
173
174     def delete(self, force=False):
175         """Delete this client workspace from the Perforce repository.
176
177         The client cannot normally be deleted when the client is locked.
178         Passing force=True will allow the client owner or a Perforce user with
179         admin privileges to forcibly delete a locked client.
180
181         @note: This operation will not delete the files from the client's
182         directory.
183
184         @raise CommandError: If there was a Perforce error deleting the client.
185         """
186         if force:
187             results = self.__connection.run('client', '-d', '-f', self.__name)
188         else:
189             results = self.__connection.run('client', '-d', self.__name)
190
191         if results.errors:
192             if force:
193                 raise CommandError('p4 client -d -f %s' % self.__name,
194                                    results.errors)
195             else:
196                 raise CommandError('p4 client -d %s' % self.__name,
197                                    results.errors)
198
199 class User(FormObject):
200     """A Perforce user object."""
201
202     __slots__ = ['__connection', '__name']
203
204     def __init__(self, connection, userName):
205         """Initialises the User based on the form retrieved on the connection.
206
207         @param connection: The connection to the Perforce server to use.
208         @type connection: L{perforce.connection.Connection}
209
210         @param userName: The name of the user to query.
211         @type userName: C{str} or C{unicode}
212
213         @raise CommandError: If there was a Perforce error querying the user.
214         """
215         results = connection.run('user', '-o', userName)
216         if results.errors or not results.forms:
217             raise CommandError('p4 user -o %s' % userName,
218                                results.messages)
219         form = results.forms[0]
220         FormObject.__init__(self, form)
221         self.__connection = connection
222         self.__name = userName
223
224     def save(self, force=False):
225         """Save any changes made to the user to the Perforce server.
226
227         By default a user can only update their own user specification.
228         Specifying the C{force} parameter as C{True} allows users with
229         C{'super'} access to modify other user's specifications and to
230         modify the C{'Update'} field.
231
232         @param force: Flag indicating whether the update is forced.
233         @type force: C{boolean}
234
235         @raise CommandError: If the operation failed due to Perforce errors.
236         """
237         if force:
238             results = self.__connection.run('user', '-i', '-f',
239                                             input=self._form)
240         else:
241             results = self.__connection.run('user', '-i',
242                                             input=self._form)
243            
244         if results.errors:
245             if force:
246                 raise CommandError('p4 user -i -f', results.errors)
247             else:
248                 raise CommandError('p4 user -i', results.errors)
249
250     def delete(self, force=False):
251         """Delete this user from the Perforce server.
252
253         By default a user can only delete their own user specification.
254         Specifying the C{force} parameter as C{True} allows users with
255         C{'super'} access to delete other users.
256
257         @param force: Flag indicating whether to force deletion of the user.
258         @type force: C{boolean}
259
260         @raise CommandError: If the operation failed due to Perforce errors.
261         """
262         if force:
263             results = self.__connection.run('user', '-d', '-f', self.__name)
264         else:
265             results = self.__connection.run('user', '-d', self.__name)
266
267         if results.errors:
268             if force:
269                 raise CommandError('p4 user -d -f %s' % self.__name,
270                                    results.errors)
271             else:
272                 raise CommandError('p4 user -d %s' % self.__name,
273                                    results.errors)
274
275     def setPassword(self, newPassword, oldPassword=None):
276         """Set a new password for this user.
277
278         By default a user can only change their own password, and then only
279         if they correctly provide their old password. However, users with
280         'super' access can set new passwords for other users without providing
281         the old password.
282
283         @param newPassword: The new password to set.
284         Pass None or '' for the new password to set it to blank.
285         @type newPassword: C{str}, C{unicode} or C{None}
286
287         @param oldPassword: The old password. Leave as C{None} to set someone
288         else's password without the old password (provided you have 'super'
289         access).
290         @type oldPassword: C{str}, C{unicode} or C{None}
291
292         @raise CommandError: If the operation failed due to Perforce errors.
293         """
294
295         if newPassword is None:
296             newPassword = ''
297
298         if oldPassword is None or self.__name != self.__connection.user:
299             # Setting a password without the old password or setting another
300             # user's password needs 'super' access.
301             results = self.__connection.run('passwd', self.__name,
302                                             input=[newPassword,
303                                                    newPassword])
304             if results.errors:
305                 raise CommandError('p4 passwd %s' % self.__name,
306                                    results.errors)
307         else:
308             # Setting the current user's password
309             if oldPassword == '':
310                 results = self.__connection.run('passwd',
311                                                 input=[newPassword,
312                                                        newPassword])
313             else:
314                 results = self.__connection.run('passwd',
315                                                 input=[oldPassword,
316                                                        newPassword,
317                                                        newPassword])
318                
319             if results.errors:
320                 raise CommandError('p4 passwd',
321                                    results.errors)
322
323 class Branch(FormObject):
324     """A Perforce branch object."""
325
326     __slots__ = ["__connection", "__name"]
327
328     def __init__(self, connection, branchName):
329         """Intialise the Branch based on the form retrieved on the connection.
330
331         @param connection: The connection to the Perforce server to use.
332         Must be already connected.
333         @type connection: L{perforce.connection.Connection}
334
335         @param branchName: The name of the branch to query.
336         @type branchName: C{str} or C{unicode}
337
338         @raise CommandError: If there was a Perforc error querying the branch.
339         """
340         results = connection.run('branch', '-o', branchName)
341         if results.errors:
342             raise CommandError('p4 branch -o %s' % branchName,
343                                results.errors)
344         form = results.forms[0]
345         FormObject.__init__(self, form)
346         self.__connection = connection
347         self.__name = branchName
348
349     def save(self, force=False):
350         """Save any changes made to the branch to the Perforce server.
351
352         By default only the owner of a locked branch can modify the branch.
353         Passing C{force} as C{True} allows users with C{'admin'} access to
354         modify locked branches owned by another user.
355
356         @param force: Flag indicating whether to force saving of updates.
357         @type force: C{boolean}
358
359         @raise CommandError: If the operation failed due to Perforce errors.
360         """
361         form = self._form
362         if force:
363             results = self.__connection.run('branch', '-i', '-f',
364                                             input=form)
365             if results.errors:
366                 raise CommandError('p4 branch -i -f', results.errors)
367         else:
368             results = self.__connection.run('branch', '-i',
369                                             input=form)
370             if results.errors:
371                 raise CommandError('p4 branch -i', results.errors)
372
373     def delete(self, force=False):
374         """Delete this branch from the Perforce server.
375
376         By default, a branch cannot be deleted if it is locked.
377         Specifying the 'force' parameter as True allows users with 'admin'
378         access to delete locked branches.
379
380         @param force: Flag indicating whether to force deletion of the branch.
381         @type force: C{boolean}
382
383         @raise CommandError: If the operation failed due to Perforce errors.
384         """
385         if force:
386             results = self.__connection.run('branch', '-d', '-f', self.__name)
387             if results.errors:
388                 raise CommandError('p4 branch -d -f %s' % self.__name,
389                                    results.errors)
390         else:
391             results = self.__connection.run('branch', '-d', self.__name)
392             if results.errors:
393                 raise CommandError('p4 branch -d %s' % self.__name,
394                                    results.errors)
395
396 class Label(FormObject):
397     """The label class wraps the Perforce label concept."""
398
399     __slots__ = ['__connection', '__name']
400
401     def __init__(self, connection, labelName):
402         """Intialise the Label based on the form retrieved on the connection.
403
404         @param connection: The connection to the Perforce server to use.
405         Must be already connected.
406         @type connection: L{perforce.connection.Connection}
407
408         @param labelName: The name of the label to query.
409         @type labelName: C{str} or C{unicode}
410
411         @raise CommandError: If there was a Perforce error querying the label.
412         """
413         results = connection.run('label', '-o', labelName)
414         if results.errors:
415             raise CommandError('p4 label -o %s' % labelName,
416                                results.errors)
417         form = results.forms[0]
418         FormObject.__init__(self, form)
419         self.__connection = connection
420         self.__name = labelName
421
422     def save(self, force=False):
423         """Save any changes made to the label on the Perforce server.
424
425         By default a locked label can only be updated by its owner. Passing
426         C{force} as C{True} will allow users with C{'admin'} access to force
427         saving changes to the labe.
428
429         @param force: Flag indicating whether to force saving of updates.
430         @type force: C{boolean}
431         
432         @raise CommandError: If the operation failed due to Perforce errors.
433         """
434         if force:
435             results = self.__connection.run('label', '-i', '-f',
436                                             input=self._form)
437         else:
438             results = self.__connection.run('label', '-i',
439                                             input=self._form)
440
441         if results.errors:
442             if force:
443                 raise CommandError('p4 label -i -f', results.errors)
444             else:
445                 raise CommandError('p4 label -i', results.errors)
446
447     def delete(self, force=False):
448         """Delete this label from the Perforce server.
449
450         By default a locked label can't be deleted. Users with 'admin' access
451         can force deletion of a locked label by specifying C{force} as C{True}.
452
453         @param force: Flag indicating whether to force deletion of a locked
454         label.
455         @type force: C{boolean}
456
457         @raise CommandError: If the operation failed due to Perforce errors.
458         """
459         if force:
460             results = self.__connection.run('label', '-d', '-f', self.__name)
461         else:
462             results = self.__connection.run('label', '-d', self.__name)
463
464         if results.errors:
465             if force:
466                 raise CommandError('p4 user -d -f %s' % self.__name,
467                                    results.errors)
468             else:
469                 raise CommandError('p4 user -d %s' % self.__name,
470                                    results.errors)
471
472 class Change(FormObject):
473     """The change class wraps the Perforce changelist concept."""
474
475     __slots__ = ['__connection', '__name', '__client']
476
477     __changeCreatedMessageRE = re.compile(
478         '^Change (?P<change>\d+) created')
479     __changeRenamedMessageRE = re.compile(
480         '^Change (?P<old>\d+) renamed change (?P<new>\d+)')
481
482     def __init__(self, connection, change=None, client=None):
483         """Initialise the Change based on the form retrieved on the connection.
484
485         @param connection: The connection to the Perforce server to use.
486         Must already be connected.
487         @type connection: L{perforce.connection.Connection}
488
489         @param change: The number of the changelist to retrieve.
490         If not specified then open the default changelist on the connection's
491         current client. Saving the default changelist will create a new named
492         changelist.
493         @type change: C{int}, C{str}, C{unicode} or C{None}
494
495         @param client: The name of the Perforce client to use for retrieving
496         default changelists and submitting files. The client used to submit
497         the changelist must be the same as the client the changelist was
498         created on.
499         @type client: C{str} or C{unicode}
500
501         @raise CommandError: If the operation failed due to Perforce errors.
502         """
503         FormObject.__init__(self, None)
504        
505         self.__connection = connection
506         self.__client = client
507        
508         if change is None:
509             self.__name = None
510         elif isinstance(change, int):
511             self.__name = str(change)
512         elif isinstance(change, basestring):
513             self.__name = change
514         else:
515             raise TypeError("Invalid type for 'change' parameter.")
516            
517         self.refresh()
518
519     def save(self, force=False):
520         """Save any changes made to the changelist on the Perforce server.
521
522         @param force: Flag indicating whether or not to force update of other
523         users' pending changelists or 'Update' and 'Description' fields of
524         submitted changelists. The user must have 'admin' privileges to
525         force modification of changelists.
526         @type force: C{boolean}
527
528         @raise CommandError: If the operation failed due to Perforce errors.
529         """
530
531         client = self.__client or self.__connection.client
532         if force:
533             results = self.__connection.run('change', '-s', '-f', '-i',
534                                             intput=self._form,
535                                             client=client)
536             if results.errors:
537                 raise CommandError('p4 change -s -f -i',
538                                    results.errors)
539         else:
540             results = self.__connection.run('change', '-s', '-i',
541                                             input=self._form,
542                                             client=client)
543             if results.errors:
544                 raise CommandError('p4 change -s -i',
545                                    results.errors)
546
547         if self.__name is None:
548             # We just saved a new change form, query the new change number.
549             message = results.infos[0].format()
550             match = Change.__changeCreatedMessageRE.match(message)
551             if not match:
552                 raise RuntimeError(
553                     "Perforce server returned an unrecognised response " +
554                     "creating a new changelist: %s" % message)
555
556             self.__name = match.group('change')
557
558     def delete(self, force=False):
559         """Delete a pending changelist on the Perforce server.
560
561         @note: The default changelist cannot be deleted.
562
563         @param force: Force deletion of another user's changelist or a
564         submitted changelist (once all files on the changelist have been
565         obliterated). The user must have 'admin' privileges to force
566         deletion of changelists.
567         @type force: C{boolean}
568
569         @raise CommandError: If the operation failed due to Perforce errors.
570         """
571
572         client = self.__client or self.__connection.client
573         if force:
574             results = self.__connection.run('change', '-d', '-f', self.__name,
575                                             client=client)
576             if results.errors:
577                 raise CommandError('p4 change -d -f %s' % self.__name,
578                                    results.errors)
579         else:
580             results = self.__connection.run('change', '-d', self.__name,
581                                             client=client)
582             if results.errors:
583                 raise CommandError('p4 change -d %s' % <