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

add support for pyproject.toml #84

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open

Conversation

cgahr
Copy link
Collaborator

@cgahr cgahr commented Apr 26, 2023

TODO:

  • change root discovery, individually named files should have individual roots
  • throw an error on non-existent files
  • ignore files in .gitignore
  • rewrite test_config.py tests to fit the style of the other tests
  • restore old/removed tests to check if the behavior remained the same
  • improve fixture usage in test_files.py

I open this PR to track the changes I introduced to support pyproject.toml configuration for ssort.

This PR depends on #81. After merging #81, the first few commits in this PR should disappear since I already merged all changes from #81 into this PR.

This PR fixes #72, #62, #9.

Still missing:

  • tests for _config.py
  • restore the removed error handling in _main.py and its tests (not necessary anymore)
  • add content of .gitignore to excluded files (should this be configurable, i.e. use_gitignore=true?) -> not now, maybe later

The code works as follows:

  • _files.py implements the logic to find the root of a project
  • if a pyproject.toml exists in the root folder, _config.py parses it and creates a config object. If pyproject.toml does not exist, the config object is populated with sensible defaults.
  • the config class then provides the iterate_files_matching_pattern iterator that iterates over all files matching the pattern provided and the configuration.
  • by default files that are passed explicitly by their full name are always sorted even if they are ignored in the config. That mirror black's default behavior.

pyproject.toml

For now, the following keys are supported. They are inspired by black's and pylint's configuration:

[tool.ssort]
# skip = []
extend_skip = ['unsortable']
skip_glob = ['src/**/ugly.py']
  • The skip key contains sensible defaults for files that should be skipped. Changing skip overwrites those defaults.
  • extend_skip can be used to extend the files skipped in skip. In this case the files and folders named unsortable are skipped.
  • skip_glob parses glob expressions to skip files. In this case, all files named ugly.py located somewhere under src are not sorted.

What do you think?

Constantin Gahr added 27 commits April 25, 2023 09:14
@cgahr cgahr marked this pull request as draft April 26, 2023 14:30
This was referenced May 1, 2023
f"ERROR: {escape_path(path)} does not exist\n",
"1 file was not sortable\n",
]
expected_status = 1
Copy link
Owner

Choose a reason for hiding this comment

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

I think that the old behaviour is preferable. If files are explicitly named then they should be sorted. If they can't be sorted then it should be reported as an error.

if subpath not in paths_set:
paths_set.add(subpath)
yield subpath
return common_base
Copy link
Owner

Choose a reason for hiding this comment

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

I think it's important for the git root to be per-path. If it isn't then the results of running ssort ./repo1/ ./repo2/ won't match the results of running ssort ./repo1; ssort ./repo2/. Having a single git root will also break with git submodules.


def find_project_root(patterns):
Copy link
Owner

Choose a reason for hiding this comment

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

I think we need to track the .git root and the pyproject.toml root separately.

We want .gitigore files outside the pyproject.toml root to be respected but inside the .git root, but we want project.toml files outside the .git root to be ignored.

assert hasattr(config, "DEFAULT_SKIP")


class TestIterValidPythonFiles:
Copy link
Owner

@bwhmather bwhmather May 3, 2023

Choose a reason for hiding this comment

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

For consistency, would it be possible to rewrite these tests using the pytest test_ function style..

(tmp_path / "link2").symlink_to(tmp_path / "link1")

assert not is_ignored("link1")
assert not is_ignored("link2")
Copy link
Owner

Choose a reason for hiding this comment

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

Would it be possible to restore these tests so that we can see whether the current no-pyproject.toml behaviour has changed?

def neither(self, tmp_path):
root = tmp_path / "root"
root.mkdir()
return root
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think that having this code in fixtures is helpful. At best, it adds a layer of indirection with no abstraction. At worst, it encourages coupling. A variable binding provides equivalent abstraction with no indirection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here my intention was to have 3 fixtures, one for find a .git folder, one for finding a pyproject.toml file and lastly finding neither.

Copy link
Owner

Choose a reason for hiding this comment

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

I see where the confusion is! I was referring specifically to the pytest.fixture when I said fixture. I agree that these are fixtures, but that doesn't mean that they have to be pytest.fixtures.

I would argue that there are at least two sorts of fixtures:

  1. Fixtures which provide the background in which your test runs: e.g. tmp_path or references to a default-initialised database.
  2. Fixtures which set the specific state against which you want to test: e.g. specific files with in a temporary directory, or particular rows in a database.

There are also other things which use pytest.fixture but that aren't fixtures: pytest.monkeypatch is a good example as it just uses pytest.fixture for lifecycle management but doesn't set up any state.

Type 1 fixtures are just background and should be attract as little attention as possible when added to a test. They should usually be defined outside the test and can often be shared.

Type 2 fixtures are core to understanding what a test is asserting and so it makes sense for them to be inline and explicit. Changing them changes what all tests that use them assert so sharing them is dangerous but it may be possible to share some code to construct them.

To my mind, these are definitely type 2 fixtures and so should be inline.

@cgahr
Copy link
Collaborator Author

cgahr commented May 5, 2023

Thanks for your feedback.

which pyproject.toml to find and determining the project root

Consider the following project:

- pyproject.toml
- dir1:
   - pyproject.toml
   - file.py
- dir2
   - pyproject.toml
   - file.py

How should the discovery of pyproject.toml work?

I based the behavior of my implementation of black:

  1. black allows only one pyproject.toml per execution of black. This means

    • black .
    • black dir1
    • black dir2

    produces different ressults, depending on the respective config.

  2. black ignores the content of .gitignore by default, I will change that back.

  3. my implementation of the root detection is similar to blacks version

IMO, these defaults seem to be sensible, and they have already passed the test of time.
So my question is:

  • Will ssort follow the example of black or will it do its own thing?

handle missing files

Lastly, regarding throwing an error if a filepath is passed that does not exist. The
default of black is to exit immediatelly and not run its code. Do we want a similar
behavior? Right now, commands like

ssort dir1/doesnt_exist.py

don't throw an error and exit with error code 0 (in contrast to blacks behavior).

If we want to change this behavior, it might make sense to switch from argparse to
click (mirroring black again) and check while parsing the input paths, if the all
exist.

What do you think?

@bwhmather
Copy link
Owner

Right now, commands like

ssort dir1/doesnt_exist.py

don't throw an error and exit with error code 0 (in contrast to blacks behavior).

That's odd. This should be an error.

@bwhmather
Copy link
Owner

If we want to change this behavior, it might make sense to switch from argparse to
click (mirroring black again) and check while parsing the input paths, if the all
exist.

We need to validate at read time anyway to handle race conditions. More consistent to just have one place where we check. Also, would prefer not to add any more external dependencies than we absolutely need.

@bwhmather
Copy link
Owner

Will ssort follow the example of black or will it do its own thing?

This is a very good question. I think the ssort model of having the roots used for determining what rules to apply being determined based on the file rather than the pwd is safer and less likely to cause surprise so I am reluctant to change it. Perhaps this is a discussion worth having with the black developers?

@cgahr
Copy link
Collaborator Author

cgahr commented May 11, 2023

Right now, commands like

ssort dir1/doesnt_exist.py

don't throw an error and exit with error code 0 (in contrast to blacks behavior).

That's odd. This should be an error.

I meant that ssort still has exit code 0, so technically ssort did not throw an error.

@cgahr
Copy link
Collaborator Author

cgahr commented May 11, 2023

What discussion do you think should be had?

@bwhmather
Copy link
Owner

I meant that ssort still has exit code 0, so technically ssort did not throw an error.

That, to me, is a bug in ssort. We should probably fix it.

@bwhmather
Copy link
Owner

What discussion do you think should be had?

Politely suggest that they adopt ssorts model of having the project root be per matching file. See what they say. Respond accordingly.

Suspect that they would come back with one of three responses:

  1. No. Doing so would break backward compatibility.
  2. No. Black's model is better. Here are the reasons why.
  3. Yes.

If 2, we update ssort. If 1, someone hopefully factors the ssort logic out into a library that supports pyproject.toml/.gitignore/.hgignore/... and eventually black picks it up. If 3, we've made the lives of lots of python developers ever so slightly better.

@cgahr
Copy link
Collaborator Author

cgahr commented May 12, 2023

I'll open such an issue, but, to be completely honest, I don't think they'll change their way and I'm also not 100% behind this logic.

In my opinion, there should be one, and only one, point of configuration per project. With this way of thinking our discussion is kind of a moot point.

In addition (also in my opinion), the default way of running ssort is ssort . from the project root, which makes our discussion a moot point again.

In any case, one possible workaround could be the following:

  1. use blacks logic of discovering the configuration
  2. if ssort is run with multiple files, i.e. ssort file.py dir/file.py, we internally run ssort for each file individually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add pyproject.toml / setup.py configuration
2 participants