From a3fb19fd0a3f912d4f57b8b034dfbfa7b777b2c7 Mon Sep 17 00:00:00 2001 From: Alexander Holas <70367168+AlexHls@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:52:38 +0100 Subject: [PATCH] Add ruff rules and docs (#2478) * Add ruff rules and docs * Update pyproject.toml Co-authored-by: Wolfgang Kerzendorf * rebuild docs --------- Co-authored-by: Wolfgang Kerzendorf --- .ruff.toml | 230 ++++++++++++++++++ .../contributing/development/code_quality.rst | 17 ++ pyproject.toml | 80 ++++++ 3 files changed, 327 insertions(+) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000000..0a6e49c83ae --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,230 @@ +# Ruff configuration file adapted from astropy +extend = "pyproject.toml" +lint.ignore = [ + # NOTE: to find a good code to fix, run: + # ruff --select="ALL" --statistics astropy/ + + # flake8-annotations (ANN) : static typing + "ANN001", # Function argument without type annotation + "ANN003", # `**kwargs` without type annotation + "ANN201", # Public function without return type annotation + "ANN202", # Private function without return type annotation + "ANN401", # Use of `Any` type + + # flake8-unused-arguments (ARG) + "ARG001", # unused-function-argument + "ARG002", # unused-method-argument + "ARG003", # unused-class-method-argument + "ARG004", # unused-static-method-argument + "ARG005", # unused-lambda-argument + + # flake8-bugbear (B) + "B006", # MutableArgumentDefault + "B007", # UnusedLoopControlVariable + "B023", # FunctionUsesLoopVariable + "B028", # No-explicit-stacklevel + "B904", # RaiseWithoutFromInsideExcept + "B905", # ZipWithoutExplicitStrict # requires 3.10+ + + # flake8-blind-except (BLE) + "BLE001", # blind-except + + # mccabe (C90) : code complexity + # TODO: configure maximum allowed complexity. + "C901", # McCabeComplexity + + # pydocstyle (D) + # Missing Docstrings + "D100", # undocumented-public-module + "D101", # undocumented-public-class + "D103", # undocumented-public-function + "D104", # undocumented-public-package + "D205", # blank-line-after-summary + # Quotes Issues + "D300", # triple-single-quotes + "D301", # escape-sequence-in-docstring + # Docstring Content Issues + "D403", # first-line-capitalized + "D404", # docstring-starts-with-this + "D401", # non-imperative-mood. + "D414", # empty-docstring-section + "D419", # docstring is empty + + # flake8-datetimez (DTZ) + "DTZ001", # call-datetime-without-tzinfo + "DTZ005", # call-datetime-now-without-tzinfo + + # pycodestyle (E, W) + "E501", # line-too-long + "E721", # type-comparison + "E731", # lambda-assignment + + # flake8-errmsg (EM) : nicer error tracebacks + "EM101", # raw-string-in-exception + "EM102", # f-string-in-exception + "EM103", # dot-format-in-exception + + # eradicate (ERA) + # NOTE: be careful that developer notes are kept. + "ERA001", # commented-out-code + + # flake8-executable (EXE) + "EXE002", # shebang-missing-executable-file + + # Pyflakes (F) + "F841", # unused-variable + + # flake8-boolean-trap (FBT) : boolean flags should be kwargs, not args + # NOTE: a good thing to fix, but changes API. + "FBT001", # boolean-positional-arg-in-function-definition + "FBT002", # boolean-default-value-in-function-definition + "FBT003", # boolean-positional-value-in-function-call + + # flake8-fixme (FIX) + "FIX001", # Line contains FIXME. this should be fixed or at least FIXME replaced with TODO + "FIX004", # Line contains HACK. replace HACK with NOTE. + + # flake8-import-conventions (ICN) : use conventional import aliases + "ICN001", # import-conventions + + # pep8-naming (N) + # NOTE: some of these can/should be fixed, but this changes the API. + "N801", # invalid-class-name + "N802", # invalid-function-name + "N803", # invalid-argument-name + "N804", # invalid-first-argument-name-for-class-method + "N805", # invalid-first-argument-name-for-method + "N807", # dunder-function-name + "N813", # camelcase-imported-as-lowercase + "N815", # mixed-case-variable-in-class-scope + "N816", # mixed-case-variable-in-global-scope + "N818", # error-suffix-on-exception-name + + # NumPy-specific rules (NPY) + "NPY002", # Replace legacy `np.random.rand` call with `np.random.Generator` (2023-05-03) + + # Perflint (PERF) + "PERF203", # `try`-`except` within a loop incurs performance overhead + "PERF401", # Use a list comprehension to create a transformed list + + # pygrep-hooks (PGH) + "PGH001", # eval + + # Pylint (PLC, PLE, PLR, PLW) + "PLE0101", # return-in-init + "PLR0124", # Name compared with itself + "PLR0402", # ConsiderUsingFromImport + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-args + "PLR0915", # too-many-statements + "PLR1714", # Consider merging multiple comparisons + "PLR2004", # MagicValueComparison + "PLR5501", # collapsible-else-if + "PLW0603", # global-statement + "PLW2901", # redefined-loop-name + + # flake8-pytest-style (PT) + "PT001", # pytest-fixture-incorrect-parentheses-style + "PT003", # pytest-extraneous-scope-function + "PT004", # pytest-missing-fixture-name-underscore + "PT006", # pytest-parametrize-names-wrong-type + "PT007", # pytest-parametrize-values-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT017", # pytest-assert-in-exceptinstead + "PT018", # pytest-composite-assertion + "PT022", # pytest-useless-yield-fixture + "PT023", # pytest-incorrect-mark-parentheses-style + + # flake8-use-pathlib (PTH) + "PTH100", # os-path-abspath + "PTH101", # os-chmod + "PTH102", # os-mkdir + "PTH103", # os-makedirs + "PTH104", # os-rename + "PTH107", # os-remove + "PTH108", # os-unlink + "PTH109", # os-getcwd + "PTH110", # os-path-exists + "PTH111", # os-path-expanduser + "PTH112", # os-path-isdir + "PTH113", # os-path-isfile + "PTH118", # os-path-join + "PTH119", # os-path-basename + "PTH120", # os-path-dirname + "PTH122", # os-path-splitext + "PTH123", # builtin-open + "PTH202", # `os.path.getsize` should be replaced by `Path.stat().st_size` + "PTH207", # Replace `glob` with `Path.glob` or `Path.rglob` + + # flake8-return (RET) + "RET501", # unnecessary-return-none + "RET502", # implicit-return-value + "RET503", # implicit-return + "RET504", # unnecessary-assign + "RET505", # superfluous-else-return + "RET506", # superfluous-else-raise + "RET507", # superfluous-else-continue + + # flake8-raise (RSE) + "RSE102", # unnecessary-paren-on-raise-exception + + # Ruff-specific rules (RUF) + "RUF001", # ambiguous-unicode-character-string + "RUF002", # ambiguous-unicode-character-docstring + "RUF010", # use conversion in f-string + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + + # flake8-bandit (S) + "S101", # Use of `assert` detected + "S105", # hardcoded-password-string + "S110", # try-except-pass + "S112", # try-except-continue + "S301", # suspicious-pickle-usage + "S307", # Use of possibly insecure function; consider using `ast.literal_eval` + "S311", # suspicious-non-cryptographic-randomness + "S324", # hashlib-insecure-hash-function + "S506", # UnsafeYAMLLoad + "S310", # Suspicious-url-open-usage + "S603", # `subprocess` call: check for execution of untrusted input + "S607", # Starting a process with a partial executable path + + # flake8-simplify (SIM) + "SIM102", # NestedIfStatements + "SIM105", # UseContextlibSuppress + "SIM108", # UseTernaryOperator + "SIM114", # if-with-same-arms + "SIM115", # OpenFileWithContextHandler + "SIM117", # MultipleWithStatements + "SIM118", # KeyInDict + "SIM201", # NegateEqualOp + "SIM300", # yoda condition + + # flake8-print (T20) + "T201", # PrintUsed + + # flake8-todos (TD) + "TD001", # Invalid TODO tag + "TD003", # Missing issue link on the line following this TODO + "TD004", # Missing colon in TODO + "TD007", # Missing space after colon in TODO + + # tryceratops (TRY) + "TRY002", # raise-vanilla-class + "TRY003", # raise-vanilla-args + "TRY004", # prefer-type-error + "TRY200", # reraise-no-cause + "TRY201", # verbose-raise + "TRY300", # Consider `else` block + "TRY301", # raise-within-try + + # flake8-quotes (Q) + "Q000", # use double quotes +] +lint.unfixable = [ + "E711" # NoneComparison. Hard to fix b/c numpy has it's own None. +] + +[lint.extend-per-file-ignores] +"docs/*" = [] diff --git a/docs/contributing/development/code_quality.rst b/docs/contributing/development/code_quality.rst index cd5ae10bc6d..71f8196315c 100644 --- a/docs/contributing/development/code_quality.rst +++ b/docs/contributing/development/code_quality.rst @@ -26,6 +26,23 @@ A better method is to run Black automatically - first `integrate it within the c .. warning :: If your code doesn't follow the Black code style, then the Black-check action on your PR will fail. +Ruff +---- +`Ruff `_ is a code linter and formatter that checks for common mistakes and automatically fixes them. It is currently not installed in the TARDIS conda environment, so you will have to install it manually: :: + + conda install -c conda-forge ruff + +To run Ruff, use the following command: :: + + ruff check # Lints the code + ruff check --fix # Lints and fixes any fixable errors + +Currently, Ruff is not integrated with the TARDIS CI and is not a requirement for merging a PR. However, it is recommended to run Ruff on your code before commiting it to ensure that new code already follows these rules. + +.. note :: We adopt the linting rules utilized by astropy. Permanent rules are defined in the ``pyproject.toml``, non-permanent rules are defined in the ``.ruff.toml`` file. If you want to add a new rule, please add it to the ``.ruff.toml`` file. If you want to add a permanent rule, please open a PR to the ``pyproject.toml``. + +.. note :: Ruff can also be used for formatting code, but for now we recommend using Black for this purpose as the CI is configured to run Black on all PRs. + Naming Conventions ------------------ diff --git a/pyproject.toml b/pyproject.toml index aa87db29650..0c71b4acb60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,3 +48,83 @@ omit-covered-files = false quiet = false verbose = 0 #whitelist-regex = [] + +[tool.ruff] +lint.select = ["ALL"] +exclude=[] +lint.ignore = [ # NOTE: non-permanent exclusions should be added to `.ruff.toml` instead. + + # flake8-builtins (A) : shadowing a Python built-in. + # New ones should be avoided and is up to maintainers to enforce. + "A00", + + # flake8-annotations (ANN) + "ANN101", # No annotation for `self`. + "ANN102", # No annotation for `cls`. + + # flake8-bugbear (B) + "B008", # FunctionCallArgumentDefault + + # flake8-commas (COM) + "COM812", # TrailingCommaMissing + "COM819", # TrailingCommaProhibited + + # pydocstyle (D) + # Missing Docstrings + "D102", # Missing docstring in public method. Don't check b/c docstring inheritance. + "D105", # Missing docstring in magic method. Don't check b/c class docstring. + # Whitespace Issues + "D200", # FitsOnOneLine + # Docstring Content Issues + "D410", # BlankLineAfterSection. Using D412 instead. + "D400", # EndsInPeriod. NOTE: might want to revisit this. + + # pycodestyle (E, W) + "E711", # NoneComparison (see unfixable) + "E741", # AmbiguousVariableName. Physics variables are often poor code variables + + # flake8-fixme (FIX) + "FIX002", # Line contains TODO | notes for improvements are OK iff the code works + + # pep8-naming (N) + "N803", # invalid-argument-name. Physics variables are often poor code variables + "N806", # non-lowercase-variable-in-function. Physics variables are often poor code variables + + # pandas-vet (PD) + "PD", + + # flake8-self (SLF) + "SLF001", # private member access + + # flake8-todos (TD) + "TD002", # Missing author in TODO + + # Ruff-specific rules (RUF) + "RUF005", # unpack-instead-of-concatenating-to-collection-literal -- it's not clearly faster. +] + +[tool.ruff.lint.extend-per-file-ignores] +"setup.py" = ["INP001"] # Part of configuration, not a package. +".github/workflows/*.py" = ["INP001"] +"test_*.py" = [ + "B018", # UselessExpression + "D", # pydocstyle + "E402", # Module level import not at top of file + "PGH001", # No builtin eval() allowed + "S101", # Use of assert detected +] +".pyinstaller/*.py" = ["INP001"] # Not a package. +"conftest.py" = ["INP001"] # Part of configuration, not a package. +"docs/*.py" = [ + "INP001", # implicit-namespace-package. The examples are not a package. +] + +[tool.ruff.lint.flake8-annotations] +ignore-fully-untyped = true +mypy-init-return = true + +[tool.ruff.lint.isort] +known-first-party = ["tardis", "carsus", "stardis"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy"