Allow pip install with entrypoint names with colons (#3901)

This commit is contained in:
Francesco Montesano 2016-11-19 00:27:16 +01:00 committed by Xavier Fernandez
parent 0964c9797c
commit d64b871d97
5 changed files with 95 additions and 59 deletions

View File

@ -1,5 +1,7 @@
**9.1.0 (UNRELEASED)**
* Use pkg_resources to parse the entry points file to allow names with
colons (:pull:`3901`)
**9.0.1 (2016-11-06)**
@ -111,7 +113,6 @@
* Restore the ability to use inline comments in requirements files passed to
``pip freeze`` (:issue:`3680`).
**8.1.2 (2016-05-10)**
* Fix a regression on systems with uninitialized locale (:issue:`3575`).

View File

@ -20,7 +20,6 @@ from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version, parse as parse_version
from pip._vendor.six.moves import configparser
import pip.wheel
@ -35,7 +34,7 @@ from pip.locations import (
from pip.utils import (
display_path, rmtree, ask_path_exists, backup_dir, is_installable_dir,
dist_in_usersite, dist_in_site_packages, egg_link_path,
call_subprocess, read_text_file, FakeFile, _make_build_dir, ensure_dir,
call_subprocess, read_text_file, _make_build_dir, ensure_dir,
get_installed_version, normalize_path, dist_is_local,
)
@ -724,36 +723,41 @@ class InstallRequirement(object):
paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
# find console_scripts
if dist.has_metadata('entry_points.txt'):
if six.PY2:
options = {}
else:
options = {"delimiters": ('=', )}
config = configparser.SafeConfigParser(**options)
config.readfp(
FakeFile(dist.get_metadata_lines('entry_points.txt'))
)
if config.has_section('console_scripts'):
for name, value in config.items('console_scripts'):
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
paths_to_remove.add(os.path.join(bin_dir, name))
if WINDOWS:
paths_to_remove.add(
os.path.join(bin_dir, name) + '.exe'
)
paths_to_remove.add(
os.path.join(bin_dir, name) + '.exe.manifest'
)
paths_to_remove.add(
os.path.join(bin_dir, name) + '-script.py'
)
_scripts_to_remove = []
console_scripts = dist.get_entry_map(group='console_scripts')
for name in console_scripts.keys():
_scripts_to_remove.extend(self._script_names(dist, name, False))
# find gui_scripts
gui_scripts = dist.get_entry_map(group='gui_scripts')
for name in gui_scripts.keys():
_scripts_to_remove.extend(self._script_names(dist, name, True))
for s in _scripts_to_remove:
paths_to_remove.add(s)
paths_to_remove.remove(auto_confirm)
self.uninstalled = paths_to_remove
def _script_names(self, dist, name, is_gui):
'''Create the fully qualified name of the files created by
{console,gui}_scripts for the given ``dist``. Returns the list of file
names'''
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
exe_name = os.path.join(bin_dir, name)
paths_to_remove = [exe_name, ]
if WINDOWS:
paths_to_remove.add(exe_name + '.exe')
paths_to_remove.add(exe_name + '.exe.manifest')
if is_gui:
paths_to_remove.add(exe_name + '-script.pyw')
else:
paths_to_remove.add(exe_name + '-script.py')
return paths_to_remove
def rollback_uninstall(self):
if self.uninstalled:
self.uninstalled.rollback()

View File

@ -39,7 +39,6 @@ from pip.utils.setuptools_build import SETUPTOOLS_SHIM
from pip._vendor.distlib.scripts import ScriptMaker
from pip._vendor import pkg_resources
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.six.moves import configparser
wheel_ext = '.whl'
@ -224,16 +223,19 @@ def get_entrypoints(filename):
data.write("\n")
data.seek(0)
cp = configparser.RawConfigParser()
cp.optionxform = lambda option: option
cp.readfp(data)
# get the entry points and then the script names
entry_points = pkg_resources.EntryPoint.parse_map(data)
console = entry_points.get('console_scripts', {})
gui = entry_points.get('gui_scripts', {})
console = {}
gui = {}
if cp.has_section('console_scripts'):
console = dict(cp.items('console_scripts'))
if cp.has_section('gui_scripts'):
gui = dict(cp.items('gui_scripts'))
def _split_ep(s):
"""get the string representation of EntryPoint, remove space and split
on '='"""
return str(s).replace(" ", "").split("=")
# convert the EntryPoint objects into strings with module:function
console = dict(_split_ep(v) for v in console.values())
gui = dict(_split_ep(v) for v in gui.values())
return console, gui

View File

@ -9,6 +9,7 @@ import pretend
from os.path import join, normpath
from tempfile import mkdtemp
from tests.lib import assert_all_changes, pyversion
from tests.lib import create_test_package_with_setup
from tests.lib.local_repos import local_repo, local_checkout
from pip.req import InstallRequirement
@ -154,32 +155,54 @@ def test_uninstall_overlapping_package(script, data):
assert_all_changes(result2, result3, [])
def test_uninstall_entry_point(script):
@pytest.mark.parametrize("console_scripts",
["test_ = distutils_install",
"test_:test_ = distutils_install"])
def test_uninstall_entry_point(script, console_scripts):
"""
Test uninstall package with two or more entry points in the same section,
whose name contain a colon.
"""
script.scratch_path.join("ep_install").mkdir()
pkg_path = script.scratch_path / 'ep_install'
pkg_path.join("setup.py").write(textwrap.dedent("""
from setuptools import setup
setup(
name='ep-install',
version='0.1',
entry_points={"pip_test.ep":
["ep:name1 = distutils_install",
"ep:name2 = distutils_install"]
}
)
"""))
pkg_name = 'ep_install'
pkg_path = create_test_package_with_setup(
script,
name=pkg_name,
version='0.1',
entry_points={"console_scripts": [console_scripts, ],
"pip_test.ep":
["ep:name1 = distutils_install",
"ep:name2 = distutils_install"]
}
)
script_name = script.bin_path.join(console_scripts.split('=')[0].strip())
result = script.pip('install', pkg_path)
assert script_name.exists
result = script.pip('list', '--format=legacy')
assert "ep-install (0.1)" in result.stdout
script.pip('uninstall', 'ep_install', '-y')
assert not script_name.exists
result2 = script.pip('list', '--format=legacy')
assert "ep-install (0.1)" not in result2.stdout
def test_uninstall_gui_scripts(script):
"""
Make sure that uninstall removes gui scripts
"""
pkg_name = "gui_pkg"
pkg_path = create_test_package_with_setup(
script,
name=pkg_name,
version='0.1',
entry_points={"gui_scripts": ["test_ = distutils_install", ], }
)
script_name = script.bin_path.join('test_')
script.pip('install', pkg_path)
assert script_name.exists
script.pip('uninstall', pkg_name, '-y')
assert not script_name.exists
@pytest.mark.network
def test_uninstall_console_scripts(script):
"""

View File

@ -11,15 +11,21 @@ from pip.exceptions import InvalidWheelFilename, UnsupportedWheel
from pip.utils import unpack_file
def test_get_entrypoints(tmpdir):
with open(str(tmpdir.join("entry_points.txt")), "w") as fp:
@pytest.mark.parametrize("console_scripts",
["pip = pip.main:pip", "pip:pip = pip.main:pip"])
def test_get_entrypoints(tmpdir, console_scripts):
entry_points = tmpdir.join("entry_points.txt")
with open(str(entry_points), "w") as fp:
fp.write("""
[console_scripts]
pip = pip.main:pip
""")
{0}
[section]
common:one = module:func
common:two = module:other_func
""".format(console_scripts))
assert wheel.get_entrypoints(str(tmpdir.join("entry_points.txt"))) == (
{"pip": "pip.main:pip"},
assert wheel.get_entrypoints(str(entry_points)) == (
dict([console_scripts.split(' = ')]),
{},
)