imr-framework/pypulseq

Define common code style (Linting/Formatting)

Closed this issue ยท 0 comments

I am very happy to see that there are currently many people actively contributing to PyPulseq and even more happy that many people are motivated to help with our bigger code refactoring project. ๐Ÿ‘

However, currently we neither enforce nor specify a specific code style, which first of all may lead to style inconsistencies, which makes it harder for new contributors to choose the "correct" style for their code contributions, but it may also lead to unwanted code changes if people with different code formatters (like autopep8, black, ruff...) work on the same files. I usually try to commit relevant changes only, but just yesterday it happened to me as well that I commited changes my formatter did.

So this Issue is meant to discuss possible code styles and more important potential ways to enforce these. For several other projects like MRPro or BMCTool we use a combination of MyPy for type checking and RUFF as linter and code formatter.

To enforce the defined code style and type checking BEFORE each commit, I really like the tool pre-commit. This can be installed locally and runs all defined checks (hooks) before beeing able to commit something. However, it's still possible to commit work-in-progress code without running the local checks using a --no-verify command.

This is the current configuration we usually use for MyPy / RUFF / pre-commit:

Relevant settings in pyproject.toml

[tool.mypy]
warn_return_any = false
check_untyped_defs = true
warn_no_return = true
warn_unreachable = true
exclude = ["docs"]
enable_error_code = ["ignore-without-code"]
warn_unused_ignores = true


[[tool.mypy.overrides]]
module = [
    "scipy.*",
]
ignore_missing_imports = true

[tool.ruff]
line-length = 120
extend-exclude = ["__init__.py"]
exclude = ["docs/**"]

[tool.ruff.lint]
select = [
    "A",   # flake8-builtins
    "ARG", # flake8-unused-arguments
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "COM", # flake8-commas
    "D",   # pydocstyle
    "E",   # pycodestyle errors
    "F",   # Pyflakes
    "FA",  # flake8-future-annotations
    "I",   # isort
    "N",   # pep8-naming
    "NPY", # NumPy-specific rules
    "RUF", # Ruff-specific rules
    "S",   # flake8-bandit
    "SIM", # flake8-simplify
    "UP",  # pyupgrade
    "PIE", # flake8-pie
    # "PL",  # PyLint
    "PTH", # flake8-use-pathlib
    "T20", # flake8-print
    "Q",   # flake8-quotes
    "W",   # pycodestyle warnings
    "YTT", # flake8-2020
    "ERA", # flake8-eradicate
]
extend-select = [
    "ANN001", #  type annotation for function argument
    "ANN201", #  return type annonation public function
    "ANN205", #  return type annonation static method
    "ANN401", #  any type annotation
    "BLE001", #  blind exception
    "D107",   #  missing docstring in __init__
    "D417",   #  undocumented-parameter
]
ignore = [
    "N999",   #  invalid module name
    "COM812", #  missing-trailing-comma (conflict with formatter)
    "SIM108", #  if-else-block-instead-of-if-exp
]

[tool.ruff.lint.isort]
force-single-line = false
split-on-trailing-comma = false

[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "single"

[tool.ruff.lint.pydocstyle]
convention = "numpy"

[tool.ruff.format]
quote-style = "single"
skip-magic-trailing-comma = false

[tool.typos.default]
locale = "en-us"

Full pre-commit config:

default_language_version:
  python: python3

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-docstring-first
      - id: check-merge-conflict
      - id: check-yaml
      - id: check-toml
      - id: check-json
        exclude: ^.vscode/
      - id: mixed-line-ending

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.7
    hooks:
      - id: ruff # linter
        args: [--fix]
      - id: ruff-format # formatter

  - repo: https://github.com/crate-ci/typos
    rev: v1.21.0
    hooks:
      - id: typos

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        pass_filenames: false
        always_run: true
        args: [src, tests, examples]
        additional_dependencies:
          - numpy
          - matplotlib
          - pytest
          - ...

These settings are kind of strict (which might mean you have to adjust your code before beeing able to commit something), but my experience is that this really pays off on the long run.

Of course we could remove some rules or at least disable them in the beginning, but a lot of things will be done by the ruff formatter automatically anyway.

I am happy to hear your opinions.