1
1
Fork 0
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:
Paul Nasrat 2013-10-05 19:13:10 -07:00
commit ad66052f05
6 changed files with 149 additions and 30 deletions

View file

@ -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)
------------------

View file

@ -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>`

View file

@ -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):

View file

@ -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')

View file

@ -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()

View file

@ -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')