diff --git a/pipenv/core.py b/pipenv/core.py index b26d38e1..5a933a3d 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -30,6 +30,7 @@ from pipenv.utils.dependencies import ( get_canonical_names, get_constraints_from_deps, get_lockfile_section_using_pipfile_category, + get_pipfile_category_using_lockfile_section, is_pinned, is_required_version, is_star, @@ -1065,11 +1066,7 @@ def do_lock( # Resolve package to generate constraints before resolving other categories for category in lockfile_categories: - pipfile_category = category - if pipfile_category == "develop": - pipfile_category = "dev-packages" - if pipfile_category == "default": - pipfile_category = "packages" + pipfile_category = get_pipfile_category_using_lockfile_section(category) if project.pipfile_exists: packages = project.parsed_pipfile.get(pipfile_category, {}) else: @@ -2426,13 +2423,13 @@ def do_uninstall( # Automatically use an activated virtualenv. if project.s.PIPENV_USE_SYSTEM: system = True - if categories is None: - categories = project.get_package_categories() # Ensure that virtualenv is available. ensure_project(project, three=three, python=python, pypi_mirror=pypi_mirror) # Uninstall all dependencies, if --all was provided. - if not any([packages, editable_packages, all_dev, all, categories]): + if not any([packages, editable_packages, all_dev, all]): raise exceptions.PipenvUsageError("No package provided!", ctx=ctx) + if not categories: + categories = project.get_package_categories(for_lockfile=True) editable_pkgs = [ Requirement.from_line(f"-e {p}").name for p in editable_packages if p ] @@ -2440,8 +2437,6 @@ def do_uninstall( package_names = {p for p in packages if p} package_map = {canonicalize_name(p): p for p in packages if p} installed_package_names = project.installed_package_names - # Intelligently detect if --dev should be used or not. - lockfile_packages = set() if project.lockfile_exists: project_pkg_names = project.lockfile_package_names else: @@ -2472,16 +2467,19 @@ def do_uninstall( ) ) preserve_packages = set() + dev_packages = set() for category in project.get_package_categories(for_lockfile=True): if category == "develop": - continue - preserve_packages &= set(project_pkg_names[category]) - package_names = set(project_pkg_names["develop"]) - preserve_packages + dev_packages |= set(project_pkg_names[category]) + else: + preserve_packages |= set(project_pkg_names[category]) + + package_names = dev_packages - preserve_packages # Remove known "bad packages" from the list. bad_pkgs = get_canonical_names(BAD_PACKAGES) ignored_packages = bad_pkgs & set(list(package_map.keys())) - for ignored_pkg in ignored_packages: + for ignored_pkg in get_canonical_names(ignored_packages): if project.s.is_verbose(): click.echo(f"Ignoring {ignored_pkg}.", err=True) package_names.discard(package_map[ignored_pkg]) @@ -2509,15 +2507,10 @@ def do_uninstall( for normalized, package_name in selected_pkg_map.items() if normalized in (used_packages - bad_pkgs) ] - for normalized_name, package_name in selected_pkg_map.items(): - click.secho( - fix_utf8(f"Uninstalling {click.style(package_name)}..."), - fg="green", - bold=True, - ) - found_package = False - for category in categories: - if normalized_name in lockfile_packages: + for category in categories: + category = get_lockfile_section_using_pipfile_category(category) + for normalized_name, package_name in selected_pkg_map.items(): + if normalized_name in project.lockfile_content[category]: click.echo( "{} {} {} {}".format( click.style("Removing", fg="cyan"), @@ -2527,25 +2520,22 @@ def do_uninstall( ) ) lockfile = project.get_or_create_lockfile() - if normalized_name in lockfile.default: - del lockfile.default[normalized_name] - if normalized_name in lockfile.develop: - del lockfile.develop[normalized_name] + if normalized_name in lockfile[category]: + del lockfile[category][normalized_name] lockfile.write() - if project.remove_package_from_pipfile(package_name, category=category): + pipfile_category = get_pipfile_category_using_lockfile_section(category) + if project.remove_package_from_pipfile( + package_name, category=pipfile_category + ): click.secho( - fix_utf8(f"Removed {package_name} from Pipfile category {category}"), + fix_utf8( + f"Removed {package_name} from Pipfile category {pipfile_category}" + ), fg="green", ) - found_package = True - if not found_package: - click.echo( - "No package {} to remove from Pipfile.".format( - click.style(package_name, fg="green") - ) - ) + for normalized_name, package_name in selected_pkg_map.items(): still_remains = False for category in project.get_package_categories(): if project.get_package_name_in_pipfile(normalized_name, category=category): @@ -2553,6 +2543,11 @@ def do_uninstall( if not still_remains: # Uninstall the package. if package_name in packages_to_remove: + click.secho( + fix_utf8(f"Uninstalling {click.style(package_name)}..."), + fg="green", + bold=True, + ) with project.environment.activated(): cmd = [ project_python(project, system=system), diff --git a/pipenv/project.py b/pipenv/project.py index 20d06beb..2d15f7fb 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -20,7 +20,6 @@ from pipenv.patched.pip._vendor import pkg_resources from pipenv.utils.constants import is_type_checking from pipenv.utils.dependencies import ( get_canonical_names, - get_lockfile_section_using_pipfile_category, is_editable, is_star, pep423_name, @@ -278,7 +277,7 @@ class Project: category_packages = get_canonical_names( self.lockfile_content[category].keys() ) - results[category] = category_packages + results[category] = set(category_packages) results["combined"] = results["combined"] | category_packages return results @@ -287,11 +286,10 @@ class Project: result = {} combined = set() for category in self.get_package_categories(): - lockfile_section = get_lockfile_section_using_pipfile_category(category) packages = self.get_pipfile_section(category) keys = get_canonical_names(packages.keys()) combined |= keys - result[lockfile_section] = keys + result[category] = keys result["combined"] = combined return result @@ -624,9 +622,10 @@ class Project: @property def all_packages(self): """Returns a list of all packages.""" - p = dict(self.parsed_pipfile.get("dev-packages", {})) - p.update(self.parsed_pipfile.get("packages", {})) - return p + packages = {} + for category in self.get_package_categories(): + packages.update(self.parsed_pipfile.get(category, {})) + return packages @property def packages(self): @@ -761,17 +760,17 @@ class Project: formatted_data = tomlkit.dumps(data).rstrip() except Exception: document = tomlkit.document() - for section in ("packages", "dev-packages"): - document[section] = tomlkit.table() + for category in self.get_package_categories(): + document[category] = tomlkit.table() # Convert things to inline tables — fancy :) - for package in data.get(section, {}): - if hasattr(data[section][package], "keys"): + for package in data.get(category, {}): + if hasattr(data[category][package], "keys"): table = tomlkit.inline_table() - table.update(data[section][package]) - document[section][package] = table + table.update(data[category][package]) + document[category][package] = table else: - document[section][package] = tomlkit.string( - data[section][package] + document[category][package] = tomlkit.string( + data[category][package] ) formatted_data = tomlkit.dumps(document).rstrip() diff --git a/pipenv/resolver.py b/pipenv/resolver.py index 37b3f561..467742c0 100644 --- a/pipenv/resolver.py +++ b/pipenv/resolver.py @@ -293,12 +293,7 @@ class Entry: @property def pipfile_packages(self): - from pipenv.utils.dependencies import ( - get_lockfile_section_using_pipfile_category, - ) - - lockfile_section = get_lockfile_section_using_pipfile_category(self.category) - return self.project.pipfile_package_names[lockfile_section] + return self.project.pipfile_package_names[self.category] def create_parent(self, name, specifier="*"): parent = self.create( diff --git a/pipenv/utils/dependencies.py b/pipenv/utils/dependencies.py index 6a1ca420..34798d88 100644 --- a/pipenv/utils/dependencies.py +++ b/pipenv/utils/dependencies.py @@ -40,6 +40,16 @@ def get_lockfile_section_using_pipfile_category(category): return lockfile_section +def get_pipfile_category_using_lockfile_section(category): + if category == "develop": + lockfile_section = "dev-packages" + elif category == "default": + lockfile_section = "packages" + else: + lockfile_section = category + return lockfile_section + + class HackedPythonVersion: """A Beautiful hack, which allows us to tell pip which version of Python we're using.""" diff --git a/tests/integration/test_uninstall.py b/tests/integration/test_uninstall.py index 7486594f..95dcefaa 100644 --- a/tests/integration/test_uninstall.py +++ b/tests/integration/test_uninstall.py @@ -200,11 +200,34 @@ def test_uninstall_all_dev_with_shared_dependencies(pipenv_instance_pypi): @pytest.mark.uninstall -def test_uninstall_missing_parameters(pipenv_instance_pypi): - with pipenv_instance_pypi() as p: - c = p.pipenv("install dataclasses-json") +def test_uninstall_missing_parameters(pipenv_instance_private_pypi): + with pipenv_instance_private_pypi() as p: + c = p.pipenv("install six") assert c.returncode == 0 c = p.pipenv("uninstall") assert c.returncode != 0 assert "No package provided!" in c.stderr + + +@pytest.mark.install +@pytest.mark.uninstall +def test_uninstall_category_with_shared_requirement(pipenv_instance_pypi): + with pipenv_instance_pypi() as p: + with open(p.pipfile_path, "w") as f: + contents = """ + [packages] + six = "*" + + [prereq] + six = "*" + """ + f.write(contents) + c = p.pipenv("install") + assert c.returncode == 0 + + c = p.pipenv("uninstall six --categories packages") + assert c.returncode == 0 + + assert "six" in p.lockfile["prereq"] + assert "six" not in p.lockfile["default"]