# Perforce Defect Tracking Integration Project # # # P4.PY -- PYTHON INTERFACE TO PERFORCE # # Gareth Rees, Ravenbrook Limited, 2000-09-25 # # # 1. INTRODUCTION # # This module defines the 'p4' class, which provides an interface to # Perforce. # # "p4 help undoc" says: # # p4 -G # Causes all output (and batch input for form commands with -i) # to be formatted as marshalled Python dictionary objects. # # The intended readership of this document is project developers. # # This document is not confidential. # # # 1.1. Using the p4 class # # To use this class, create an instance, passing appropriate parameters # if necessary (if parameters are missing, the interface doesn't supply # values for them, so Perforce will pick up its normal defaults from # environment variables). # # import p4 # p4i = p4.p4(port = 'perforce:1666', user = 'root') # # The 'run' method takes a Perforce command and returns a list of # dictionaries; for example: # # >>> for c in p4i.run('changes -m 2'): # ... print c['change'], c['desc'] # ... # 10021 Explaining how to use the autom # 10020 Archiving new mail # # To pass information to Perforce, supply a dictionary as the second # argument, for example: # # >>> job = p4i.run('job -o job000001')[0] # >>> job['Title'] = string.replace(job['Title'], 'p4dti', 'P4DTI') # >>> p4i.run('job -i', job) # [{'code': 'info', 'data': 'Job job000001 saved.', 'level': 0}] # # Note the [0] at the end of line 1 of the above example: the run() # method always returns a list, even of 1 element. This point is easy # to forget. import marshal import os import re import string import tempfile import types error = 'Perforce error' # 2. THE P4 CLASS class p4: client = None client_executable = None logger = None password = None port = None user = None # 2.1. Create an instance # # We supply a default value for the client_executable parameter, but # for no others; Perforce will use its own default values if these # are not supplied. If logger is None then no messages will be # logged. # # We check that the Perforce client named by the client_executable # parameter is recent enough that it supports the -G option. It's # important to check this now because the error that we would get if # we just tried using the -G flag is "ValueError: bad marshal data" # (the marshal module is failing to read Perforce's error message # "Invalid option: -G."). # # We check that the Perforce client version supports "p4 -G" by # running "p4 -V" which produces some lines of text output, one of # which looks like "Rev. P4/NTX86/2000.2/19520 (2000/12/18)." The # changelevel in this case is 19520. If no line looks like this, # then raise an error anyway. (This makes the module fragile if # Perforce change the format of the output of "p4 -V".) server_version_re = re.compile('Server version: ' '[^/]+/[^/]+/[^/]+/([0-9]+)') server_version_re2 = re.compile('[^/]+/[^/]+/[^/]+/([0-9]+)') def __init__(self, client = None, client_executable = 'p4', logger = None, password = None, port = None, user = None): self.client = client self.client_executable = client_executable self.logger = logger self.password = password self.port = port self.user = user client_changelevel = 0 p4_exe = self.client_executable # Run a Perforce command # # run(arguments, input): Run the Perforce client with the given # command-line arguments, passing the dictionary 'input' to the # client's standard input. # # The arguments should be a Perforce command and its arguments, like # "jobs -o //foo/...". Options should generally include -i or -o to # avoid forms being put up interactively. # # Return a list of dictionaries containing the output of the # Perforce command. (Each dictionary contains one Perforce entity, # so "job -o" will return a list of one element, but "jobs -o" will # return a list of many elements.) def run(self, arguments, input = None): assert isinstance(arguments, types.StringType) assert input is None or isinstance(input, types.DictType) # Build a command line suitable for use with CMD.EXE on Windows # NT, or /bin/sh on POSIX. Make sure to quote the Perforce # command if it contains spaces. if ' ' in self.client_executable: command_words = ['"%s"' % self.client_executable] else: command_words = [self.client_executable] command_words.append('-G') if self.port: command_words.extend(['-p', self.port]) if self.user: command_words.extend(['-u', self.user]) if self.password: command_words.extend(['-P', self.password]) if self.client: command_words.extend(['-c', self.client]) command_words.append(arguments) # Pass the input dictionary (if any) to Perforce. temp_filename = None if input: tempfile.template = 'vsstop4_data' temp_filename = tempfile.mktemp() # Python marshalled dictionaries are binary, so use mode # 'wb'. temp_file = open(temp_filename, 'wb') marshal.dump(input, temp_file) temp_file.close() command_words.extend(['<', temp_filename]) command = string.join(command_words, ' ') # Python marshalled data is in binary, so we need mode 'rb'. mode = 'rb' stream = os.popen(command, mode) # Read the results of the Perforce command. results = [] try: while 1: results.append(marshal.load(stream)) except EOFError: if temp_filename: os.remove(temp_filename) # Check the exit status of the Perforce command, rather than # simply returning empty output when the command didn't run for # some reason (such as the Perforce server being down). exit_status = stream.close() # Check for errors from Perforce (either errors returned in the # data, or errors signalled by the exit status, or both) and # raise a Python exception. # # Perforce signals an error by the presence of a 'code' key in # the dictionary output. if (len(results) == 1 and results[0].has_key('code') and results[0]['code'] == 'error'): msg = results[0]['data'] if exit_status: # "%s The Perforce client exited with error code %d." raise error, catalog.msg(706, (msg, exit_status)) else: # "%s" raise error, catalog.msg(708, msg) elif exit_status: # "The Perforce client exited with error code %d. The # server might be down; the server address might be # incorrect; or your Perforce licence might have expired." raise error, catalog.msg(707, exit_status) else: return results def system(self, command, ignore_failure = 0, input_lines = None): (child_stdin, child_stdout) = os.popen4(command) if input_lines: child_stdin.writelines(input_lines) child_stdin.close() output = child_stdout.read() result = child_stdout.close() return output def run_output(self, arguments, input = None): assert isinstance(arguments, types.StringType) assert input is None or isinstance(input, types.DictType) # Build a command line suitable for use with CMD.EXE on Windows # NT, or /bin/sh on POSIX. Make sure to quote the Perforce # command if it contains spaces. if ' ' in self.client_executable: command_words = ['"%s"' % self.client_executable] else: command_words = [self.client_executable] if self.port: command_words.extend(['-p', self.port]) if self.user: command_words.extend(['-u', self.user]) if self.password: command_words.extend(['-P', self.password]) if self.client: command_words.extend(['-c', self.client]) command_words.append(arguments) command = string.join(command_words, ' ') return self.system(command)