Skip to content

conftest

conftest ¤

Pytest plugin to validate docstring code examples.

Code examples are often wrapped in triple backticks (```) within our docstrings. This plugin extracts these code examples and validates them using pylint.

Classes¤

conftest.CustomPythonCodeBlockParser ¤

Bases: CodeBlockParser

Code block parser that validates extracted code examples using pylint.

This parser is a modified version of the default Python code block parser from the Sybil library. It uses pylint to validate the extracted code examples.

All code examples are preceded by the original file's import statements as well as an wildcard import of the file itself. This allows us to use the code examples as if they were part of the original file.

Additionally, the code example is padded with empty lines to make sure the line numbers are correct.

Pylint warnings which are unimportant for code examples are disabled.

Source code in site-packages/conftest.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
class CustomPythonCodeBlockParser(CodeBlockParser):
    """Code block parser that validates extracted code examples using pylint.

    This parser is a modified version of the default Python code block parser
    from the Sybil library.
    It uses pylint to validate the extracted code examples.

    All code examples are preceded by the original file's import statements as
    well as an wildcard import of the file itself.
    This allows us to use the code examples as if they were part of the original
    file.

    Additionally, the code example is padded with empty lines to make sure the
    line numbers are correct.

    Pylint warnings which are unimportant for code examples are disabled.
    """

    def __init__(self):
        """Initialize the parser."""
        super().__init__("python")

    def evaluate(self, example: Example) -> None | str:
        """Validate the extracted code example using pylint.

        Args:
            example: The extracted code example.

        Returns:
            None if the code example is valid, otherwise the pylint output.
        """
        # Get the import statements for the original file
        import_header = get_import_statements(example.document.text)
        # Add a wildcard import of the original file
        import_header.append(
            path_to_import_statement(Path(os.path.relpath(example.path)))
        )
        imports_code = "\n".join(import_header)

        # Dedent the code example
        # There is also example.parsed that is already prepared, but it has
        # empty lines stripped and thus fucks up the line numbers.
        example_code = textwrap.dedent(
            example.document.text[example.start : example.end]
        )
        # Remove first line (the line with the triple backticks)
        example_code = example_code[example_code.find("\n") + 1 :]

        example_with_imports = FORMAT_STRING.format(
            disable_pylint=PYLINT_DISABLE_COMMENT.format("disable"),
            imports=imports_code,
            enable_pylint=PYLINT_DISABLE_COMMENT.format("enable"),
            code=example_code,
        )

        # Make sure the line numbers are correct
        source = pad(
            example_with_imports,
            example.line - imports_code.count("\n") - FORMAT_STRING.count("\n"),
        )

        # pylint disable parameters
        pylint_disable_params = [
            "missing-module-docstring",
            "missing-class-docstring",
            "missing-function-docstring",
            "reimported",
            "unused-variable",
            "no-name-in-module",
            "await-outside-async",
        ]

        response = validate_with_pylint(source, example.path, pylint_disable_params)

        if len(response) > 0:
            response_concats = "\n".join(response)
            return (
                f"Pylint validation failed for code example:\n"
                f"{example_with_imports}\nOutput: {response_concats}"
            )

        return None
Functions¤
__init__() ¤

Initialize the parser.

Source code in site-packages/conftest.py
106
107
108
def __init__(self):
    """Initialize the parser."""
    super().__init__("python")
evaluate(example) ¤

Validate the extracted code example using pylint.

PARAMETER DESCRIPTION
example

The extracted code example.

TYPE: Example

RETURNS DESCRIPTION
None | str

None if the code example is valid, otherwise the pylint output.

Source code in site-packages/conftest.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def evaluate(self, example: Example) -> None | str:
    """Validate the extracted code example using pylint.

    Args:
        example: The extracted code example.

    Returns:
        None if the code example is valid, otherwise the pylint output.
    """
    # Get the import statements for the original file
    import_header = get_import_statements(example.document.text)
    # Add a wildcard import of the original file
    import_header.append(
        path_to_import_statement(Path(os.path.relpath(example.path)))
    )
    imports_code = "\n".join(import_header)

    # Dedent the code example
    # There is also example.parsed that is already prepared, but it has
    # empty lines stripped and thus fucks up the line numbers.
    example_code = textwrap.dedent(
        example.document.text[example.start : example.end]
    )
    # Remove first line (the line with the triple backticks)
    example_code = example_code[example_code.find("\n") + 1 :]

    example_with_imports = FORMAT_STRING.format(
        disable_pylint=PYLINT_DISABLE_COMMENT.format("disable"),
        imports=imports_code,
        enable_pylint=PYLINT_DISABLE_COMMENT.format("enable"),
        code=example_code,
    )

    # Make sure the line numbers are correct
    source = pad(
        example_with_imports,
        example.line - imports_code.count("\n") - FORMAT_STRING.count("\n"),
    )

    # pylint disable parameters
    pylint_disable_params = [
        "missing-module-docstring",
        "missing-class-docstring",
        "missing-function-docstring",
        "reimported",
        "unused-variable",
        "no-name-in-module",
        "await-outside-async",
    ]

    response = validate_with_pylint(source, example.path, pylint_disable_params)

    if len(response) > 0:
        response_concats = "\n".join(response)
        return (
            f"Pylint validation failed for code example:\n"
            f"{example_with_imports}\nOutput: {response_concats}"
        )

    return None

Functions¤

conftest.get_import_statements(code) ¤

Get all import statements from a given code string.

PARAMETER DESCRIPTION
code

The code to extract import statements from.

TYPE: str

RETURNS DESCRIPTION
list[str]

A list of import statements.

Source code in site-packages/conftest.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def get_import_statements(code: str) -> list[str]:
    """Get all import statements from a given code string.

    Args:
        code: The code to extract import statements from.

    Returns:
        A list of import statements.
    """
    tree = ast.parse(code)
    import_statements = []

    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            import_statement = ast.get_source_segment(code, node)
            import_statements.append(import_statement)

    return import_statements

conftest.path_to_import_statement(path) ¤

Convert a path to a Python file to an import statement.

PARAMETER DESCRIPTION
path

The path to convert.

TYPE: Path

RETURNS DESCRIPTION
str

The import statement.

RAISES DESCRIPTION
ValueError

If the path does not point to a Python file.

Source code in site-packages/conftest.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def path_to_import_statement(path: Path) -> str:
    """Convert a path to a Python file to an import statement.

    Args:
        path: The path to convert.

    Returns:
        The import statement.

    Raises:
        ValueError: If the path does not point to a Python file.
    """
    # Make the path relative to the present working directory
    if path.is_absolute():
        path = path.relative_to(Path.cwd())

    # Check if the path is a Python file
    if path.suffix != ".py":
        raise ValueError("Path must point to a Python file (.py)")

    # Remove 'src' prefix if present
    parts = path.parts
    if parts[0] == "src":
        parts = parts[1:]

    # Remove the '.py' extension and join parts with '.'
    module_path = ".".join(parts)[:-3]

    # Create the import statement
    import_statement = f"from {module_path} import *"
    return import_statement

conftest.validate_with_pylint(code_example, path, disable_params) ¤

Validate a code example using pylint.

PARAMETER DESCRIPTION
code_example

The code example to validate.

TYPE: str

path

The path to the original file.

TYPE: str

disable_params

The pylint disable parameters.

TYPE: list[str]

RETURNS DESCRIPTION
list[str]

A list of pylint messages.

Source code in site-packages/conftest.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def validate_with_pylint(
    code_example: str, path: str, disable_params: list[str]
) -> list[str]:
    """Validate a code example using pylint.

    Args:
        code_example: The code example to validate.
        path: The path to the original file.
        disable_params: The pylint disable parameters.

    Returns:
        A list of pylint messages.
    """
    try:
        pylint_command = [
            "pylint",
            "--disable",
            ",".join(disable_params),
            "--from-stdin",
            path,
        ]

        subprocess.run(
            pylint_command,
            input=code_example,
            text=True,
            capture_output=True,
            check=True,
        )
    except subprocess.CalledProcessError as exception:
        return exception.output.splitlines()

    return []