From e8ea1faf9c69a14ebd07007b357d072966c8f89a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 27 Apr 2025 18:04:00 -0400 Subject: [PATCH] Support --dev flag for uninstall --- docs/commands.md | 8 +- pipenv/cli/options.py | 21 ++++- pipenv/routines/uninstall.py | 8 ++ tests/integration/test_uninstall_dev.py | 103 ++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_uninstall_dev.py diff --git a/docs/commands.md b/docs/commands.md index 8e0669324..e0ea00c64 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -42,8 +42,9 @@ The user can provide these additional parameters: ## uninstall -``$ pipenv uninstall`` supports all of the parameters in `pipenv install, as well as two additional options, -``--all`` and ``--all-dev``. +``$ pipenv uninstall`` supports all of the parameters in `pipenv install, as well as these additional options: + + - --dev — This parameter will uninstall packages from the dev-packages section. - --all — This parameter will purge all files from the virtual environment, but leave the Pipfile untouched. @@ -51,6 +52,9 @@ The user can provide these additional parameters: - --all-dev — This parameter will remove all of the development packages from the virtual environment, and remove them from the Pipfile. + - --categories — This parameter allows you to specify which categories to uninstall from. + For example: ``pipenv uninstall ruff --categories dev-packages`` + ## lock diff --git a/pipenv/cli/options.py b/pipenv/cli/options.py index 0a4d7de9b..c6ba2c800 100644 --- a/pipenv/cli/options.py +++ b/pipenv/cli/options.py @@ -187,9 +187,24 @@ def lock_dev_option(f): def uninstall_dev_option(f): - return _dev_option( - f, "Deprecated (as it has no effect). May be removed in a future release." - ) + def callback(ctx, param, value): + state = ctx.ensure_object(State) + state.installstate.dev = value + if value: + state.installstate.categories.append("dev-packages") + return value + + return option( + "--dev", + "-d", + is_flag=True, + default=False, + type=click_types.BOOL, + help="Uninstall packages from dev-packages.", + callback=callback, + expose_value=False, + show_envvar=True, + )(f) def pre_option(f): diff --git a/pipenv/routines/uninstall.py b/pipenv/routines/uninstall.py index 664634e1b..aebc3e070 100644 --- a/pipenv/routines/uninstall.py +++ b/pipenv/routines/uninstall.py @@ -62,6 +62,14 @@ def do_uninstall( if not categories: categories = ["default"] + # If dev flag is set but no dev-packages in categories, add it + if ( + "dev-packages" not in categories + and "develop" not in categories + and project.s.PIPENV_DEV + ): + categories.append("dev-packages") + lockfile_content = project.lockfile_content if all_dev: diff --git a/tests/integration/test_uninstall_dev.py b/tests/integration/test_uninstall_dev.py new file mode 100644 index 000000000..03d47addf --- /dev/null +++ b/tests/integration/test_uninstall_dev.py @@ -0,0 +1,103 @@ +import pytest + + +@pytest.mark.install +@pytest.mark.uninstall +def test_uninstall_dev_flag(pipenv_instance_private_pypi): + """Ensure that running `pipenv uninstall --dev` properly removes packages from dev-packages""" + with pipenv_instance_private_pypi() as p: + with open(p.pipfile_path, "w") as f: + contents = """ +[packages] +six = "*" + +[dev-packages] +pytest = "*" + """.strip() + f.write(contents) + + # Install both packages + c = p.pipenv("install --dev") + assert c.returncode == 0 + assert "six" in p.pipfile["packages"] + assert "pytest" in p.pipfile["dev-packages"] + assert "six" in p.lockfile["default"] + assert "pytest" in p.lockfile["develop"] + + # Verify both packages are installed + c = p.pipenv('run python -c "import six, pytest"') + assert c.returncode == 0 + + # Uninstall pytest with --dev flag + c = p.pipenv("uninstall pytest --dev") + assert c.returncode == 0 + + # Verify pytest was removed from dev-packages + assert "six" in p.pipfile["packages"] + assert "pytest" not in p.pipfile["dev-packages"] + assert "six" in p.lockfile["default"] + assert "pytest" not in p.lockfile["develop"] + + # Verify pytest is no longer importable + c = p.pipenv('run python -c "import pytest"') + assert c.returncode != 0 + + # Verify six is still importable + c = p.pipenv('run python -c "import six"') + assert c.returncode == 0 + + +@pytest.mark.install +@pytest.mark.uninstall +def test_uninstall_dev_flag_with_categories(pipenv_instance_private_pypi): + """Ensure that running `pipenv uninstall --dev` works the same as `--categories dev-packages`""" + with pipenv_instance_private_pypi() as p: + with open(p.pipfile_path, "w") as f: + contents = """ +[packages] +six = "*" + +[dev-packages] +pytest = "*" + """.strip() + f.write(contents) + + # Install both packages + c = p.pipenv("install --dev") + assert c.returncode == 0 + + # Create a second project to test with categories + with pipenv_instance_private_pypi() as p2: + with open(p2.pipfile_path, "w") as f: + contents = """ +[packages] +six = "*" + +[dev-packages] +pytest = "*" + """.strip() + f.write(contents) + + # Install both packages + c = p2.pipenv("install --dev") + assert c.returncode == 0 + + # Uninstall pytest with --categories + c = p2.pipenv("uninstall pytest --categories dev-packages") + assert c.returncode == 0 + + # Verify pytest was removed from dev-packages + assert "six" in p2.pipfile["packages"] + assert "pytest" not in p2.pipfile["dev-packages"] + assert "six" in p2.lockfile["default"] + assert "pytest" not in p2.lockfile["develop"] + + # Compare with first project + c = p.pipenv("uninstall pytest --dev") + assert c.returncode == 0 + + # Verify both approaches have the same result + assert p.pipfile["packages"] == p2.pipfile["packages"] + assert p.pipfile["dev-packages"] == p2.pipfile["dev-packages"] + assert p.lockfile["default"] == p2.lockfile["default"] + assert p.lockfile["develop"] == p2.lockfile["develop"]