Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 1 | """distutils.command.register |
| 2 | |
| 3 | Implements the Distutils 'register' command (register with the repository). |
| 4 | """ |
| 5 | |
| 6 | # created 2002/10/21, Richard Jones |
| 7 | |
Tarek Ziadé | 3679727 | 2010-07-22 12:50:05 +0000 | [diff] [blame] | 8 | import os, string, getpass |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 9 | import io |
Tarek Ziadé | 3679727 | 2010-07-22 12:50:05 +0000 | [diff] [blame] | 10 | import urllib.parse, urllib.request |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 11 | from warnings import warn |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 12 | |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 13 | from distutils.core import PyPIRCCommand |
Tarek Ziadé | 3679727 | 2010-07-22 12:50:05 +0000 | [diff] [blame] | 14 | from distutils.errors import * |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 15 | from distutils import log |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 16 | |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 17 | class register(PyPIRCCommand): |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 18 | |
Andrew M. Kuchling | a9ccce3 | 2003-03-03 18:26:01 +0000 | [diff] [blame] | 19 | description = ("register the distribution with the Python package index") |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 20 | user_options = PyPIRCCommand.user_options + [ |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 21 | ('list-classifiers', None, |
| 22 | 'list the valid Trove classifiers'), |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 23 | ('strict', None , |
| 24 | 'Will stop the registering if the meta-data are not fully compliant') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 25 | ] |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 26 | boolean_options = PyPIRCCommand.boolean_options + [ |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 27 | 'verify', 'list-classifiers', 'strict'] |
| 28 | |
| 29 | sub_commands = [('check', lambda self: True)] |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 30 | |
| 31 | def initialize_options(self): |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 32 | PyPIRCCommand.initialize_options(self) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 33 | self.list_classifiers = 0 |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 34 | self.strict = 0 |
| 35 | |
| 36 | def finalize_options(self): |
| 37 | PyPIRCCommand.finalize_options(self) |
| 38 | # setting options for the `check` subcommand |
| 39 | check_options = {'strict': ('register', self.strict), |
| 40 | 'restructuredtext': ('register', 1)} |
| 41 | self.distribution.command_options['check'] = check_options |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 42 | |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 43 | def run(self): |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 44 | self.finalize_options() |
| 45 | self._set_config() |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 46 | |
| 47 | # Run sub commands |
| 48 | for cmd_name in self.get_sub_commands(): |
| 49 | self.run_command(cmd_name) |
| 50 | |
Andrew M. Kuchling | 058a84f | 2003-04-09 12:35:51 +0000 | [diff] [blame] | 51 | if self.dry_run: |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 52 | self.verify_metadata() |
| 53 | elif self.list_classifiers: |
| 54 | self.classifiers() |
| 55 | else: |
| 56 | self.send_metadata() |
| 57 | |
| 58 | def check_metadata(self): |
Tarek Ziadé | 5af55c6 | 2009-05-16 16:52:13 +0000 | [diff] [blame] | 59 | """Deprecated API.""" |
| 60 | warn("distutils.command.register.check_metadata is deprecated, \ |
| 61 | use the check command instead", PendingDeprecationWarning) |
| 62 | check = self.distribution.get_command_obj('check') |
| 63 | check.ensure_finalized() |
| 64 | check.strict = self.strict |
| 65 | check.restructuredtext = 1 |
| 66 | check.run() |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 67 | |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 68 | def _set_config(self): |
| 69 | ''' Reads the configuration file and set attributes. |
| 70 | ''' |
| 71 | config = self._read_pypirc() |
| 72 | if config != {}: |
| 73 | self.username = config['username'] |
| 74 | self.password = config['password'] |
| 75 | self.repository = config['repository'] |
| 76 | self.realm = config['realm'] |
| 77 | self.has_config = True |
| 78 | else: |
| 79 | if self.repository not in ('pypi', self.DEFAULT_REPOSITORY): |
| 80 | raise ValueError('%s not found in .pypirc' % self.repository) |
| 81 | if self.repository == 'pypi': |
| 82 | self.repository = self.DEFAULT_REPOSITORY |
| 83 | self.has_config = False |
| 84 | |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 85 | def classifiers(self): |
| 86 | ''' Fetch the list of classifiers from the server. |
| 87 | ''' |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 88 | url = self.repository+'?:action=list_classifiers' |
| 89 | response = urllib.request.urlopen(url) |
Antoine Pitrou | 335a512 | 2013-12-22 18:13:51 +0100 | [diff] [blame] | 90 | log.info(self._read_pypi_response(response)) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 91 | |
| 92 | def verify_metadata(self): |
| 93 | ''' Send the metadata to the package index server to be checked. |
| 94 | ''' |
| 95 | # send the info to the server and report the result |
| 96 | (code, result) = self.post_to_server(self.build_post_data('verify')) |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 97 | log.info('Server response (%s): %s' % (code, result)) |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 98 | |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 99 | def send_metadata(self): |
| 100 | ''' Send the metadata to the package index server. |
| 101 | |
| 102 | Well, do the following: |
| 103 | 1. figure who the user is, and then |
| 104 | 2. send the data as a Basic auth'ed POST. |
| 105 | |
| 106 | First we try to read the username/password from $HOME/.pypirc, |
| 107 | which is a ConfigParser-formatted file with a section |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 108 | [distutils] containing username and password entries (both |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 109 | in clear text). Eg: |
| 110 | |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 111 | [distutils] |
| 112 | index-servers = |
| 113 | pypi |
| 114 | |
| 115 | [pypi] |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 116 | username: fred |
| 117 | password: sekrit |
| 118 | |
| 119 | Otherwise, to figure who the user is, we offer the user three |
| 120 | choices: |
| 121 | |
| 122 | 1. use existing login, |
| 123 | 2. register as a new user, or |
| 124 | 3. set the password to a random string and email the user. |
| 125 | |
| 126 | ''' |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 127 | # see if we can short-cut and get the username/password from the |
| 128 | # config |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 129 | if self.has_config: |
| 130 | choice = '1' |
| 131 | username = self.username |
| 132 | password = self.password |
| 133 | else: |
| 134 | choice = 'x' |
| 135 | username = password = '' |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 136 | |
| 137 | # get the user's login info |
| 138 | choices = '1 2 3 4'.split() |
| 139 | while choice not in choices: |
Benjamin Peterson | 9203501 | 2008-12-27 16:00:54 +0000 | [diff] [blame] | 140 | self.announce('''\ |
| 141 | We need to know who you are, so please choose either: |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 142 | 1. use your existing login, |
| 143 | 2. register as a new user, |
| 144 | 3. have the server generate a new password for you (and email it to you), or |
| 145 | 4. quit |
Benjamin Peterson | 9203501 | 2008-12-27 16:00:54 +0000 | [diff] [blame] | 146 | Your selection [default 1]: ''', log.INFO) |
Benjamin Peterson | 467a7bd | 2008-12-27 17:00:44 +0000 | [diff] [blame] | 147 | choice = input() |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 148 | if not choice: |
| 149 | choice = '1' |
| 150 | elif choice not in choices: |
Guido van Rossum | be19ed7 | 2007-02-09 05:37:30 +0000 | [diff] [blame] | 151 | print('Please choose one of the four options!') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 152 | |
| 153 | if choice == '1': |
| 154 | # get the username and password |
| 155 | while not username: |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 156 | username = input('Username: ') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 157 | while not password: |
| 158 | password = getpass.getpass('Password: ') |
| 159 | |
| 160 | # set up the authentication |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 161 | auth = urllib.request.HTTPPasswordMgr() |
| 162 | host = urllib.parse.urlparse(self.repository)[1] |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 163 | auth.add_password(self.realm, host, username, password) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 164 | # send the info to the server and report the result |
| 165 | code, result = self.post_to_server(self.build_post_data('submit'), |
| 166 | auth) |
Benjamin Peterson | 9203501 | 2008-12-27 16:00:54 +0000 | [diff] [blame] | 167 | self.announce('Server response (%s): %s' % (code, result), |
| 168 | log.INFO) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 169 | |
| 170 | # possibly save the login |
Tarek Ziadé | 13f7c3b | 2009-01-09 00:15:45 +0000 | [diff] [blame] | 171 | if code == 200: |
| 172 | if self.has_config: |
| 173 | # sharing the password in the distribution instance |
| 174 | # so the upload command can reuse it |
| 175 | self.distribution.password = password |
| 176 | else: |
| 177 | self.announce(('I can store your PyPI login so future ' |
| 178 | 'submissions will be faster.'), log.INFO) |
| 179 | self.announce('(the login will be stored in %s)' % \ |
| 180 | self._get_rc_file(), log.INFO) |
| 181 | choice = 'X' |
| 182 | while choice.lower() not in 'yn': |
| 183 | choice = input('Save your login (y/N)?') |
| 184 | if not choice: |
| 185 | choice = 'n' |
| 186 | if choice.lower() == 'y': |
| 187 | self._store_pypirc(username, password) |
Alexandre Vassalotti | 5f8ced2 | 2008-05-16 00:03:33 +0000 | [diff] [blame] | 188 | |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 189 | elif choice == '2': |
| 190 | data = {':action': 'user'} |
| 191 | data['name'] = data['password'] = data['email'] = '' |
| 192 | data['confirm'] = None |
| 193 | while not data['name']: |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 194 | data['name'] = input('Username: ') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 195 | while data['password'] != data['confirm']: |
| 196 | while not data['password']: |
| 197 | data['password'] = getpass.getpass('Password: ') |
| 198 | while not data['confirm']: |
| 199 | data['confirm'] = getpass.getpass(' Confirm: ') |
| 200 | if data['password'] != data['confirm']: |
| 201 | data['password'] = '' |
| 202 | data['confirm'] = None |
Guido van Rossum | be19ed7 | 2007-02-09 05:37:30 +0000 | [diff] [blame] | 203 | print("Password and confirm don't match!") |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 204 | while not data['email']: |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 205 | data['email'] = input(' EMail: ') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 206 | code, result = self.post_to_server(data) |
| 207 | if code != 200: |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 208 | log.info('Server response (%s): %s' % (code, result)) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 209 | else: |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 210 | log.info('You will receive an email shortly.') |
| 211 | log.info(('Follow the instructions in it to ' |
| 212 | 'complete registration.')) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 213 | elif choice == '3': |
| 214 | data = {':action': 'password_reset'} |
| 215 | data['email'] = '' |
| 216 | while not data['email']: |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 217 | data['email'] = input('Your email address: ') |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 218 | code, result = self.post_to_server(data) |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 219 | log.info('Server response (%s): %s' % (code, result)) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 220 | |
| 221 | def build_post_data(self, action): |
| 222 | # figure the data to send - the metadata plus some additional |
| 223 | # information used by the package server |
| 224 | meta = self.distribution.metadata |
| 225 | data = { |
| 226 | ':action': action, |
| 227 | 'metadata_version' : '1.0', |
| 228 | 'name': meta.get_name(), |
| 229 | 'version': meta.get_version(), |
| 230 | 'summary': meta.get_description(), |
| 231 | 'home_page': meta.get_url(), |
| 232 | 'author': meta.get_contact(), |
| 233 | 'author_email': meta.get_contact_email(), |
| 234 | 'license': meta.get_licence(), |
| 235 | 'description': meta.get_long_description(), |
| 236 | 'keywords': meta.get_keywords(), |
| 237 | 'platform': meta.get_platforms(), |
Andrew M. Kuchling | 23c98c5 | 2003-02-19 13:49:35 +0000 | [diff] [blame] | 238 | 'classifiers': meta.get_classifiers(), |
Andrew M. Kuchling | 80be59b | 2003-02-19 14:27:21 +0000 | [diff] [blame] | 239 | 'download_url': meta.get_download_url(), |
Fred Drake | db7b002 | 2005-03-20 22:19:47 +0000 | [diff] [blame] | 240 | # PEP 314 |
| 241 | 'provides': meta.get_provides(), |
| 242 | 'requires': meta.get_requires(), |
| 243 | 'obsoletes': meta.get_obsoletes(), |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 244 | } |
Fred Drake | db7b002 | 2005-03-20 22:19:47 +0000 | [diff] [blame] | 245 | if data['provides'] or data['requires'] or data['obsoletes']: |
| 246 | data['metadata_version'] = '1.1' |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 247 | return data |
| 248 | |
| 249 | def post_to_server(self, data, auth=None): |
| 250 | ''' Post a query to the server, and return a string response. |
| 251 | ''' |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 252 | if 'name' in data: |
| 253 | self.announce('Registering %s to %s' % (data['name'], |
| 254 | self.repository), |
| 255 | log.INFO) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 256 | # Build up the MIME payload for the urllib2 POST data |
| 257 | boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
| 258 | sep_boundary = '\n--' + boundary |
| 259 | end_boundary = sep_boundary + '--' |
Guido van Rossum | 34d1928 | 2007-08-09 01:03:29 +0000 | [diff] [blame] | 260 | body = io.StringIO() |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 261 | for key, value in data.items(): |
| 262 | # handle multiple entries for the same name |
Thomas Wouters | 89f507f | 2006-12-13 04:49:30 +0000 | [diff] [blame] | 263 | if type(value) not in (type([]), type( () )): |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 264 | value = [value] |
| 265 | for value in value: |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 266 | value = str(value) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 267 | body.write(sep_boundary) |
| 268 | body.write('\nContent-Disposition: form-data; name="%s"'%key) |
| 269 | body.write("\n\n") |
| 270 | body.write(value) |
| 271 | if value and value[-1] == '\r': |
| 272 | body.write('\n') # write an extra newline (lurve Macs) |
| 273 | body.write(end_boundary) |
| 274 | body.write("\n") |
Martin v. Löwis | 2d1ca2d | 2008-11-20 16:21:55 +0000 | [diff] [blame] | 275 | body = body.getvalue().encode("utf-8") |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 276 | |
| 277 | # build the Request |
| 278 | headers = { |
Walter Dörwald | a6e8a4a | 2005-03-31 13:57:38 +0000 | [diff] [blame] | 279 | 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary, |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 280 | 'Content-length': str(len(body)) |
| 281 | } |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 282 | req = urllib.request.Request(self.repository, body, headers) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 283 | |
| 284 | # handle HTTP and include the Basic Auth handler |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 285 | opener = urllib.request.build_opener( |
| 286 | urllib.request.HTTPBasicAuthHandler(password_mgr=auth) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 287 | ) |
| 288 | data = '' |
| 289 | try: |
| 290 | result = opener.open(req) |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 291 | except urllib.error.HTTPError as e: |
Andrew M. Kuchling | 23c98c5 | 2003-02-19 13:49:35 +0000 | [diff] [blame] | 292 | if self.show_response: |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 293 | data = e.fp.read() |
| 294 | result = e.code, e.msg |
Jeremy Hylton | 1afc169 | 2008-06-18 20:49:58 +0000 | [diff] [blame] | 295 | except urllib.error.URLError as e: |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 296 | result = 500, str(e) |
| 297 | else: |
Andrew M. Kuchling | 23c98c5 | 2003-02-19 13:49:35 +0000 | [diff] [blame] | 298 | if self.show_response: |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 299 | data = result.read() |
| 300 | result = 200, 'OK' |
Andrew M. Kuchling | 23c98c5 | 2003-02-19 13:49:35 +0000 | [diff] [blame] | 301 | if self.show_response: |
Tarek Ziadé | baf5180 | 2009-03-31 21:37:16 +0000 | [diff] [blame] | 302 | dashes = '-' * 75 |
Serhiy Storchaka | bc27a05 | 2014-02-06 22:49:45 +0200 | [diff] [blame] | 303 | self.announce('%s%r%s' % (dashes, data, dashes)) |
Andrew M. Kuchling | 51a6a4c | 2003-01-03 15:29:28 +0000 | [diff] [blame] | 304 | return result |