#1858 from Tarek Ziade:

Allow multiple repositories in .pypirc; see http://wiki.python.org/moin/EnhancedPyPI
for discussion.

The patch is slightly revised from Tarek's last patch: I've simplified
the PyPIRCCommand.finalize_options() method to not look at sys.argv.
Tests still pass.
This commit is contained in:
Andrew M. Kuchling 2008-05-10 19:51:55 +00:00
parent 4ff7fc49c6
commit cd8001c8ed
9 changed files with 158 additions and 84 deletions

View File

@ -55,11 +55,40 @@ The .pypirc file
The format of the :file:`.pypirc` file is as follows:: The format of the :file:`.pypirc` file is as follows::
[server-login] [distutils]
index-servers =
pypi
[pypi]
repository: <repository-url> repository: <repository-url>
username: <username> username: <username>
password: <password> password: <password>
*repository* can be omitted and defaults to ``http://www.python.org/pypi``. *repository* can be omitted and defaults to ``http://www.python.org/pypi``.
If you want to define another server a new section can be created::
[distutils]
index-servers =
pypi
other
[pypi]
repository: <repository-url>
username: <username>
password: <password>
[other]
repository: http://example.com/pypi
username: <username>
password: <password>
The command can then be called with the -r option::
python setup.py register -r http://example.com/pypi
Or even with the section name::
python setup.py register -r other

View File

@ -24,14 +24,20 @@ The :command:`upload` command uses the username, password, and repository URL
from the :file:`$HOME/.pypirc` file (see section :ref:`pypirc` for more on this from the :file:`$HOME/.pypirc` file (see section :ref:`pypirc` for more on this
file). file).
You can specify another PyPI server with the :option:`--repository=*url*` option::
python setup.py sdist bdist_wininst upload -r http://example.com/pypi
See section :ref:`pypirc` for more on defining several servers.
You can use the :option:`--sign` option to tell :command:`upload` to sign each You can use the :option:`--sign` option to tell :command:`upload` to sign each
uploaded file using GPG (GNU Privacy Guard). The :program:`gpg` program must uploaded file using GPG (GNU Privacy Guard). The :program:`gpg` program must
be available for execution on the system :envvar:`PATH`. You can also specify be available for execution on the system :envvar:`PATH`. You can also specify
which key to use for signing using the :option:`--identity=*name*` option. which key to use for signing using the :option:`--identity=*name*` option.
Other :command:`upload` options include :option:`--repository=*url*` (which Other :command:`upload` options include :option:`--repository=*url*`
lets you override the repository setting from :file:`$HOME/.pypirc`), and or :option:`--repository=*section*` where `url` is the url of the server
and `section` the name of the section in :file:`$HOME/.pypirc`, and
:option:`--show-response` (which displays the full response text from the PyPI :option:`--show-response` (which displays the full response text from the PyPI
server for help in debugging upload problems). server for help in debugging upload problems).

View File

@ -8,37 +8,29 @@ Implements the Distutils 'register' command (register with the repository).
__revision__ = "$Id$" __revision__ = "$Id$"
import os, string, urllib2, getpass, urlparse import os, string, urllib2, getpass, urlparse
import StringIO, ConfigParser import StringIO
from distutils.core import Command from distutils.core import PyPIRCCommand
from distutils.errors import * from distutils.errors import *
from distutils import log
class register(Command): class register(PyPIRCCommand):
description = ("register the distribution with the Python package index") description = ("register the distribution with the Python package index")
user_options = PyPIRCCommand.user_options + [
DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi'
user_options = [
('repository=', 'r',
"url of repository [default: %s]"%DEFAULT_REPOSITORY),
('list-classifiers', None, ('list-classifiers', None,
'list the valid Trove classifiers'), 'list the valid Trove classifiers'),
('show-response', None,
'display full response text from server'),
] ]
boolean_options = ['verify', 'show-response', 'list-classifiers'] boolean_options = PyPIRCCommand.boolean_options + [
'verify', 'list-classifiers']
def initialize_options(self): def initialize_options(self):
self.repository = None PyPIRCCommand.initialize_options(self)
self.show_response = 0
self.list_classifiers = 0 self.list_classifiers = 0
def finalize_options(self):
if self.repository is None:
self.repository = self.DEFAULT_REPOSITORY
def run(self): def run(self):
self.finalize_options()
self._set_config()
self.check_metadata() self.check_metadata()
if self.dry_run: if self.dry_run:
self.verify_metadata() self.verify_metadata()
@ -77,6 +69,23 @@ class register(Command):
"or (maintainer and maintainer_email) " + "or (maintainer and maintainer_email) " +
"must be supplied") "must be supplied")
def _set_config(self):
''' Reads the configuration file and set attributes.
'''
config = self._read_pypirc()
if config != {}:
self.username = config['username']
self.password = config['password']
self.repository = config['repository']
self.realm = config['realm']
self.has_config = True
else:
if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
raise ValueError('%s not found in .pypirc' % self.repository)
if self.repository == 'pypi':
self.repository = self.DEFAULT_REPOSITORY
self.has_config = False
def classifiers(self): def classifiers(self):
''' Fetch the list of classifiers from the server. ''' Fetch the list of classifiers from the server.
''' '''
@ -90,6 +99,7 @@ class register(Command):
(code, result) = self.post_to_server(self.build_post_data('verify')) (code, result) = self.post_to_server(self.build_post_data('verify'))
print 'Server response (%s): %s'%(code, result) print 'Server response (%s): %s'%(code, result)
def send_metadata(self): def send_metadata(self):
''' Send the metadata to the package index server. ''' Send the metadata to the package index server.
@ -99,10 +109,14 @@ class register(Command):
First we try to read the username/password from $HOME/.pypirc, First we try to read the username/password from $HOME/.pypirc,
which is a ConfigParser-formatted file with a section which is a ConfigParser-formatted file with a section
[server-login] containing username and password entries (both [distutils] containing username and password entries (both
in clear text). Eg: in clear text). Eg:
[server-login] [distutils]
index-servers =
pypi
[pypi]
username: fred username: fred
password: sekrit password: sekrit
@ -114,21 +128,15 @@ class register(Command):
3. set the password to a random string and email the user. 3. set the password to a random string and email the user.
''' '''
choice = 'x'
username = password = ''
# see if we can short-cut and get the username/password from the # see if we can short-cut and get the username/password from the
# config # config
config = None if self.has_config:
if 'HOME' in os.environ: choice = '1'
rc = os.path.join(os.environ['HOME'], '.pypirc') username = self.username
if os.path.exists(rc): password = self.password
print 'Using PyPI login from %s'%rc else:
config = ConfigParser.ConfigParser() choice = 'x'
config.read(rc) username = password = ''
username = config.get('server-login', 'username')
password = config.get('server-login', 'password')
choice = '1'
# get the user's login info # get the user's login info
choices = '1 2 3 4'.split() choices = '1 2 3 4'.split()
@ -155,32 +163,24 @@ Your selection [default 1]: ''',
# set up the authentication # set up the authentication
auth = urllib2.HTTPPasswordMgr() auth = urllib2.HTTPPasswordMgr()
host = urlparse.urlparse(self.repository)[1] host = urlparse.urlparse(self.repository)[1]
auth.add_password('pypi', host, username, password) auth.add_password(self.realm, host, username, password)
# send the info to the server and report the result # send the info to the server and report the result
code, result = self.post_to_server(self.build_post_data('submit'), code, result = self.post_to_server(self.build_post_data('submit'),
auth) auth)
print 'Server response (%s): %s'%(code, result) print 'Server response (%s): %s' % (code, result)
# possibly save the login # possibly save the login
if 'HOME' in os.environ and config is None and code == 200: if not self.has_config and code == 200:
rc = os.path.join(os.environ['HOME'], '.pypirc')
print 'I can store your PyPI login so future submissions will be faster.' print 'I can store your PyPI login so future submissions will be faster.'
print '(the login will be stored in %s)'%rc print '(the login will be stored in %s)' % self._get_rc_file()
choice = 'X' choice = 'X'
while choice.lower() not in 'yn': while choice.lower() not in 'yn':
choice = raw_input('Save your login (y/N)?') choice = raw_input('Save your login (y/N)?')
if not choice: if not choice:
choice = 'n' choice = 'n'
if choice.lower() == 'y': if choice.lower() == 'y':
f = open(rc, 'w') self._store_pypirc(username, password)
f.write('[server-login]\nusername:%s\npassword:%s\n'%(
username, password))
f.close()
try:
os.chmod(rc, 0600)
except:
pass
elif choice == '2': elif choice == '2':
data = {':action': 'user'} data = {':action': 'user'}
data['name'] = data['password'] = data['email'] = '' data['name'] = data['password'] = data['email'] = ''
@ -243,7 +243,8 @@ Your selection [default 1]: ''',
def post_to_server(self, data, auth=None): def post_to_server(self, data, auth=None):
''' Post a query to the server, and return a string response. ''' Post a query to the server, and return a string response.
''' '''
self.announce('Registering %s to %s' % (data['name'],
self.repository), log.INFO)
# Build up the MIME payload for the urllib2 POST data # Build up the MIME payload for the urllib2 POST data
boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
sep_boundary = '\n--' + boundary sep_boundary = '\n--' + boundary

View File

@ -3,7 +3,7 @@
Implements the Distutils 'upload' subcommand (upload package to PyPI).""" Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
from distutils.errors import * from distutils.errors import *
from distutils.core import Command from distutils.core import PyPIRCCommand
from distutils.spawn import spawn from distutils.spawn import spawn
from distutils import log from distutils import log
from hashlib import md5 from hashlib import md5
@ -16,53 +16,38 @@ import base64
import urlparse import urlparse
import cStringIO as StringIO import cStringIO as StringIO
class upload(Command): class upload(PyPIRCCommand):
description = "upload binary package to PyPI" description = "upload binary package to PyPI"
DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi' user_options = PyPIRCCommand.user_options + [
user_options = [
('repository=', 'r',
"url of repository [default: %s]" % DEFAULT_REPOSITORY),
('show-response', None,
'display full response text from server'),
('sign', 's', ('sign', 's',
'sign files to upload using gpg'), 'sign files to upload using gpg'),
('identity=', 'i', 'GPG identity used to sign files'), ('identity=', 'i', 'GPG identity used to sign files'),
] ]
boolean_options = ['show-response', 'sign']
boolean_options = PyPIRCCommand.boolean_options + ['sign']
def initialize_options(self): def initialize_options(self):
PyPIRCCommand.initialize_options(self)
self.username = '' self.username = ''
self.password = '' self.password = ''
self.repository = ''
self.show_response = 0 self.show_response = 0
self.sign = False self.sign = False
self.identity = None self.identity = None
def finalize_options(self): def finalize_options(self):
PyPIRCCommand.finalize_options(self)
if self.identity and not self.sign: if self.identity and not self.sign:
raise DistutilsOptionError( raise DistutilsOptionError(
"Must use --sign for --identity to have meaning" "Must use --sign for --identity to have meaning"
) )
if 'HOME' in os.environ: config = self._read_pypirc()
rc = os.path.join(os.environ['HOME'], '.pypirc') if config != {}:
if os.path.exists(rc): self.username = config['username']
self.announce('Using PyPI login from %s' % rc) self.password = config['password']
config = ConfigParser.ConfigParser({ self.repository = config['repository']
'username':'', self.realm = config['realm']
'password':'',
'repository':''})
config.read(rc)
if not self.repository:
self.repository = config.get('server-login', 'repository')
if not self.username:
self.username = config.get('server-login', 'username')
if not self.password:
self.password = config.get('server-login', 'password')
if not self.repository:
self.repository = self.DEFAULT_REPOSITORY
def run(self): def run(self):
if not self.distribution.dist_files: if not self.distribution.dist_files:

View File

@ -20,6 +20,7 @@ from distutils.util import grok_environment_error
# Mainly import these so setup scripts can "from distutils.core import" them. # Mainly import these so setup scripts can "from distutils.core import" them.
from distutils.dist import Distribution from distutils.dist import Distribution
from distutils.cmd import Command from distutils.cmd import Command
from distutils.config import PyPIRCCommand
from distutils.extension import Extension from distutils.extension import Extension
# This is a barebones help message generated displayed when the user # This is a barebones help message generated displayed when the user

View File

@ -343,10 +343,9 @@ Common commands: (see '--help-commands' for more)
user_filename = "pydistutils.cfg" user_filename = "pydistutils.cfg"
# And look for the user config file # And look for the user config file
if 'HOME' in os.environ: user_file = os.path.join(os.path.expanduser('~'), user_filename)
user_file = os.path.join(os.environ.get('HOME'), user_filename) if os.path.isfile(user_file):
if os.path.isfile(user_file): files.append(user_file)
files.append(user_file)
# All platforms support local setup.cfg # All platforms support local setup.cfg
local_file = "setup.cfg" local_file = "setup.cfg"

View File

@ -55,6 +55,7 @@ class DistributionTestCase(unittest.TestCase):
self.assertEqual(d.get_command_packages(), ["distutils.command"]) self.assertEqual(d.get_command_packages(), ["distutils.command"])
def test_command_packages_cmdline(self): def test_command_packages_cmdline(self):
from distutils.tests.test_dist import test_dist
sys.argv.extend(["--command-packages", sys.argv.extend(["--command-packages",
"foo.bar,distutils.tests", "foo.bar,distutils.tests",
"test_dist", "test_dist",
@ -65,6 +66,7 @@ class DistributionTestCase(unittest.TestCase):
self.assertEqual(d.get_command_packages(), self.assertEqual(d.get_command_packages(),
["distutils.command", "foo.bar", "distutils.tests"]) ["distutils.command", "foo.bar", "distutils.tests"])
cmd = d.get_command_obj("test_dist") cmd = d.get_command_obj("test_dist")
print cmd.__class__, test_dist
self.assert_(isinstance(cmd, test_dist)) self.assert_(isinstance(cmd, test_dist))
self.assertEqual(cmd.sample_option, "sometext") self.assertEqual(cmd.sample_option, "sometext")
@ -179,9 +181,54 @@ class MetadataTestCase(unittest.TestCase):
dist.metadata.write_pkg_file(sio) dist.metadata.write_pkg_file(sio)
return sio.getvalue() return sio.getvalue()
def test_custom_pydistutils(self):
# fixes #2166
# make sure pydistutils.cfg is found
old = {}
for env in ('HOME', 'HOMEPATH', 'HOMEDRIVE'):
value = os.environ.get(env)
old[env] = value
if value is not None:
del os.environ[env]
if os.name == 'posix':
user_filename = ".pydistutils.cfg"
else:
user_filename = "pydistutils.cfg"
curdir = os.path.dirname(__file__)
user_filename = os.path.join(curdir, user_filename)
f = open(user_filename, 'w')
f.write('.')
f.close()
try:
dist = distutils.dist.Distribution()
# linux-style
if sys.platform in ('linux', 'darwin'):
os.environ['HOME'] = curdir
files = dist.find_config_files()
self.assert_(user_filename in files)
# win32-style
if sys.platform == 'win32':
# home drive should be found
os.environ['HOMEPATH'] = curdir
files = dist.find_config_files()
self.assert_(user_filename in files)
finally:
for key, value in old.items():
if value is None:
continue
os.environ[key] = value
os.remove(user_filename)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(DistributionTestCase)) suite.addTest(unittest.makeSuite(DistributionTestCase))
suite.addTest(unittest.makeSuite(MetadataTestCase)) suite.addTest(unittest.makeSuite(MetadataTestCase))
return suite return suite
if __name__ == "__main__":
unittest.main(defaultTest="test_suite")

View File

@ -758,3 +758,4 @@ Siebren van der Zee
Uwe Zessin Uwe Zessin
Amaury Forgeot d'Arc Amaury Forgeot d'Arc
Peter Åstrand Peter Åstrand
Tarek ZiadŽ

View File

@ -369,6 +369,11 @@ Library
platforms when x is too large to fit into an IEEE 754 float; previously platforms when x is too large to fit into an IEEE 754 float; previously
it only raised OverflowError on non IEEE 754 platforms. it only raised OverflowError on non IEEE 754 platforms.
- Issues #2166, #1741 and #1531505: now distutils deals with HOME
correctly under win32
- distutils: added multiple server support in .pypirc
- Issue #1106316: pdb.post_mortem()'s parameter, "traceback", is now - Issue #1106316: pdb.post_mortem()'s parameter, "traceback", is now
optional: it defaults to the traceback of the exception that is currently optional: it defaults to the traceback of the exception that is currently
being handled (is mandatory to be in the middle of an exception, otherwise being handled (is mandatory to be in the middle of an exception, otherwise