mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Merge pull request #1215 from pnasrat/subdirectory-editable
Merge subdirectory editable feature
This commit is contained in:
commit
ad66052f05
6 changed files with 149 additions and 30 deletions
|
@ -31,6 +31,9 @@ Changelog
|
|||
|
||||
* Added warnings when using an insecure index, find-link, or dependency link. (Pull #1121)
|
||||
|
||||
* Added support for installing packages from a subdirectory using the ``subdirectory``
|
||||
editable option. ( Pull request #1082 )
|
||||
|
||||
|
||||
1.4.2 (unreleased)
|
||||
------------------
|
||||
|
|
|
@ -88,7 +88,7 @@ Examples
|
|||
$ pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial
|
||||
$ pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn
|
||||
$ pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch
|
||||
|
||||
$ pip install -e git+https://git.repo/some_repo.git@egg=subdir&subdirectory=subdir_path # install a python package from a repo subdirectory
|
||||
|
||||
6) Install a package with `setuptools extras`_.
|
||||
|
||||
|
@ -414,4 +414,3 @@ Options
|
|||
**Other Options:**
|
||||
|
||||
* :ref:`General Options <General Options>`
|
||||
|
||||
|
|
108
pip/req.py
108
pip/req.py
|
@ -39,7 +39,7 @@ class InstallRequirement(object):
|
|||
|
||||
def __init__(self, req, comes_from, source_dir=None, editable=False,
|
||||
url=None, as_egg=False, update=True, prereleases=None,
|
||||
from_bundle=False):
|
||||
editable_options=None, from_bundle=False):
|
||||
self.extras = ()
|
||||
if isinstance(req, string_types):
|
||||
req = pkg_resources.Requirement.parse(req)
|
||||
|
@ -48,6 +48,11 @@ class InstallRequirement(object):
|
|||
self.comes_from = comes_from
|
||||
self.source_dir = source_dir
|
||||
self.editable = editable
|
||||
|
||||
if editable_options is None:
|
||||
editable_options = {}
|
||||
|
||||
self.editable_options = editable_options
|
||||
self.url = url
|
||||
self.as_egg = as_egg
|
||||
self._egg_info_path = None
|
||||
|
@ -85,7 +90,11 @@ class InstallRequirement(object):
|
|||
else:
|
||||
source_dir = None
|
||||
|
||||
res = cls(name, comes_from, source_dir=source_dir, editable=True, url=url, prereleases=True)
|
||||
res = cls(name, comes_from, source_dir=source_dir,
|
||||
editable=True,
|
||||
url=url,
|
||||
editable_options=extras_override,
|
||||
prereleases=True)
|
||||
|
||||
if extras_override is not None:
|
||||
res.extras = extras_override
|
||||
|
@ -219,14 +228,24 @@ class InstallRequirement(object):
|
|||
|
||||
@property
|
||||
def setup_py(self):
|
||||
return os.path.join(self.source_dir, 'setup.py')
|
||||
setup_file = 'setup.py'
|
||||
|
||||
if self.editable_options and 'subdirectory' in self.editable_options:
|
||||
setup_py = os.path.join(self.source_dir,
|
||||
self.editable_options['subdirectory'],
|
||||
setup_file)
|
||||
|
||||
else:
|
||||
setup_py = os.path.join(self.source_dir, setup_file)
|
||||
|
||||
return setup_py
|
||||
|
||||
def run_egg_info(self, force_root_egg_info=False):
|
||||
assert self.source_dir
|
||||
if self.name:
|
||||
logger.notify('Running setup.py egg_info for package %s' % self.name)
|
||||
logger.notify('Running setup.py (path:%s) egg_info for package %s' % (self.setup_py, self.name))
|
||||
else:
|
||||
logger.notify('Running setup.py egg_info for package from %s' % self.url)
|
||||
logger.notify('Running setup.py (path:%s) egg_info for package from %s' % (self.setup_py, self.url))
|
||||
logger.indent += 2
|
||||
try:
|
||||
|
||||
|
@ -1484,6 +1503,47 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
|
|||
req = InstallRequirement.from_line(line, comes_from, prereleases=getattr(options, "pre", None))
|
||||
yield req
|
||||
|
||||
def _strip_postfix(req):
|
||||
"""
|
||||
Strip req postfix ( -dev, 0.2, etc )
|
||||
"""
|
||||
## FIXME: use package_to_requirement?
|
||||
match = re.search(r'^(.*?)(?:-dev|-\d.*)$', req)
|
||||
if match:
|
||||
# Strip off -dev, -0.2, etc.
|
||||
req = match.group(1)
|
||||
return req
|
||||
|
||||
def _build_req_from_url(url):
|
||||
|
||||
parts = [p for p in url.split('#', 1)[0].split('/') if p]
|
||||
|
||||
req = None
|
||||
if parts[-2] in ('tags', 'branches', 'tag', 'branch'):
|
||||
req = parts[-3]
|
||||
elif parts[-1] == 'trunk':
|
||||
req = parts[-2]
|
||||
return req
|
||||
|
||||
def _build_editable_options(req):
|
||||
|
||||
"""
|
||||
This method generates a dictionary of the query string
|
||||
parameters contained in a given editable URL.
|
||||
"""
|
||||
regexp = re.compile(r"[\?#&](?P<name>[^&=]+)=(?P<value>[^&=]+)")
|
||||
matched = regexp.findall(req)
|
||||
|
||||
if matched:
|
||||
ret = dict()
|
||||
for option in matched:
|
||||
(name, value) = option
|
||||
if name in ret:
|
||||
raise Exception("%s option already defined" % name)
|
||||
ret[name] = value
|
||||
return ret
|
||||
return None
|
||||
|
||||
|
||||
def parse_editable(editable_req, default_vcs=None):
|
||||
"""Parses svn+http://blahblah@rev#egg=Foobar into a requirement
|
||||
|
@ -1515,37 +1575,39 @@ def parse_editable(editable_req, default_vcs=None):
|
|||
for version_control in vcs:
|
||||
if url.lower().startswith('%s:' % version_control):
|
||||
url = '%s+%s' % (version_control, url)
|
||||
break
|
||||
|
||||
if '+' not in url:
|
||||
if default_vcs:
|
||||
url = default_vcs + '+' + url
|
||||
else:
|
||||
raise InstallationError(
|
||||
'%s should either by a path to a local project or a VCS url beginning with svn+, git+, hg+, or bzr+' % editable_req)
|
||||
|
||||
vc_type = url.split('+', 1)[0].lower()
|
||||
|
||||
if not vcs.get_backend(vc_type):
|
||||
error_message = 'For --editable=%s only ' % editable_req + \
|
||||
', '.join([backend.name + '+URL' for backend in vcs.backends]) + \
|
||||
' is currently supported'
|
||||
raise InstallationError(error_message)
|
||||
match = re.search(r'(?:#|#.*?&)egg=([^&]*)', editable_req)
|
||||
if (not match or not match.group(1)) and vcs.get_backend(vc_type):
|
||||
parts = [p for p in editable_req.split('#', 1)[0].split('/') if p]
|
||||
if parts[-2] in ('tags', 'branches', 'tag', 'branch'):
|
||||
req = parts[-3]
|
||||
elif parts[-1] == 'trunk':
|
||||
req = parts[-2]
|
||||
else:
|
||||
raise InstallationError(
|
||||
'--editable=%s is not the right format; it must have #egg=Package'
|
||||
% editable_req)
|
||||
|
||||
try:
|
||||
options = _build_editable_options(editable_req)
|
||||
except Exception:
|
||||
message = sys.exc_info()[1]
|
||||
raise InstallationError(
|
||||
'--editable=%s error in editable options:%s' % (editable_req, message))
|
||||
|
||||
if not options or 'egg' not in options:
|
||||
req = _build_req_from_url(editable_req)
|
||||
if not req:
|
||||
raise InstallationError('--editable=%s is not the right format; it must have #egg=Package' % editable_req)
|
||||
else:
|
||||
req = match.group(1)
|
||||
## FIXME: use package_to_requirement?
|
||||
match = re.search(r'^(.*?)(?:-dev|-\d.*)$', req)
|
||||
if match:
|
||||
# Strip off -dev, -0.2, etc.
|
||||
req = match.group(1)
|
||||
return req, url, None
|
||||
req = options['egg']
|
||||
|
||||
package = _strip_postfix(req)
|
||||
return package, url, options
|
||||
|
||||
|
||||
class UninstallPathSet(object):
|
||||
|
|
|
@ -7,8 +7,8 @@ from mock import patch
|
|||
|
||||
from pip.backwardcompat import urllib
|
||||
from pip.req import Requirements, parse_editable, parse_requirements
|
||||
|
||||
from tests.lib import pyversion, path_to_url
|
||||
from tests.lib import (pyversion, path_to_url,
|
||||
_create_test_package_with_subdirectory)
|
||||
from tests.lib.local_repos import local_checkout
|
||||
from tests.lib.path import Path
|
||||
|
||||
|
@ -105,3 +105,13 @@ def test_install_local_editable_with_extras(script, data):
|
|||
assert script.site_packages/'easy-install.pth' in res.files_updated, str(result)
|
||||
assert script.site_packages/'LocalExtras.egg-link' in res.files_created, str(result)
|
||||
assert script.site_packages/'simple' in res.files_created, str(result)
|
||||
|
||||
|
||||
def test_install_local_editable_with_subdirectory(script):
|
||||
version_pkg_path = _create_test_package_with_subdirectory(script,
|
||||
'version_subpkg')
|
||||
result = script.pip('install', '-e',
|
||||
'%s#egg=version_subpkg&subdirectory=version_subpkg' %
|
||||
('git+file://%s' % version_pkg_path,))
|
||||
|
||||
result.assert_installed('version-subpkg')
|
||||
|
|
|
@ -381,6 +381,45 @@ def assert_all_changes(start_state, end_state, expected_changes):
|
|||
# Don't throw away this potentially useful information
|
||||
return diff
|
||||
|
||||
def _create_test_package_with_subdirectory(script, subdirectory):
|
||||
script.scratch_path.join("version_pkg").mkdir()
|
||||
version_pkg_path = script.scratch_path/'version_pkg'
|
||||
version_pkg_path.join("version_pkg.py").write(textwrap.dedent("""
|
||||
def main():
|
||||
print('0.1')
|
||||
"""))
|
||||
version_pkg_path.join("setup.py").write(textwrap.dedent("""
|
||||
from setuptools import setup, find_packages
|
||||
setup(name='version_pkg',
|
||||
version='0.1',
|
||||
packages=find_packages(),
|
||||
py_modules=['version_pkg'],
|
||||
entry_points=dict(console_scripts=['version_pkg=version_pkg:main']))
|
||||
"""))
|
||||
|
||||
subdirectory_path = version_pkg_path.join(subdirectory)
|
||||
subdirectory_path.mkdir()
|
||||
subdirectory_path.join('version_subpkg.py').write(textwrap.dedent("""
|
||||
def main():
|
||||
print('0.1')
|
||||
"""))
|
||||
|
||||
subdirectory_path.join('setup.py').write(textwrap.dedent("""
|
||||
from setuptools import setup, find_packages
|
||||
setup(name='version_subpkg',
|
||||
version='0.1',
|
||||
packages=find_packages(),
|
||||
py_modules=['version_subpkg'],
|
||||
entry_points=dict(console_scripts=['version_pkg=version_subpkg:main']))
|
||||
"""))
|
||||
|
||||
script.run('git', 'init', cwd=version_pkg_path)
|
||||
script.run('git', 'add', '.', cwd=version_pkg_path)
|
||||
script.run('git', 'commit', '-q',
|
||||
'--author', 'Pip <python-virtualenv@googlegroups.com>',
|
||||
'-am', 'initial version', cwd=version_pkg_path)
|
||||
|
||||
return version_pkg_path
|
||||
|
||||
def _create_test_package(script):
|
||||
script.scratch_path.join("version_pkg").mkdir()
|
||||
|
|
|
@ -100,13 +100,19 @@ def test_parse_editable_local(isdir_mock, exists_mock, getcwd_mock, normcase_moc
|
|||
assert parse_editable('foo', 'git') == (None, 'file:///some/path/foo', None)
|
||||
|
||||
def test_parse_editable_default_vcs():
|
||||
assert parse_editable('https://foo#egg=foo', 'git') == ('foo', 'git+https://foo#egg=foo', None)
|
||||
assert parse_editable('https://foo#egg=foo', 'git') == ('foo',
|
||||
'git+https://foo#egg=foo',
|
||||
{'egg': 'foo'})
|
||||
|
||||
def test_parse_editable_explicit_vcs():
|
||||
assert parse_editable('svn+https://foo#egg=foo', 'git') == ('foo', 'svn+https://foo#egg=foo', None)
|
||||
assert parse_editable('svn+https://foo#egg=foo', 'git') == ('foo',
|
||||
'svn+https://foo#egg=foo',
|
||||
{'egg': 'foo'})
|
||||
|
||||
def test_parse_editable_vcs_extras():
|
||||
assert parse_editable('svn+https://foo#egg=foo[extras]', 'git') == ('foo[extras]', 'svn+https://foo#egg=foo[extras]', None)
|
||||
assert parse_editable('svn+https://foo#egg=foo[extras]', 'git') == ('foo[extras]',
|
||||
'svn+https://foo#egg=foo[extras]',
|
||||
{'egg': 'foo[extras]'})
|
||||
|
||||
@patch('os.path.normcase')
|
||||
@patch('pip.req.os.getcwd')
|
||||
|
|
Loading…
Reference in a new issue