Ruff: Python Linting for Clean and Compliant Code¶
ChatGPT Prompt for Google-Style Docstrings
A fast and easy way to create Google-style docstrings is by using ChatGPT. You can provide a simple prompt to request the creation of the docstring for any Python function.
Example Prompt for a Function¶
Here’s an example prompt you could use with ChatGPT to request a Google-style
docstring following the settings in your .vscode/settings.json
file:
Prompt:
Create a Google-style docstring for the following Python function.
Limit comments to 72 characters per line and code lines to 79 characters.
Function:
def add_numbers(a, b):
result = a + b
return result
Context (optional): This function adds two numbers together.
Generated Docstring¶
Using the prompt above, ChatGPT will generate a docstring similar to this:
def add_numbers(a, b):
"""
Adds two numbers together.
Args:
a (int): The first number.
b (int): The second number.
Returns:
int: The sum of the two numbers.
Raises:
TypeError: If either 'a' or 'b' is not an integer.
"""
result = a + b
return result
Example Prompt for a Class¶
Here’s an example prompt you could use with ChatGPT to request Google-style docstrings for an entire class, including the module-level docstring, class-level docstring, and docstrings for each method:
Prompt:
Create Google-style docstrings for the following Python class.
Limit comments to 72 characters per line and code lines to 79 characters.
Include the module-level docstring at the very top of the script before the imports,
the class-level docstring, and the docstrings for each method.
Class:
class TextProcessor:
def __init__(self, language):
if language not in ['en', 'es', 'fr']:
raise ValueError(f"Unsupported language: {language}")
self.language = language
def clean_text(self, text):
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
def tokenize(self, text):
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
def analyze_sentiment(self, text):
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
Context (optional): This class is used for text processing in NLP tasks.
It includes methods for cleaning text, tokenizing sentences, and analyzing sentiment.
Generated Docstring¶
ChatGPT might respond with:
"""
This module provides text processing utilities for NLP projects.
The utilities include functions for text cleaning, tokenization, and
sentiment analysis.
"""
class TextProcessor:
"""
A class used to perform text processing for NLP tasks.
This class includes methods for cleaning text, tokenizing sentences,
and calculating sentiment scores.
Attributes:
language (str): The language of the text to be processed.
"""
def __init__(self, language):
"""
Initializes the TextProcessor with a specified language.
Args:
language (str): The language of the text to be processed.
Raises:
ValueError: If the provided language is not supported.
"""
if language not in ['en', 'es', 'fr']:
raise ValueError(f"Unsupported language: {language}")
self.language = language
def clean_text(self, text):
"""
Cleans the input text by removing special characters and extra spaces.
Args:
text (str): The text to be cleaned.
Returns:
str: The cleaned text.
Raises:
TypeError: If the input text is not a string.
"""
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
def tokenize(self, text):
"""
Tokenizes the input text into a list of words.
Args:
text (str): The text to be tokenized.
Returns:
list: A list of words (tokens).
Raises:
TypeError: If the input text is not a string.
"""
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
def analyze_sentiment(self, text):
"""
Analyzes the sentiment of the input text.
Args:
text (str): The text to be analyzed.
Returns:
float: The sentiment score of the text.
Raises:
TypeError: If the input text is not a string.
"""
if not isinstance(text, str):
raise TypeError("Input text must be a string")
# Implementation goes here
pass
Running Ruff¶
To check your code with Ruff, run the following command in your terminal:
Ruff is a high-performance linter for Python that swiftly evaluates and enhances the quality of your code. With an emphasis on speed and practicality, Ruff assists developers in maintaining consistent coding styles and standards within Python scripts and Jupyter notebooks alike.
Getting Started with ruff check
¶
The ruff check
command forms the backbone of Ruff, providing a quick
and thorough analysis of your Python files to identify and flag
potential issues.
Standard Linting Procedure¶
To initiate linting, utilize the following command pattern:
For example, to lint a service component within your application, you might use:
Automated Error Correction with --fix
¶
Ruff's auto-fix capability streamlines the error correction process by automatically resolving numerous common linting issues:
For instance, to apply auto-fixes to an entire application directory:
Utilizing Nursery Rules with --preview
¶
To explore beyond standard linting rules and include experimental ones,
use the --preview
flag:
This is particularly useful when linting individual files that may leverage cutting-edge Python features:
Understanding Ruff's Rules¶
An extensive explanation of Ruff's rules, both standard and nursery, is available at the Ruff Rules Documentation.
Example: Linting a Python Script¶
Let's say you have a Python script at
project_directory/services/parser.py
. To lint this script, apply
standard and nursery rules, and fix issues:
Example: Working with Jupyter Notebooks¶
For a Jupyter notebook, analysis.ipynb
, located in the same directory:
Ruff will lint the notebook and provide feedback or fixes in a format compatible with Jupyter's code cells.
Real-World Example and Terminal Output¶
Here's a real-world example showcasing the execution of Ruff and the corresponding terminal output:
# Non-compliant code with E201
def my_function(): # Violates "undocumented-public-function"
my_list = [1, 2, 3, 4 ] # Violates "whitespace-after-open-bracket" and "whitespace-before-close-bracket"
long_string = "This is a very long string..." # Violates "line-too-long"
Assuming you're working on project_directory/models/data_model.py
with
some issues, you run:
The terminal output might look like this:
project_directory/models/data_model.py:1:1: D100 Missing docstring in public module
project_directory/models/data_model.py:5:10: E231 missing whitespace after ','
project_directory/models/data_model.py:7:1: E302 expected 2 blank lines, found 1
Found 5 errors (3 fixed, 2 remaining).
Each line of the output directs you to the specific issue within your code, providing guidance on the nature of the problem and its location.
Quick Code Navigation
In integrated development environments like VS Code, you can use the command-click (or control-click on some systems) feature on the path in the terminal output to jump directly to the problematic line, facilitating quick fixes.
Handling Nursery Rule Warnings
If you encounter a warning like:
This indicates that your pyproject.toml
includes nursery rules, which are experimental and require the --preview
flag during execution. In our configuration, where we utilize a set of 50 rules that encompasses several nursery rules, it's essential to always use the --preview
flag. This ensures that Ruff considers all specified rules, including the experimental ones, to maintain the integrity of your code linting strategy.
Always run Ruff with the --preview
flag to avoid deprecation warnings and adhere to the complete set of rules defined for your project:
By doing so, you'll leverage the full capabilities of Ruff, keeping your codebase up to date with both standard and progressive linting practices.
Advanced Integration of Ruff in Python Development¶
Continuing from the foundational ruff check
usage, let's delve into
how Ruff can be integrated into more advanced development workflows.
This includes setting up pre-commit hooks, utilizing GitHub Actions for
continuous integration, and customizing Ruff to adhere to a specific set
of rules defined in a project's pyproject.toml
.
Pre-commit Hooks for Ruff¶
Pre-commit hooks are a powerful way to ensure that code is automatically linted before it's committed to your repository, helping to maintain code quality and consistency.
Setting Up Pre-commit Hooks¶
To utilize Ruff as a pre-commit hook, you'll need to add a configuration
to your .pre-commit-config.yaml
file:
repos:
- repo: https://github.com/charliermarsh/ruff
rev: '' # Use the tag for the desired release
hooks:
- id: ruff
This configuration will instruct the pre-commit tool to run Ruff against
staged files whenever git commit
is executed.
Example: Pre-commit Hook in Action¶
When you attempt to commit, the hook will run, and you might see output like this if issues are found:
pre-commit running: ruff
project_directory/services/parser.py:10:1: E302 expected 2 blank lines, found 1
Failed. Ruff found linting errors. Review the issues and commit again.
GitHub Actions for Continuous Linting¶
GitHub Actions can be set up to run Ruff on every push or pull request to ensure that all contributions meet your coding standards.
GitHub Actions Integration with Ruff¶
To ensure the codebase maintains high-quality standards and adheres to our defined linting rules, we use GitHub Actions to automate the linting process. This section provides an up-to-date guide on how Ruff is integrated within our CI/CD pipeline.
GitHub Actions Workflow for Ruff Linting¶
Our GitHub Actions workflow is configured to run Ruff on every push and pull request. The action checks the code against a set of 50 rules, including several nursery rules, ensuring all contributions are up to the linting standards before merging into the main branch.
Workflow Setup¶
The workflow is defined in the .github/workflows/ruff.yaml
file and
consists of the following steps:
- Checkout Code: Retrieves the code from the current repository branch that triggered the action.
- Set up Python: Prepares the Python environment using the version specified, ensuring compatibility with Ruff.
- Install Ruff: Installs Ruff, making it available for linting.
- Check Code with Ruff: Runs Ruff against the specified directories
(
src/
andnotebooks/
) using the--preview
flag to include nursery rules from ourpyproject.toml
. - Fail if Linting Errors Are Detected: If Ruff detects any linting errors, the action outputs an error message and fails, prompting the user to lint their code locally and resolve the issues before pushing to the repository.
Ruff Linting Action Definition¶
Here is the code snippet for the GitHub Actions workflow:
name: Ruff Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Ruff
run: pip install ruff
- name: Check code with Ruff
run: ruff check --preview src/ notebooks/
- name: Fail if linting errors are detected
run: |
if ruff check --preview src/ notebooks/
--quiet | grep -q 'error'; then
echo "Linting errors detected. Please lint your code locally with Ruff and resolve all issues before pushing."
exit 1
else
echo "No linting errors detected."
fi
With this setup, we do not automatically fix any issues with Ruff in the GitHub Actions workflow. Instead, we enforce that the code must be linted locally by the developer. This approach ensures that all team members are actively involved in maintaining the code quality and are aware of the coding standards enforced by our Ruff configuration.
Branch Protection with Ruff
To ensure code quality, we need to protect the main branch with branch protection rules. This setup requires that all code must pass Ruff linting checks before being merged into the main branch. While you can push code to your feature branches even if it fails linting, merging into main is blocked until all linting errors are resolved. This approach mimics pre-commit hooks, maintaining a clean and compliant main branch. Developers should run Ruff locally and fix issues before pushing code for review.
Example: GitHub Action Log for Linting Errors¶
When a developer pushes code or creates a pull request, the GitHub Action is triggered to ensure the code meets our linting standards. If Ruff detects linting issues, the action will fail, and the output in the GitHub Actions log will resemble the following:
Run ruff check --preview src/ notebooks/
Linting errors detected. Please lint your code locally with Ruff and resolve all issues before pushing.
project_directory/models/data_model.py:1:1: D100 Missing docstring in public module
project_directory/services/parser.py:5:10: E231 missing whitespace after ','
project_directory/utils/calculations.py:7:1: E302 expected 2 blank lines, found 1
...
Error: Process completed with exit code 1.
This log provides detailed feedback about the linting errors found. The
developer is expected to address these issues by running Ruff locally
with the --preview
flag, ensuring that the code is compliant with our
established set of rules, including nursery rules, before attempting to
push again.
Customizing Ruff with pyproject.toml
¶
Ruff allows for extensive customization of its linting rules through a
pyproject.toml
file. This enables you to specify which rules to
enable, disable, or configure further.
Commentary on pyproject.toml
Configuration¶
# pyproject.toml
[tool.ruff]
# We're using a curated set of 50 rules, which you can review and adjust as needed
include = [ ... ]
exclude = [ ... ]
The include
and exclude
keys allow you to precisely control which
rules Ruff will apply or ignore. This ensures that the linter is aligned
with your project's standards and practices.
Conclusion¶
By integrating Ruff with pre-commit hooks and GitHub Actions, and
customizing its behavior with pyproject.toml
, you can automate and
fine-tune the linting process to fit your project's needs. This ensures
high-quality, clean code that adheres to your defined standards,
ultimately leading to a more robust and maintainable codebase.
Remember, the set of rules within pyproject.toml
is at the core of
Ruff's operation within your project, and you should review and adapt
these rules to align with your coding principles.
Further Resources¶
For more detailed information on using Ruff, customizing rules, and integrating with your development environment, please refer to the following:
Leverage these resources to fully harness the capabilities of Ruff for your Python projects.
Selected rules:
Code | Rule Description | Documentation Link |
---|---|---|
F401 | Unused imports | F401 |
F402 | Import shadowed by loop var | F402 |
F403 | from module import * used |
F403 |
F405 | Name may be undefined, or defined from star imports | F405 |
F601 | Dictionary key literal repeated | F601 |
F602 | Dictionary key variable repeated | F602 |
F621 | Too many expressions in star-unpacking assignment | F621 |
F631 | Assert test is a non-empty tuple | F631 |
F632 | Use == to compare constant literals |
F632 |
F701 | break outside loop |
F701 |
F702 | continue not properly in loop |
F702 |
F704 | Yield statement outside of a function | F704 |
F706 | return statement outside of a function/method |
F706 |
F707 | except block not the last exception handler |
F707 |
F722 | Syntax error in forward annotation | F722 |
F811 | Redefinition of unused variable from line | F811 |
F821 | Undefined name | F821 |
F841 | Local variable is assigned to but never used | F841 |
E101 | Indentation contains mixed spaces and tabs | E101 |
E111 | Indentation is not a multiple of four | E111 |
E112 | Expected an indented block | E112 |
E113 | Unexpected indentation | E113 |
E114 | Indentation is not a multiple of four (comment) | E114 |
E115 | Expected an indented block (comment) | E115 |
E116 | Unexpected indentation (comment) | E116 |
E117 | Over-indented (comment) | E117 |
E201 | Whitespace after '(' or '[' | E201 |
E202 | Whitespace before ')' or ']' | E202 |
E203 | Whitespace before ':' | E203 |
E211 | Whitespace before '(' or '[' | E211 |
E225 | Missing whitespace around operator | E225 |
E231 | Missing whitespace after ',', ';', or ':' | E231 |
E251 | Unexpected spaces around keyword / parameter equals | E251 |
E261 | At least two spaces before inline comment | E261 |
E262 | Inline comment should start with '# ' | E262 |
E265 | Block comment should start with '# ' | E265 |
E266 | Too many leading '#' for block comment | E266 |
E271 | Multiple spaces after keyword | E271 |
E272 | Multiple spaces before keyword | E272 |
E273 | Tab after keyword | E273 |
E274 | Tab before keyword | E274 |
E275 | Missing whitespace after keyword | E275 |
E401 | Multiple imports on one line | E401 |
E402 | Module level import not at top of file | E402 |
E501 | Line too long | E501 |
E711 | Comparison to None should be 'expr is None' | E711 |
E712 | Comparison to True should be 'if cond is True:' or 'if cond:' | E712 |
E713 | Test for membership should be 'not in' | E713 |
E714 | Test for object identity should be 'is not' | E714 |
E721 | Do not compare types, use 'isinstance()' | E721 |
E722 | Do not use bare 'except' | E722 |
E731 | Do not assign a lambda expression, use a def | E731 |
I001 | Import block is un-sorted or un-formatted | I001 |
I002 | Missing required import | I002 |
N801 | Class name should use CapWords convention | N801 |
N802 | Function name should be lowercase | N802 |
N803 | Argument name should be lowercase | N803 |
N804 | First argument of a class method should be named 'cls' | N804 |
N805 | First argument of a method should be named 'self' | N805 |
N806 | Variable in function should be lowercase | N806 |
D100 | Missing docstring in public module | D100 |
D101 | Missing docstring in public class | D101 |
D102 | Missing docstring in public method | D102 |
D103 | Missing docstring in public function | D103 |
D104 | Missing docstring in public package | D104 |
D105 | Missing docstring in magic method | D105 |
D106 | Missing docstring in public nested class | D106 |
D107 | Missing docstring in init | D107 |