Repository: pypa/pip

Issue:
--force-reinstall should probably work without needing to pass --upgrade
It's always bugged me to need to pass `-U` here, and I don't see an open ticket for it.

`--force-reinstall` should force a reinstall I think, upgrade or not.



Discussion:
I'm also quite confused with the flags force_reinstall and ignore_installed (CF pip/req.py).

Actually force_reinstall seems to do the same as ignore_installed, except that it also works in the context of an upgrade.

Why not get rid of force_reinstall, and simply advocate ignore_installed to force reinstallation in any case ? What are the use cases for this flag ?

I've just had a co-worker get very confused by this. IMO `--force-reinstall` should do exactly what it says on the tin - reinstall the given package without requiring any other arguments.

+1, I was confused by this behavior as well.

See more recent discussion related to this here: issue #4400.
I submitted a PR for this here (with tests): https://github.com/pypa/pip/issues/4432 .

Resolving patch:
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -129,8 +129,8 @@ def __init__(self, *args, **kw):
             '--force-reinstall',
             dest='force_reinstall',
             action='store_true',
-            help='When upgrading, reinstall all packages even if they are '
-                 'already up-to-date.')
+            help='Reinstall all packages even if they are already '
+                 'up-to-date.')
 
         cmd_opts.add_option(
             '-I', '--ignore-installed',
diff --git a/src/pip/_internal/resolve.py b/src/pip/_internal/resolve.py
--- a/src/pip/_internal/resolve.py
+++ b/src/pip/_internal/resolve.py
@@ -150,6 +150,10 @@ def _check_skip_installed(self, req_to_install):
         if not req_to_install.satisfied_by:
             return None
 
+        if self.force_reinstall:
+            self._set_req_to_reinstall(req_to_install)
+            return None
+
         if not self._is_upgrade_allowed(req_to_install):
             if self.upgrade_strategy == "only-if-needed":
                 return 'not upgraded as not directly required'
@@ -158,7 +162,7 @@ def _check_skip_installed(self, req_to_install):
         # Check for the possibility of an upgrade.  For link-based
         # requirements we have to pull the tree down and inspect to assess
         # the version #, so it's handled way down.
-        if not (self.force_reinstall or req_to_install.link):
+        if not req_to_install.link:
             try:
                 self.finder.find_requirement(req_to_install, upgrade=True)
             except BestVersionAlreadyInstalled:
@@ -216,7 +220,7 @@ def _get_abstract_dist_for(self, req):
         if req.satisfied_by:
             should_modify = (
                 self.upgrade_strategy != "to-satisfy-only" or
-                self.ignore_installed
+                self.force_reinstall or self.ignore_installed
             )
             if should_modify:
                 self._set_req_to_reinstall(req)


Test patch:
diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/test_install_force_reinstall.py
@@ -0,0 +1,47 @@
+import pytest
+
+from tests.lib import assert_all_changes
+
+
+def check_installed_version(script, package, expected):
+    result = script.pip('show', package)
+    lines = result.stdout.splitlines()
+    version = None
+    for line in lines:
+        if line.startswith('Version: '):
+            version = line.split()[-1]
+            break
+    assert version == expected, 'version {} != {}'.format(version, expected)
+
+
+def check_force_reinstall(script, specifier, expected):
+    """
+    Args:
+      specifier: the requirement specifier to force-reinstall.
+      expected: the expected version after force-reinstalling.
+    """
+    result = script.pip_install_local('simplewheel==1.0')
+    check_installed_version(script, 'simplewheel', '1.0')
+
+    result2 = script.pip_install_local('--force-reinstall', specifier)
+    assert result2.files_updated, 'force-reinstall failed'
+    check_installed_version(script, 'simplewheel', expected)
+
+    result3 = script.pip('uninstall', 'simplewheel', '-y', expect_error=True)
+    assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+
+
+def test_force_reinstall_with_no_version_specifier(script):
+    """
+    Check --force-reinstall when there is no version specifier and the
+    installed version is not the newest version.
+    """
+    check_force_reinstall(script, 'simplewheel', '2.0')
+
+
+def test_force_reinstall_with_same_version_specifier(script):
+    """
+    Check --force-reinstall when the version specifier equals the installed
+    version and the installed version is not the newest version.
+    """
+    check_force_reinstall(script, 'simplewheel==1.0', '1.0')