Skip to content

Commit

Permalink
Merge pull request #5965 from daveschaefer/sort-category-alphabetical
Browse files Browse the repository at this point in the history
feat: Option to sort packages alphabetically inside each Pipfile category
  • Loading branch information
matteius committed Oct 20, 2023
2 parents 1ed9cfc + 4802425 commit 8fd6dfc
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/pipfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ This file is managed automatically through locking actions.

You should add both `Pipfile` and `Pipfile.lock` to the project's source control.

## `[pipenv]` Directives

`Pipfile` may contain a `[pipenv]` section to control the behaviour of pipenv itself. Some available settings include:

* `allow_prereleases` - Tell pipenv to install pre-release versions of a package -i.e. a version with an alpha/beta/etc. suffix, such as _1.0b1_. Equivalent to passing the `--pre` flag on the command line.
* `disable_pip_input` - Prevent pipenv from asking for input. Equivalent to the `--no-input` flag.
* `install_search_all_sources` - Allow installation of packages from an existing `Pipfile.lock` to search all defined indexes for the constrained package version and hash signatures. See [Specifying Package Indexes](indexes.md).
* `sort_pipfile` - Sort package names alphabetically inside each category. Categories will be sorted and updated on `install` and `uninstall`. This is purely cosmetic to make reading easier for humans, and has no effect on installation order or dependency resolution. Note that `Pipfile.lock` packages are always sorted alphabetically.


## Example Pipfile

Here is a simple example of a `Pipfile` and the resulting `Pipfile.lock`.
Expand Down
15 changes: 15 additions & 0 deletions pipenv/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,12 +1111,24 @@ def get_package_name_in_pipfile(self, package_name, category):
return name
return None

def _sort_category(self, category):
# toml tables won't maintain sorted dictionary order
# so construct the table in the order that we need
sorted_category = dict(sorted(category.items()))
table = tomlkit.table()
for key, value in sorted_category.items():
table.add(key, value)

return table

def remove_package_from_pipfile(self, package_name, category):
# Read and append Pipfile.
name = self.get_package_name_in_pipfile(package_name, category=category)
p = self.parsed_pipfile
if name:
del p[category][name]
if self.settings.get("sort_pipfile"):
p[category] = self._sort_category(p[category])
self.write_toml(p)
return True
return False
Expand Down Expand Up @@ -1227,6 +1239,9 @@ def add_pipfile_entry_to_pipfile(self, name, normalized_name, entry, category=No

p[category][normalized_name] = entry

if self.settings.get("sort_pipfile"):
p[category] = self._sort_category(p[category])

# Write Pipfile.
self.write_toml(p)
return newly_added, category, normalized_name
Expand Down
69 changes: 69 additions & 0 deletions tests/integration/test_install_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,72 @@ def test_install_tarball_is_actually_installed(pipenv_instance_pypi):
assert c.returncode == 0
c = p.pipenv("""run python -c "from dataclasses_json import dataclass_json" """)
assert c.returncode == 0


@pytest.mark.basic
@pytest.mark.install
def test_category_sorted_alphabetically_with_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[pipenv]
sort_pipfile = true
[packages]
atomicwrites = "*"
colorama = "*"
""".strip()
f.write(contents)
c = p.pipenv("install build")
assert c.returncode == 0
assert "build" in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == ["atomicwrites", "build", "colorama"]


@pytest.mark.basic
@pytest.mark.install
def test_sorting_handles_str_values_and_dict_values(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[pipenv]
sort_pipfile = true
[packages]
zipp = {version = "*"}
parse = "*"
colorama = "*"
atomicwrites = {version = "*"}
""".strip()
f.write(contents)
c = p.pipenv("install build")
assert c.returncode == 0
assert "build" in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == [
"atomicwrites",
"build",
"colorama",
"parse",
"zipp",
]


@pytest.mark.basic
@pytest.mark.install
def test_category_not_sorted_without_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[packages]
atomicwrites = "*"
colorama = "*"
""".strip()
f.write(contents)
c = p.pipenv("install build")
assert c.returncode == 0
assert "build" in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == [
"atomicwrites",
"colorama",
"build",
]
87 changes: 87 additions & 0 deletions tests/integration/test_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def test_uninstall_all_dev(pipenv_instance_private_pypi):
assert c.returncode == 0


@pytest.mark.install
@pytest.mark.uninstall
def test_normalize_name_uninstall(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
Expand Down Expand Up @@ -188,6 +189,7 @@ def test_uninstall_all_dev_with_shared_dependencies(pipenv_instance_pypi):
assert "six" in p.lockfile["default"]


@pytest.mark.install
@pytest.mark.uninstall
def test_uninstall_missing_parameters(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
Expand All @@ -200,6 +202,7 @@ def test_uninstall_missing_parameters(pipenv_instance_private_pypi):


@pytest.mark.categories
@pytest.mark.install
@pytest.mark.uninstall
def test_uninstall_category_with_shared_requirement(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
Expand All @@ -223,6 +226,7 @@ def test_uninstall_category_with_shared_requirement(pipenv_instance_pypi):


@pytest.mark.categories
@pytest.mark.install
@pytest.mark.uninstall
def test_uninstall_multiple_categories(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
Expand All @@ -243,3 +247,86 @@ def test_uninstall_multiple_categories(pipenv_instance_private_pypi):

assert "six" not in p.lockfile.get("prereq", {})
assert "six" not in p.lockfile["default"]


@pytest.mark.install
@pytest.mark.uninstall
def test_category_sorted_alphabetically_with_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[pipenv]
sort_pipfile = true
[packages]
parse = "*"
colorama = "*"
build = "*"
atomicwrites = "*"
""".strip()
f.write(contents)

c = p.pipenv("install")
assert c.returncode == 0

c = p.pipenv("uninstall build")
assert c.returncode == 0
assert "build" not in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == ["atomicwrites", "colorama", "parse"]


@pytest.mark.install
@pytest.mark.uninstall
def test_sorting_handles_str_values_and_dict_values(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[pipenv]
sort_pipfile = true
[packages]
zipp = "*"
parse = {version = "*"}
colorama = "*"
build = "*"
atomicwrites = {version = "*"}
""".strip()
f.write(contents)
c = p.pipenv("install")
assert c.returncode == 0

c = p.pipenv("uninstall build")
assert c.returncode == 0
assert "build" not in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == [
"atomicwrites",
"colorama",
"parse",
"zipp",
]

@pytest.mark.install
@pytest.mark.uninstall
def test_category_not_sorted_without_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[packages]
parse = "*"
colorama = "*"
build = "*"
atomicwrites = "*"
""".strip()
f.write(contents)

c = p.pipenv("install")
assert c.returncode == 0

c = p.pipenv("uninstall build")
assert c.returncode == 0
assert "build" not in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == [
"parse",
"colorama",
"atomicwrites",
]
52 changes: 52 additions & 0 deletions tests/integration/test_upgrade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest


@pytest.mark.upgrade
def test_category_sorted_alphabetically_with_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[pipenv]
sort_pipfile = true
[packages]
zipp = "*"
six = 1.11
colorama = "*"
atomicwrites = "*"
""".strip()
f.write(contents)

package_name = "six"
c = p.pipenv(f"upgrade {package_name}")
assert c.returncode == 0
assert list(p.pipfile["packages"].keys()) == [
"atomicwrites",
"colorama",
"six",
"zipp",
]


@pytest.mark.upgrade
def test_category_not_sorted_without_directive(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[packages]
zipp = "*"
six = 1.11
colorama = "*"
atomicwrites = "*"
""".strip()
f.write(contents)

package_name = "six"
c = p.pipenv(f"upgrade {package_name}")
assert c.returncode == 0
assert list(p.pipfile["packages"].keys()) == [
"zipp",
"six",
"colorama",
"atomicwrites",
]

0 comments on commit 8fd6dfc

Please sign in to comment.