Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort packages alphabetically inside each category #5965

Merged
merged 12 commits into from Oct 20, 2023

Conversation

daveschaefer
Copy link
Contributor

@daveschaefer daveschaefer commented Oct 5, 2023

Add a new [pipenv] directive to sort package names alphabetically inside the category, for easier reading.

  • sort on install
  • sort on uninstall
  • sort on upgrade
  • Does this need to run on update?

--

Unsure if this is the best place or way to implement. Small prototype to add to discussion in #5964 .

Tests:

before patch:

AssertionError: assert ['atomicwrite...ama', 'build'] == ['atomicwrite...', 'colorama']
  At index 1 diff: 'colorama' != 'build'
  Full diff:
  - ['atomicwrites', 'build', 'colorama']
  ?                  ---------
  + ['atomicwrites', 'colorama', 'build']
  ?                            +++++++++

after patch: pass.


The checklist

  • Associated issue
  • A news fragment in the news/ directory to describe this fix with the extension .bugfix.rst, .feature.rst, .behavior.rst, .doc.rst. .vendor.rst. or .trivial.rst (this will appear in the release changelog). Use semantic line breaks and name the file after the issue number or the PR #.

When installing any package, sort all package names alphabetically inside the category, for easier reading.

Unsure if this is the best place or way to implement.
Small prototype to add to discussion in pypa#5964

Tests:

before patch:

```
AssertionError: assert ['atomicwrite...ama', 'build'] == ['atomicwrite...', 'colorama']
  At index 1 diff: 'colorama' != 'build'
  Full diff:
  - ['atomicwrites', 'build', 'colorama']
  ?                  ---------
  + ['atomicwrites', 'colorama', 'build']
  ?                            +++++++++
```

after patch: pass.
assert c.returncode == 0
assert "build" in p.pipfile["packages"]
assert list(p.pipfile["packages"].keys()) == ["atomicwrites", "build", "colorama"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this test I just chose three arbitrary packages that are already used inside pipenv's own Pipfile.

@matteius
Copy link
Member

matteius commented Oct 5, 2023

I would recommend making this a behavior driven by a [pipenv] directive in the Pipfile -- the reason why is because then its opt-in/opt-out at the project level if the directive is in the Pipfile; it will sort the packages alphabetically for the project whenever a change is made, install/uninstall/update/upgrade -- but otherwise it will continue to behave as is for folks that don't want this behavior.

@daveschaefer
Copy link
Contributor Author

I would recommend making this a behavior driven by a [pipenv] directive in the Pipfile -- the reason why is because then its opt-in/opt-out at the project level if the directive is in the Pipfile; it will sort the packages alphabetically for the project whenever a change is made, install/uninstall/update/upgrade -- but otherwise it will continue to behave as is for folks that don't want this behavior.

Hello, thank you for the feedback. Implementing this as a config setting/directive sounds quite reasonable.

The [pipenv] section appears to be the same place that stores settings such as allow_prereleases, disable_pip_input, install_search_all_sources, and keep_outdated. Is that correct?
At first glance, implementing a new setting like this seems like it could simply be an if statement to check for the setting. i.e. in add_pipfile_entry_to_pipfile(), something like:

if self.settings.get("sort_alphabetical"):
    p[category] = dict(sorted(p[category].items())

Does that sound like a good way to do it?

Let me know if there is a particular setting name you want. Perhaps sort_alphabetical?

Sort packages alphabetically inside each category.
Currently runs on `install`.
@daveschaefer
Copy link
Contributor Author

Which commands should we make this run on? Perhaps install, uninstall, and upgrade - since those are the commands that normally make changes to the Pipfile?

@matteius
Copy link
Member

matteius commented Oct 8, 2023

Thanks @daveschaefer -- some thoughts:
Perhaps sort_alphabetical --> sort_pipfile

Which commands should we make this run on? Perhaps install, uninstall, and upgrade

That makes sense, just calling out: upgrade and update (update uses upgrade, but I forget if it touches Pipfile directly so worth taking a look).

`sort_pipfile` , as requested in pypa#5965
@daveschaefer
Copy link
Contributor Author

Thanks @daveschaefer -- some thoughts: Perhaps sort_alphabetical --> sort_pipfile

Thank you. I have updated the code to use sort_pipfile, and added code and tests for uninstall.

Once this is implemented, is there a place in the docs that I can or should update to mention this new directive?
I don't think I see a specific place in the documentation that discusses the [pipfile] section. Should I add one? I can make that a separate PR if that's best.

@@ -243,3 +243,54 @@ 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.uninstall
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some of the uninstall tests are marked with @pytest.mark.install, and some are not. Is this based on whether the test calls install during the test? Something else? Let me know if you want me to mark these two tests with @pytest.mark.install also.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this based on whether the test calls install during the test?
Yeah, the test markers needs some separate TLC -- but that is the idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I have marked these tests with @pytest.mark.install.

I saw there are some other tests in test_uninstall.py that call install, but don't have the mark.
Since I am here editing the file, I marked those tests as well.
I can drop the commit if that's not helpful.

@matteius
Copy link
Member

matteius commented Oct 8, 2023

I don't think I see a specific place in the documentation that discusses the [pipfile] section. Should I add one? I can make that a separate PR if that's best.

We should include the documentation in this PR -- let's see ... probably this is the closest section that documents the Pipfile: https://pipenv.pypa.io/en/latest/pipfile/

Any TLC you put into the docs would be appreciated.

Based on notes from existing code and release docs

Note that `keep_outdated` has been discontinued,
so no docs added for it.
@daveschaefer
Copy link
Contributor Author

We should include the documentation in this PR -- let's see ... probably this is the closest section that documents the Pipfile: https://pipenv.pypa.io/en/latest/pipfile/

Any TLC you put into the docs would be appreciated.

Thank you. I added a section to pipfile.md based on the notes I could find from the codebase, docs, and release history.
I also added a note for the sort_pipfile setting that would be created with this patch.
Since it looks like the keep_outdated flag has been discontinued, I left it out and did not write anything for it.

Feedback on any of the docs or wording is welcome.

I may not have time immediately, but I will see if I can add tests and code for sorting on upgrade or update as well.

since the test calls `install` while running.
as discussed in pypa#5965
Since the goal of the mark is to note which tests use it.
Currently these fail. Will be fixed shortly in the next patch.

Pipfiles can contain different formats for package specifications.
Current default behaivour is to sort packages into groups - all string values will be sorted first, followed by all dictionary values. e.g.

```
aa = "*"
bb = "*"
cc = "*"
aaa = {version = "*"}
bbb = {version = "*"}
ccc = {version = "*"}
```

This will have to be fixed.
This is not as clean as only working with dicts.
`p[category]`, the parsed pipfile category,
is already of type `tomlkit.items.Table` when it is passed to `_sort_category()`.

Currently this is only constructing the table right before data is sent to `write_tom()`.
@daveschaefer
Copy link
Contributor Author

So, the good news: the upgrade command already calls add_pipfile_entry_to_pipfile(), where this code already does the sorting. So we may be able to keep all of the sorting code in one place.

The bad news: sorting categories and passing a dict to write_toml() works when all Pipfile entries have the same value type, but not if the category values have both strings and dictionary values.

What I mean is: If all of the package entries use strings like parse = "*", sphinx = "==4.*", the sorting works fine.
But some package entries use other descriptions, such as a dictionary like gunicorn = {version = "*"}.
In this case the toml sorting and printing will default to sorting the data so that all of the string packages like = "*" are sorted first, as a group, followed by all of the dictionary packages like = {version = "*"} after that.
The Pipfile will look like:

aa = "*"
bb = "*"
cc = "*"
aaa = {version = "*"}
bbb = {version = "*"}
ccc = {version = "*"}

I have added test cases to install and uninstall for this to enforce dealing with it.

Currently I have solved this by implementing a private _sort_category() function in project.py that takes a category dict and builds a tomlkit.table with data in the correct sorted order.
This succeeds, and sorts the Pipfile correctly. But feels like a bit of a kludge.
Technically the p[category] data is already of type tomlkit.table right where _sort_category() is called anyway. It gets called right before the data is written with write_toml(). But it feels less clean.

Let me know what you think. If there is a better way to solve this, I am all ears.
I could also move _sort_category() into toml.py if that's a better place to put the toml doc formatting code and keep it all in one place.

@daveschaefer
Copy link
Contributor Author

Is it helpful if I annotate functions with argument types and return types?

@@ -0,0 +1,52 @@
import pytest
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see any existing integration tests for the upgrade command, so I created this file.
I am attempting to match the pattern of test format from other tests.
Let me know if you want me to put these somewhere else.

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()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will definitely work, but I am wondering if it would be more efficient to move into the loop and not cast to a dict so like:
for key, value in sorted(category.items():

Correct me if that won't work or there is an issue with it, but thats my last bit of feedback. I'll see if @oz123 can review this too but I want to get this merged into the release for this month.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much! That is very kind.

Are there other test cases I should consider or other tests I can add?

I noticed some projects that have # comments on some lines of their Pipfiles. When I add a comment on both string-type lines and dictionary-type lines those are both sorted correctly.

Are there other values or types that can go on a line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if it would be more efficient to move into the loop and not cast to a dict so like: for key, value in sorted(category.items():

Thanks! I created a PR to do that - https://github.com/pypa/pipenv/pull/5990/files

Is it helpful if I create a news fragment .rst ? Or is that no longer useful since the PR has been merged?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new change is trivial and the docs change you did is good -- we just need to figure out why the docs don't seem to be getting published at some point during the new releases.

@matteius matteius merged commit 8fd6dfc into pypa:main Oct 20, 2023
16 checks passed
daveschaefer added a commit to daveschaefer/pipenv that referenced this pull request Oct 20, 2023
For pypa#5964
As suggested in pypa#5965

We don't need to cast to `dict` first, we can just sort.
@daveschaefer daveschaefer mentioned this pull request Oct 20, 2023
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants