diff --git a/CHANGES.txt b/CHANGES.txt index 1f898c733..3c4559526 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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`). diff --git a/pip/req/req_install.py b/pip/req/req_install.py index 1a98f377b..435b4f8bb 100644 --- a/pip/req/req_install.py +++ b/pip/req/req_install.py @@ -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() diff --git a/pip/wheel.py b/pip/wheel.py index 9ac9dffed..c58d9fe2f 100644 --- a/pip/wheel.py +++ b/pip/wheel.py @@ -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 diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 61aff3b1b..2877b2df4 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -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): """ diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 931c7f54e..9f9a8bffc 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -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(' = ')]), {}, )