kieran-ryan / pyprojectsort Goto Github PK
View Code? Open in Web Editor NEWFormatter for pyproject.toml files
Home Page: https://kieran-ryan.github.io/pyprojectsort
License: MIT License
Formatter for pyproject.toml files
Home Page: https://kieran-ryan.github.io/pyprojectsort
License: MIT License
pyprojectsort normalises dashes in key and section names to underscores. While this is useful from the point of view of needing to read the configuration for internal use within an application, by writing it back to the file, we are modifying keys expected by various tools, breaking compatibility with those tools.
Outputted pyproject keeps dash seperators in keys:
[build-system]
build-backend = "flit.buildapi"
requires = [
"flit",
]
Outputted pyproject keeps dash seperators in keys:
[build_system]
build_backend = "flit.buildapi"
requires = [
"flit",
]
Run pyproject:
pyprojectsort
against a pyproject.toml containing the following:
[build_system]
build-backend = "flit.buildapi"
requires = [
"flit",
]
Like other Python formatters such as black and isort, having a command line option to check whether formatting would be applied - without actually applying formatting - would be useful for linting purposes, such as in a continuous integration pipeline, pre-commit git hook, or to check locally from the command line whether it is formatted correctly.
pyprojectsort --check
While a command line option could be used by a user to check whether their pyproject.toml will be reformatted (#10), it will not allow them to see the actual changes that would be made. New users might want to check how the package will reformat their pyproject file before deciding whether to adopt it, developers accustomed to a command line interface may simply want to be aware of what changes would be made; and for continuous integration pipelines and linting purposes, it could be advantageous to have traceability on what would be changed to understand how the failing code may have entered the pipeline.
Example usage:
pyprojectsort --diff
Reformatting is performed if the original pyproject is not equivalent to the modified pyproject. As these are read as a dictionary and their equivalence checked, their equivalence only evaluates to False when lists are unsorted; as for example ["a", "b"] == ["b", "a"]
evaluates to False
.
pyproject is reformatted if its format post-modification by pyprojectsort, differs from the original.
pyproject is not reformatted unless there is a change in list value order.
Create a pyproject.toml as follows:
[build-system]
requires = ["flit", "z"]
Run pyprojectsort against it:
pyprojectsort
The pyproject.toml is not reformatted and a 'left unchanged' message is displayed.
Then, swap the list order in a pyproject.toml as follows:
[build-system]
requires = ["z", "flit"]
Run pyprojectsort against it:
pyprojectsort
The pyproject.toml is reformatted as follows:
[build-system]
requires = [
"flit",
"z",
]
Alternatively to pip and PyPI, Anaconda is a popular choice for the distribution of Python packages: aimed at scientific computing, and to simplify package management and deployment.
Thus it would be useful for pyprojectsort to extend support to this user base and be installable through conda (via conda-forge) as follows:
conda install -c conda-forge pyprojectsort
When the project dependencies are sorted, they are done so alphabetically with the full content of the list. This includes operators. As a result, when we have the same package name for two packages, but one is extended with a dash, they end up in reverse order as '-' is compared against and precedes '=' alphabetically.
To resolve this issue, the logic would need to be modified to specifically check package names only and not the operators they are using to check versions, etc.
[project]
dependencies = [
"tomli==2.0.1",
"tomli-w==1.0.0",
]
[project]
dependencies = [
"tomli-w==1.0.0",
"tomli==2.0.1",
]
pyprojectsort
The keys of pyproject.toml sections are highly susceptible to change as a project matures with time. They are thus difficult to keep aligned across developers and can be a source of merge conflicts. By alphabetically sorting these keys, a standardised formatting can be applied that will ensure the output of each developer is exactly the same.
With an example of the following, unsorted section:
[tool.mypy]
mypy_path = "pyprojectsort"
exclude = "__init__.py|docs|tests|venv"
files = "."
The expect output post-formatting would be the alphabetically sorted section below:
[tool.mypy]
exclude = "__init__.py|docs|tests|venv"
files = "."
mypy_path = "pyprojectsort"
Referencing the pre-commit CI documentation:
Developers spend a fair chunk of time during their development flow on fixing relatively trivial problems in their code. pre-commit.ci both enforces that these issues are discovered (which is opt-in for each developer's workflow via pre-commit) but also fixes the issues automatically, letting developers focus their time on more valuable problems.
It is clear that there is a significant benefit in automatically fixing trivial issues that interrupt the software development lifecycle and also in keeping git hooks up to date. While dependabot automatically updates most package dependency files (such as requirements files for Python), it does not currently support updating pre-commit hook versions; thus, this tool covers that important and tedious use case.
List values in pyproject.sort
can grow significantly, particularly with larger projects (see home-assistant). These lists are typically kept in a logicial, alphabetical order for consistency and to ensure ease of reference. This presents a maintenance challenge for the project.
With the following configuration:
[tool.ruff]
ignore = [
"T201",
"G004",
"D203",
"ARG",
"INP001",
"DTZ005",
"ANN",
]
Automation could alphabetically sort the the configuration as follows:
[tool.ruff]
ignore = [
"ANN",
"ARG",
"DTZ005",
"D203",
"G004",
"INP001",
"T201",
]
Package build process is failing due to an invalid absolute import.
pyprojectsort to build and deploy to PyPI.
python -m build
[23](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:24)
* Creating venv isolated environment...
[24](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:25)
* Installing packages in isolated environment... (flit)
[25](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:26)
* Getting build dependencies for sdist...
[26](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:27)
* Installing packages in isolated environment... (tomli-w==1.0.0, tomli==2.0.1)
[27](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:28)
* Building sdist...
[28](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:29)
Traceback (most recent call last):
[29](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:30)
File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
[30](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:31)
main()
[31](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:32)
File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/pyproject_hooks/_in_process/_in_process.py", line 335, in main
[32](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:33)
json_out['return_val'] = hook(**hook_input['kwargs'])
[33](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:34)
File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/pyproject_hooks/_in_process/_in_process.py", line 304, in build_sdist
[34](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:35)
return backend.build_sdist(sdist_directory, config_settings)
[35](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:36)
File "/tmp/build-env-51skd9k7/lib/python3.10/site-packages/flit_core/buildapi.py", line 82, in build_sdist
[36](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:37)
path = SdistBuilder.from_ini_path(pyproj_toml).build(Path(sdist_directory))
[37](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:38)
File "/tmp/build-env-51skd9k7/lib/python3.10/site-packages/flit_core/sdist.py", line 93, in from_ini_path
[38](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:39)
metadata = common.make_metadata(module, ini_info)
[39](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:40)
File "/tmp/build-env-51skd9k7/lib/python3.10/site-packages/flit_core/common.py", line 425, in make_metadata
[40](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:41)
md_dict.update(get_info_from_module(module, ini_info.dynamic_metadata))
[41](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:42)
File "/tmp/build-env-51skd9k7/lib/python3.10/site-packages/flit_core/common.py", line 222, in get_info_from_module
[42](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:43)
docstring, version = get_docstring_and_version_via_import(target)
[43](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:44)
File "/tmp/build-env-51skd9k7/lib/python3.10/site-packages/flit_core/common.py", line 195, in get_docstring_and_version_via_import
[44](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:45)
spec.loader.exec_module(m)
[45](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:46)
File "<frozen importlib._bootstrap_external>", line 883, in exec_module
[46](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:47)
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
[47](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:48)
File "/home/runner/work/pyprojectsort/pyprojectsort/pyprojectsort/__init__.py", line 5, in <module>
[48](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:49)
from .main import reformat_pyproject
[49](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:50)
File "/home/runner/work/pyprojectsort/pyprojectsort/pyprojectsort/main.py", line 12, in <module>
[50](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:51)
from pyprojectsort import __version__
[51](https://github.com/kieran-ryan/pyprojectsort/actions/runs/5467369865/jobs/9953581985#step:5:52)
ModuleNotFoundError: No module named 'pyprojectsort'
Within pyprojectsort's repository, run:
python -m build
Currently, the pyproject.toml is rewritten to whether any reformatting would be applied or not. This is inefficient as unnecessary io write operations performed when there are no reformatting changes, but the package writes the original contents back to the file. As such, it would beneficial to rectify this and first check whether reformatting would occur, and to secondly only write to the file if there are changes to reformat.
To ensure spelling can be searched across a repository as expected and that all text is readable, a spell checker (such as CSpell can be employed; ensuring all words match the checker's dictionary. The alternative of checking all contributions manually is error-prone and effort-intensive.
A spell-checker can be introduced to a pipeline, git hook, or IDE; which in their respective order, facilitate an increasing shift-left in the detection of spelling mistakes.
Now that pyprojectsort provides a --check
command line option (#10), its own pyproject.toml file can be reformatted and thereafter validated within a pipeline without actually performing the reformatting of the file itself - which can affect other jobs if not using a clean repo clone for each task.
This will allow the project to realise the benefit of its own package and to assist in identifying opportunities for enhancement.
When selecting dependencies to use in a project, certain criteria are typically examined to determine suitability, security, and levels of maintenance and reliability. An important aspect in terms of reliability - and 'will this tool work as expected' - is the test coverage: high test coverage indicating well-tested, maintained software and low coverage indicating otherwise.
It is thus important to make the test coverage highly visible and easily accessible. One of the best ways to do this is to output your test coverage to a file or logs and have it be detected and rendered for display in a markdown badge at the top of a project's README file.
For example:
Could additionally upload a JUnit report for test report summary information in merge requests.
The project documentation is rendered through a 'gh-pages' branch using GitHub Pages. The pages deployment job checks whether the GitHub event is a release so it doesn't deploy on every push or PR; however, the GitHub workflow specification that the documentation build job sits in only runs on push and PRs, so the deployment never runs.
Project release documentation is deployed to the 'gh-pages' branch when a release is created.
Project release documentation is not deployed to the 'gh-pages' branch when a release is created.
Create a new project release.
Rather than executing tests against development source code locally, tox can be used to execute tests against package distributions - building them prior to testing - against each supported Python distribution.
This makes it very quick and easy to test builds locally, rather than development code; and can also be executed in the same way in the GitHub workflow - ensuring builds are thoroughly tested locally and through continuous integration with the same builds and test automation tooling.
Overall, this will increase package robustness, testability and development workflow.
An example tox.ini
might appear as follows:
[tox]
env_list =
py37
py38
py39
py310
py311
minversion = 4.6.4
[testenv]
description = run the tests with pytest
package = wheel
wheel_build_env = .pkg
deps =
pytest>=6
commands =
pytest {tty:--color=yes} {posargs}
The project is currently packaged and published in each of the deployment jobs: to PyPI and TestPyPI. This slows down the pipeline as the package build process runs twice. As the project is only built on release - when the package would be deployed - build failures are not detected in the pipeline until a release is created.
To resolve these issues, the build process can be extracted from the deployment tasks into its own individual task, of which other tasks can use its distribution.
The package currently only officially supports Python 3.10, however efforts have been made to ensure backwards compatibility:
tomli
is being used as a dependency and aliased to tomllib
to allow versions prior to 3.11 to have TOML support with a matching interface to that version onwardsfrom __future__ import annotations
is being applied to use type-hints of 3.11 onwards in earlier Python versionsBy configuring a test environment setup that runs and tests the package against Python 3.8-3.11 (3.7 having reached end-of-life on the 27th of June 2023; and 3.12 not due for initial release until the 2nd of October 2023), the package can indicate official support for the full range of actively developed Python distributions which will make adoption easier. Additionally, for the supported versions badge in README.md, it would be advantageous to pull the supported versions directly from PyPI to ensure a source of truth and to automate that information being updated in the badge rather than having to do so manually.
While it is trivial to manually run pyprojectsort from the command line against a pyproject.toml file (e.g. simply pyprojectsort
within the same directory), it is a task developers must remember to do with each committed change to the file and thus is susceptible to follow-on cleanup work where it is missed. To automate this process and ensure every committed change has been formatted with pyprojectsort, a pre-commit
git hook can be implemented.
The below is an example of how the tool might be specified inside a .pre-commit-config.yaml
pre-commit configuration file.
- repo: https://github.com/kieran-ryan/pyprojectsort
rev: v0.1.1
hooks:
- id: pyprojectsort
A CONTRIBUTING.md
file is an important element of every repository. It provides visitors with an overview of how they can set up, work with and contribute to the repository. Writing this documentation, also provides repository owners with a useful opportunity to assess usability issues and to examine the project from a fresh perspective, with a mindset of 'What issues might someone seeing this for the first time encounter?' or 'Is there any way I could make this implementation more explicit and reduce its "unknown unknowns"?'.
This will inform anyone looking to contribute to the project how to act with autonomy and to not be dependant on direct support and answers from the original author.
The inbuilt sorted
command in Python is unable to do dictionary and mixed data type comparisons to determine alphabetical ordering in an array. Matching floats and integer values are also kept in their original order by sorted
(e.g. [5.0, 5]
sorts to [5.0, 5]
and [5, 5.0]
sorts to [5, 5.0]
). A solution is required to handle these cases and to also alphabetically order the contents of the dictionaries, and the order of each of those dictionaries within the array based on their keys and values.
An array of dictionaries and mixed data types are alphabetically sorted.
A TypeError
was raised that '<' not supported between instances of 'dict' and 'dict'
when trying to sort the array containing those dictionaries.
Traceback (most recent call last):
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/bin/pyprojectsort", line 8, in <module>
sys.exit(main())
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 85, in main
reformatted_pyproject: dict = reformat_pyproject(pyproject)
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 58, in reformat_pyproject
return {
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 59, in <dictcomp>
key: reformat_pyproject(value)
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 58, in reformat_pyproject
return {
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 59, in <dictcomp>
key: reformat_pyproject(value)
File "/Users/KieranRyan/projects/python/pyproject-sort/venv/lib/python3.10/site-packages/pyprojectsort/main.py", line 63, in reformat_pyproject
return sorted(reformat_pyproject(item) for item in pyproject)
TypeError: '<' not supported between instances of 'dict' and 'dict'
Create a pyproject.toml file as follows:
[project]
authors = [
{ name = "Kieran Ryan" },
{ name = "Author Name" },
]
Execute pyprojectsort:
pyprojectsort
Material for Mkdocs delivers a clean aesthetic for generating static-site project documentation - adhering to the well-established Material design system.
The project's current documentation is generated through Sphinx, using the furo theme. While this is satisfactory, the markdown-first approach of Material for Mkdocs is desirable - though markdown can be enabled with Sphinx. Also a feature of Sphinx, generating documentation from Python docstrings is also possible with mkdocstrings.
With the limited scope of this project, some of the extensibility and rich ecosystem of Sphinx are unlikely to be necessary, and delivering improved documentation through Material for Mkdocs may be the desired approach.
This work may also offer the opportunity to significantly expand the project's documentation and provide detailed examples of individual formatting changes that are made by the library. The docs could be generated in their own separate pipeline yaml and its status displayed in a README badge.
Due to alphanumerical ordering not respecting all natural numerical patterns when included in strings, higher values can proceed lower values e.g. "10" comes before "7" due to "1" of course coming before "7" in the ASCII character table.
Thus, a solution similar to natsort is required with a more complex algorithm to deal with such edge cases.
[project]
authors = [
{ name = "Kieran Ryan" },
]
classifiers = [
...
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
...
]
[project]
authors = [
{ name = "Kieran Ryan" },
]
classifiers = [
...
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
...
]
Editorconfig configuration files can be placed at the root of a project to instruct tools, IDEs, etc. on the configuration to use - essentially a source of truth.
EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems.
Adding .editorconfig support would enable the tool to read and respect a user's central configuration. Assessment would need to be completed to determine whether this would be an opt-in feature - perhaps by specifying to use it in the pyproject.toml OR otherwise whether to autodetect and use the settings from a present .editorconfig file.
The continuous integration pipeline and static analysis jobs only run against the source code on 'push', not on 'pull_request' (as evidenced by #20); this means linting jobs, etc. will not run against pull requests from forked repositories.
The following configuration:
on: [push]
Must change to:
on: [push, pull_request]
The package must currently be executed in the same working directory as the pyproject.toml
file in order to format it. While this file is typically placed at the root of the project, this may not always be the case. Further, it may be desired to run the application while not within the current working directory containing the configuration file. Thus it would be useful to be able to specify the file path as an optional command line argument - continuing to check the current directory when no argument is supplied:
pyprojectsort ../pyproject.toml
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.