Skip to content

Commit 27ef3c6

Browse files
authored
Support --dev flag for uninstall (#6392)
* Support --dev flag for uninstall * Add news fragment for --dev flag in uninstall command
1 parent 49d5e76 commit 27ef3c6

File tree

6 files changed

+142
-7
lines changed

6 files changed

+142
-7
lines changed

docs/commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,20 @@ Uninstall all development packages:
158158
$ pipenv uninstall --all-dev
159159
```
160160

161+
Uninstall a dev package using --dev flag:
162+
163+
```bash
164+
$ pipenv uninstall ruff --dev
165+
```
166+
161167
### Options
162168

163169
| Option | Description |
164170
|--------|-------------|
165171
| `--all` | Remove all packages from virtual environment |
166172
| `--all-dev` | Remove all development packages |
173+
| `--dev` | Uninstall package from dev-packages section |
174+
| `--categories` | Specify which categories to uninstall from |
167175
| `--skip-lock` | Don't update Pipfile.lock after uninstalling |
168176

169177
## lock

news/6392.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for ``--dev`` flag in ``pipenv uninstall`` command to remove packages from ``dev-packages`` section directly.

pipenv/cli/options.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,24 @@ def lock_dev_option(f):
186186

187187

188188
def uninstall_dev_option(f):
189-
return _dev_option(
190-
f, "Deprecated (as it has no effect). May be removed in a future release."
191-
)
189+
def callback(ctx, param, value):
190+
state = ctx.ensure_object(State)
191+
state.installstate.dev = value
192+
if value:
193+
state.installstate.categories.append("dev-packages")
194+
return value
195+
196+
return option(
197+
"--dev",
198+
"-d",
199+
is_flag=True,
200+
default=False,
201+
type=click_types.BOOL,
202+
help="Uninstall packages from dev-packages.",
203+
callback=callback,
204+
expose_value=False,
205+
show_envvar=True,
206+
)(f)
192207

193208

194209
def pre_option(f):

pipenv/project.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,9 +1179,16 @@ def _sort_category(self, category) -> Table:
11791179

11801180
def remove_package_from_pipfile(self, package_name, category):
11811181
# Read and append Pipfile.
1182-
name = self.get_package_name_in_pipfile(package_name, category=category)
11831182
p = self.parsed_pipfile
1184-
if name:
1183+
section = p.get(category, {})
1184+
# Find the actual key in the section that matches the normalized name
1185+
normalized_name = pep423_name(package_name)
1186+
name = None
1187+
for key in section:
1188+
if pep423_name(key) == normalized_name:
1189+
name = key
1190+
break
1191+
if name and name in section:
11851192
del p[category][name]
11861193
if self.settings.get("sort_pipfile"):
11871194
p[category] = self._sort_category(p[category])
@@ -1208,7 +1215,8 @@ def remove_packages_from_pipfile(self, packages):
12081215
to_remove = packages & pipfile_packages
12091216
for pkg in to_remove:
12101217
pkg_name = self.get_package_name_in_pipfile(pkg, category=category)
1211-
del parsed[category][pkg_name]
1218+
if pkg_name:
1219+
del parsed[category][pkg_name]
12121220
self.write_toml(parsed)
12131221

12141222
def generate_package_pipfile_entry(

tests/integration/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def __exit__(self, *args):
224224
self._path = None
225225

226226
def run_command(self, cmd):
227-
result = subprocess.run(cmd, shell=True, capture_output=True, check=False)
227+
result = subprocess.run(cmd, shell=True, capture_output=True, check=False, cwd=self.path)
228228
try:
229229
std_out_decoded = result.stdout.decode("utf-8")
230230
except UnicodeDecodeError:
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import pytest
2+
3+
4+
@pytest.mark.install
5+
@pytest.mark.uninstall
6+
def test_uninstall_dev_flag(pipenv_instance_private_pypi):
7+
"""Ensure that running `pipenv uninstall --dev` properly removes packages from dev-packages"""
8+
with pipenv_instance_private_pypi() as p:
9+
with open(p.pipfile_path, "w") as f:
10+
contents = """
11+
[packages]
12+
six = "*"
13+
14+
[dev-packages]
15+
pytest = "*"
16+
""".strip()
17+
f.write(contents)
18+
19+
# Install both packages
20+
c = p.pipenv("install --dev")
21+
assert c.returncode == 0
22+
assert "six" in p.pipfile["packages"]
23+
assert "pytest" in p.pipfile["dev-packages"]
24+
assert "six" in p.lockfile["default"]
25+
assert "pytest" in p.lockfile["develop"]
26+
27+
# Verify both packages are installed
28+
c = p.pipenv('run python -c "import six, pytest"')
29+
assert c.returncode == 0
30+
31+
# Uninstall pytest with --dev flag
32+
c = p.pipenv("uninstall pytest --dev")
33+
assert c.returncode == 0
34+
35+
# Verify pytest was removed from dev-packages
36+
assert "six" in p.pipfile["packages"]
37+
assert "pytest" not in p.pipfile["dev-packages"]
38+
assert "six" in p.lockfile["default"]
39+
assert "pytest" not in p.lockfile["develop"]
40+
41+
# Verify pytest is no longer importable
42+
c = p.pipenv('run python -c "import pytest"')
43+
assert c.returncode != 0
44+
45+
# Verify six is still importable
46+
c = p.pipenv('run python -c "import six"')
47+
assert c.returncode == 0
48+
49+
50+
@pytest.mark.install
51+
@pytest.mark.uninstall
52+
def test_uninstall_dev_flag_with_categories(pipenv_instance_private_pypi):
53+
"""Ensure that running `pipenv uninstall --dev` works the same as `--categories dev-packages`"""
54+
with pipenv_instance_private_pypi() as p:
55+
with open(p.pipfile_path, "w") as f:
56+
contents = """
57+
[packages]
58+
six = "*"
59+
60+
[dev-packages]
61+
pytest = "*"
62+
""".strip()
63+
f.write(contents)
64+
65+
# Install both packages
66+
c = p.pipenv("install --dev")
67+
assert c.returncode == 0
68+
69+
# Create a second project to test with categories
70+
with pipenv_instance_private_pypi() as p2:
71+
with open(p2.pipfile_path, "w") as f:
72+
contents = """
73+
[packages]
74+
six = "*"
75+
76+
[dev-packages]
77+
pytest = "*"
78+
""".strip()
79+
f.write(contents)
80+
81+
# Install both packages
82+
c = p2.pipenv("install --dev")
83+
assert c.returncode == 0
84+
85+
# Uninstall pytest with --categories
86+
c = p2.pipenv("uninstall pytest --categories dev-packages")
87+
assert c.returncode == 0
88+
89+
# Verify pytest was removed from dev-packages
90+
assert "six" in p2.pipfile["packages"]
91+
assert "pytest" not in p2.pipfile["dev-packages"]
92+
assert "six" in p2.lockfile["default"]
93+
assert "pytest" not in p2.lockfile["develop"]
94+
95+
# Compare with first project
96+
c = p.pipenv("uninstall pytest --dev")
97+
assert c.returncode == 0
98+
99+
# Verify both approaches have the same result
100+
assert p.pipfile["packages"] == p2.pipfile["packages"]
101+
assert p.pipfile["dev-packages"] == p2.pipfile["dev-packages"]
102+
assert p.lockfile["default"] == p2.lockfile["default"]
103+
assert p.lockfile["develop"] == p2.lockfile["develop"]

0 commit comments

Comments
 (0)