Documenting Python Code and Projects

Зачем вам нужно документировать свой код на Python? Что должна включать ваша проектная документация? Как вы пишете и генерируете документацию?

Документация является важной частью разработки программного обеспечения. Без надлежащей документации внутренним и внешним заинтересованным сторонам может быть очень сложно или даже невозможно использовать и/или поддерживать ваш код. Это также значительно затрудняет привлечение новых разработчиков. Если сделать еще один шаг вперед, то без культуры документирования и обучения в целом вы будете часто совершать одни и те же ошибки снова и снова. К сожалению, многие разработчики относятся к документации как к чему-то второстепенному - к чему-то, что можно посыпать, например, черным перцем, без особого внимания.

В этой статье рассматривается, почему вы должны документировать свой код на Python и как это сделать.

Полное руководство по Python:

  1. Современные среды Python - управление зависимостями и рабочим пространством
  2. Тестирование на Python
  3. Современная разработка на основе тестирования на Python
  4. Качество кода на Python
  5. Проверка типа Python
  6. Документирование кода и проектов на Python (эта статья!)
  7. Рабочий процесс проекта на Python

Содержимое

Комментарии против документации

В чем разница между комментариями к коду и документацией?

Документация - это отдельный ресурс, который помогает другим пользователям использовать ваш API, пакет, библиотеку или фреймворк без необходимости читать исходный код. С другой стороны, комментарии предназначены для разработчиков, которые читают ваш исходный код. Документация - это то, что должно присутствовать всегда, но этого нельзя сказать о комментариях. Их наличие приятно, но не обязательно. Документация должна указывать другим пользователям как и когда что-либо использовать, а комментарии должны отвечать на почему вопросы:

  1. Почему это делается именно так?
  2. Почему это здесь, а не там?

На какие вопросы затем должен ответить ваш чистый код:

  1. Что это?
  2. Что делает этот метод?
Type Answers Stakeholder
Documentation When and How Users
Code Comments Why Developers
Clean Code What Developers

Строки документации

Как указано в PEP-257, строка документации Python (или docstring) - это специальный "строковый литерал", который встречается как первая инструкция в определении модуля, функции, класса или метода" для формирования атрибута __doc__ данного объекта. Это позволяет вам встраивать документацию непосредственно в ваш исходный код.

Например, предположим, что у вас есть модуль с именем temperature.py с единственной функцией, которая вычисляет среднесуточные температуры. Используя docstrings, вы можете задокументировать это следующим образом:

"""
The temperature module: Manipulate your temperature easily

Easily calculate daily average temperature
"""

from typing import List


class HighTemperature:
    """Class representing very high temperatures"""

    def __init__(self, value: float):
        """
        :param value: value of temperature
        """

        self.value = value


def daily_average(temperatures: List[float]) -> float:
    """
    Get average daily temperature

    Calculate average temperature from multiple measurements

    :param temperatures: list of temperatures
    :return: average temperature
    """

    return sum(temperatures)/len(temperatures)

Вы можете просмотреть строки документации, указанные для функции daily_average, обратившись к ее атрибуту __doc__:

>>> from temperature import daily_average
>>>
>>> print(daily_average.__doc__)

    Get average daily temperature

    :param temperatures: list of temperatures
    :return: average temperature

Вы также можете просмотреть всю документацию на уровне модуля, используя встроенную функцию справка:

>>> import temperature
>>>
>>> help(temperature)

Стоит отметить, что вы можете использовать функцию help со встроенными ключевыми словами (int, float, def и так далее), классами, функциями и модулями.

Однострочный против многострочного

Строки документации могут быть однострочными или многострочными. В любом случае первая строка всегда рассматривается как краткое изложение. Строка сводки может использоваться инструментами автоматической индексации, поэтому важно, чтобы она помещалась в одной строке. При использовании однострочных строк документации все должно быть в одной строке: начальные кавычки, итоговые и заключительные кавычки.

class HighTemperature:
    """Class representing very high temperatures"""

    # code starts here

При использовании многострочных строк документации структура выглядит следующим образом: вводные кавычки, краткое содержание, пустая строка, более подробное описание и заключительные кавычки.

def daily_average(temperatures: List[float]) -> float:
    """
    Get average daily temperature

    Calculate average temperature from multiple measurements

    :param temperatures: list of temperatures
    :return: average temperature
    """

    return sum(temperatures) / len(temperatures)

Помимо описания того, что делает конкретная функция, класс, метод или модуль, вы также можете указать:

  1. аргументы функции
  2. функция возвращает
  3. атрибуты класса
  4. возникали ошибки
  5. ограничения
  6. примеры кода

Форматы

Существуют четыре наиболее распространенных формата:

  1. Google
  2. Реструктурированный текст
  3. NumPy
  4. Эпитекст

Выберите тот, который подходит вам больше всего, и придерживайтесь его на протяжении всего проекта.

Используя docstrings, вы можете четко выразить свои намерения на разговорном языке, чтобы помочь другим (и самому себе в будущем!) лучше понять, когда, где и как использовать определенный код.

Ворсинка

Строки документации можно компоновать так же, как и в вашем коде. Средства компоновки обеспечивают правильный формат строк документации и их соответствие фактической реализации, что помогает сохранять документацию свежей.

Darglint - популярный инструмент для компоновки документации на Python.

$ pip install darglint

Давайте добавим temperature.py модуль:

def daily_average(temperatures: List[float]) -> float:
    """
    Get average daily temperature

    Calculate average temperature from multiple measurements

    :param temperatures: list of temperatures
    :return: average temperature
    """

    return sum(temperatures) / len(temperatures)

Ворсинок:

$ darglint --docstring-style sphinx temperature.py

Что произойдет, если вы измените название параметра с temperatures на temperatures_list?

$ darglint --docstring-style sphinx temperature.py

temperature.py:daily_average:27: DAR102: + temperatures
temperature.py:daily_average:27: DAR101: - temperatures_list

Примеры кода

Вы также можете добавить примеры кода в строки документации, показывающие пример использования функции, метода или класса.

Например:

def daily_average(temperatures: List[float], new_param=None) -> float:
    """
    Get average daily temperature

    Calculate average temperature from multiple measurements

    >>> daily_average([10.0, 12.0, 14.0])
    12.0

    :param temperatures: list of temperatures
    :return: Average temperature
    """

    return sum(temperatures)/len(temperatures)

Примеры кода также могут быть выполнены с помощью pytest, как и любой другой тест с помощью doctest. Наряду с редактированием, это также помогает гарантировать, что ваша документация остается свежей и синхронизированной с кодом.

Ознакомьтесь с doctest — тестирование с помощью документации для получения дополнительной информации doctest.

Итак, в приведенном выше примере pytest будет утверждать, что daily_average([10.0, 12.0, 14.0]) равно 12.0. Чтобы запустить этот пример кода в качестве теста, вам просто нужно запустить pytest с параметром doctest-modules:

$ python -m pytest --doctest-modules temperature.py

=============================== test session starts ===============================
platform darwin -- Python 3.11.0, pytest-7.2.1, pluggy-1.0.0
rootdir: /Users/michael/repos/testdriven/documenting-python
collected 1 item

temperature.py .                                                            [100%]

================================ 1 passed in 0.01s ================================

Что произойдет, если вы измените пример кода на:

>>> daily_average([10.0, 12.0, 14.0])
13.0
$ python -m pytest --doctest-modules temperature.py

=============================== test session starts ===============================
platform darwin -- Python 3.11.0, pytest-7.2.1, pluggy-1.0.0
rootdir: /Users/michael/repos/testdriven/documenting-python
collected 1 item

temperature.py F                                                            [100%]

==================================== FAILURES =====================================
_______________________ [doctest] temperature.daily_average _______________________
022
023     Get average daily temperature
024
025     Calculate average temperature from multiple measurements
026
027     >>> daily_average([10.0, 12.0, 14.0])
Expected:
    13.0
Got:
    12.0

/Users/michael/repos/testdriven/documenting-python/temperature.py:27: DocTestFailure
============================= short test summary info =============================
FAILED temperature.py::temperature.daily_average
================================ 1 failed in 0.01s ================================

Подробнее о pytest читайте в статье Тестирование на Python.

Сфинкс

Добавлять строки документации в свой код - это здорово, но вам все равно нужно представить это своим пользователям.

Здесь используются такие инструменты, как Sphinx, Epydoc и MkDocs вступайте в игру, которая преобразует строки документации вашего проекта в HTML и CSS.

Сфинкс, безусловно, самый популярный из них. Он используется для создания документации для ряда проектов с открытым исходным кодом, таких как Python и Flask. Это также один из инструментов документирования, поддерживаемых Read the Docs, который используется тысячами проектов с открытым исходным кодом, таких как Requests., Flake8 и pytest и это лишь некоторые из них.

Давайте посмотрим на это в действии. Для начала следуйте официальному руководству по загрузке и установке Sphinx.

$ sphinx-quickstart --version

sphinx-quickstart 6.1.3

Создайте новый каталог проекта:

$ mkdir sphinx_example
$ cd sphinx_example

Затем добавьте новый файл с именем temperature.py:

"""
The temperature module: Manipulate your temperature easily

Easily calculate daily average temperature
"""

from typing import List


class HighTemperature:
    """Class representing very high temperatures"""

    def __init__(self, value: float):
        """
        :param value: value of temperature
        """

        self.value = value


def daily_average(temperatures: List[float]) -> float:
    """
    Get average daily temperature

    :param temperatures: list of temperatures
    :return: average temperature
    """

    return sum(temperatures)/len(temperatures)

Чтобы выделить файлы и папки для Sphinx и создать документацию для temperature.py в корне проекта выполните следующее:

$ sphinx-quickstart docs

Вам будет предложено ответить на несколько вопросов:

> Separate source and build directories (y/n) [n]: n
> Project name: Temperature
> Author name(s): Your Name
> Project release []: 1.0.0
> Project language [en]: en

После этого каталог "документы" должен содержать следующие файлы и папки:

docs
├── Makefile
├── _build
├── _static
├── _templates
├── conf.py
├── index.rst
└── make.bat

Далее давайте обновим конфигурацию проекта. Откройте docs/conf.py и добавьте сверху следующее:

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

Теперь autodoc, который используется для извлечения документации из docstrings, будет искать модули в родительской папке "docs".

Добавьте следующие расширения в список extensions:

extensions = [
    'sphinx.ext.autodoc',
]

Откройте docs/index.rst и отредактируйте его так, чтобы он выглядел следующим образом:

Welcome to Temperature documentation!
=====================================

.. automodule:: temperature
    :members:



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Содержимое index.rst записано в reStructuredText, который представляет собой формат файла для текстовых данных, аналогичный Markdown, но гораздо более мощный, поскольку предназначен для написания технической документации.

Примечания:

  1. Заголовки создаются путем подчеркивания (и необязательно наложения) заголовка символом =, по крайней мере, такой же длины, как текст:
  2. Директива automodule используется для сбора строк документации из модулей Python. Итак, .. automodule:: temperature сообщает Sphinx собрать строки документации из модуля temperature.py .
  3. Директивы genindex, modindex, и search используются для создания общего индекса, индекса документированных модулей, и страница поиска, соответственно.

Из каталога "docs" создайте документацию:

$ make html

Откройте docs/_build/html/index.html в вашем браузере. Вы должны увидеть:

Sphinx docs

Теперь вы можете загружать документы самостоятельно, используя инструмент, подобный Netlify, или с помощью сервиса, подобного Прочитайте документы.

Документация по API

Говоря о документации, не забывайте о документации для ваших API. У вас есть конечные точки с их URL-адресами, параметрами URL-адресов, параметрами запроса, кодами состояния, текстами запросов и текстами ответов. Даже простой API может иметь ряд параметров, которые трудно запомнить.

Спецификация OpenAPI (ранее - спецификация Swagger) предоставляет стандартный формат для описания, создания, использования и визуализации RESTful API. Спецификация используется для создания документации с помощью Swagger UI или ReDoc. Его также можно импортировать в такие инструменты, как Postman. Вы также можете создавать разделительные заглушки и клиентские SDK-пакеты с помощью таких инструментов, как Swagger Codegen и OpenAPI Generator.

Полный список редакторов, линтеров, анализаторов, генераторов кода, документации, инструментов тестирования и проверки схем/данных для OpenAPI приведен в разделе Инструменты OpenAPI.

Сама спецификация должна быть написана либо в YAML, либо в JSON. Например:

---
openapi: 3.0.2
info:
  title: Swagger Petstore - OpenAPI 3.0
  description: |-
    This is a sample Open API
  version: 1.0.0
servers:
- url: "/api/v3"
paths:
  "/pet":
    post:
      summary: Add a new pet to the store
      description: Add a new pet to the store
      operationId: addPet
      requestBody:
        description: Create a new pet in the store
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/Pet"
        required: true
      responses:
        '200':
          description: Successful operation
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Pet"
        '405':
          description: Invalid input
components:
  schemas:
    Pet:
      required:
      - name
      - photoUrls
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        name:
          type: string
          example: doggie
        photoUrls:
          type: array
          items:
            type: string
        status:
          type: string
          description: pet status in the store
          enum:
          - available
          - pending
          - sold
  requestBodies:
    Pet:
      description: Pet object that needs to be added to the store
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Pet"

Писать такую схему вручную очень скучно и часто приводит к ошибкам. К счастью, существует ряд инструментов, которые помогают автоматизировать этот процесс:

Тесты в виде документации

До сих пор мы говорили о документации для пользователей (проектная документация) и разработчиков (комментарии к коду). Другой тип документации для разработчиков связан с самими тестами.

Как разработчик, работающий над проектом, вы должны знать больше, чем просто как использовать тот или иной метод. Вам нужно знать, работает ли он так, как ожидалось, и как его использовать для дальнейшей разработки. Хотя добавление примеров кода в строки документации может помочь в этом, такие примеры предназначены не для чего иного, как для простых примеров. Вам нужно добавить тесты, которые охватывают не только путь выполнения функции.

Тесты документируют три вещи:

  1. Каков ожидаемый результат при заданных входных данных
  2. Как обрабатываются пути к исключениям
  3. Как использовать данную функцию, метод или класс

Когда вы пишете тесты, обязательно используйте правильные названия и четко указывайте, что именно вы тестируете. Это значительно облегчит разработчику просмотр набора тестов, чтобы выяснить, как следует использовать ту или иную функцию или метод.

Более того, при написании теста вы в основном определяете, что должно входить в ваши документы. Структура , ЗАДАННАЯ, КОГДА, ЗАТЕМ, может быть легко преобразована в строки документации функции.

Например:

  • ПРИВЕДЕН список измерений температуры ->; :param temperatures: list of temperatures
  • ПРИ вызове 'daily_average' -> >>> daily_average([10.0, 12.0, 14.0])
  • ЗАТЕМ возвращается средняя температура -> Get average temperature, :return: Average temperature
def daily_average(temperatures: List[float]) -> float:
    """
    Get average temperature

    Calculate average temperature from multiple measurements

    >>> daily_average([10.0, 12.0, 14.0])
    12.0

    :param temperatures: list of temperatures
    :return: Average temperature
    """

    return sum(temperatures)/len(temperatures)

Итак, вы можете рассматривать Разработку на основе тестирования (TDD) как форму разработки на основе документации, создав свои строки документации в виде кода:

  1. Написать тест
  2. Убедиться, что тест не пройден
  3. Написать код
  4. Убедитесь, что тест прошел успешно
  5. Выполните рефакторинг и добавьте строки документации

Подробнее о TDD читайте в статье Современная разработка на основе тестирования на Python.

Документирование REST API Flask

До сих пор мы рассматривали только теорию, поэтому давайте перейдем к реальному примеру. Мы создадим RESTful API с Flask для измерения температуры. Каждое измерение будет иметь следующие атрибуты: временная метка, температура, примечания. Flask-RESTX будет использоваться для автоматической генерации спецификации OpenAPI.

Итак, давайте начнем. Сначала создайте новую папку:

$ mkdir flask_temperature
$ cd flask_temperature

Затем инициализируйте свой проект с помощью Poetry:

$ poetry init
Package name [flask_temperature]:
Version [0.1.0]:
Description []:
Author [Your name <[email protected]>, n to skip]:
License []:
Compatible Python versions [^3.11]:

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Do you confirm generation? (yes/no) [yes]

После этого добавьте Flask и Flask-RESTX:

$ poetry add flask flask-restx

Теперь давайте создадим наш документированный API. Добавьте файл для приложения Flask с именем app.py:

import uuid

from flask import Flask, request
from flask_restx import Api, Resource

app = Flask(__name__)
api = Api(app)

measurements = []


@api.route('/measurements')
class Measurement(Resource):
    def get(self):
        return measurements

    def post(self):
        measurement = {
            'id': str(uuid.uuid4()),
            'timestamp': request.json['timestamp'],
            'temperature': request.json['temperature'],
            'notes': request.json.get('notes'),
        }
        measurements.append(measurement)

        return measurement


if __name__ == '__main__':
    app.run()

Flask-RESTX использует представления на основе классов для организации ресурсов, маршрутов и методов HTTP. В приведенном выше примере класс Measurement поддерживает методы HTTP GET и POST. Другие методы вернут ошибку MethodNotAllowed. Flask-RESTX также сгенерирует схему OpenAPI при запуске приложения.

$ python app.py

Вы можете ознакомиться со схемой по адресу http://localhost:5000/swagger.json . Вы также сможете просмотреть доступный для просмотра API по адресу http://localhost:5000.

SwaggerUI

В настоящее время схема содержит только конечные точки. Мы можем определить текст запроса и ответа, чтобы сообщить нашим пользователям, что от них ожидается, а также что будет возвращено.

Обновить app.py:

import uuid

from flask import Flask, request
from flask_restx import Api, Resource, fields

app = Flask(__name__)
api = Api(app)

measurements = []

add_measurement_request_body = api.model(
    'AddMeasurementRequestBody', {
        'timestamp': fields.Integer(
            description='Timestamp of measurement',
            required=True,
            example=1606509272
        ),
        'temperature': fields.Float(
            description='Measured temperature',
            required=True, example=22.3),
        'notes': fields.String(
            description='Additional notes',
            required=False, example='Strange day'),
    }
)

measurement_model = api.model(
    'Measurement', {
        'id': fields.String(
            description='Unique ID',
            required=False,
            example='354e405c-136f-4e03-b5ce-5f92e3ed3ff8'
        ),
        'timestamp': fields.Integer(
            description='Timestamp of measurement',
            required=True,
            example=1606509272
        ),
        'temperature': fields.Float(
            description='Measured temperature',
            required=True,
            example=22.3
        ),
        'notes': fields.String(
            description='Additional notes',
            required=True,
            example='Strange day'
        ),
    }
)


@api.route('/measurements')
class Measurement(Resource):
    @api.doc(model=[measurement_model])
    def get(self):
        return measurements

    @api.doc(model=[measurement_model], body=add_measurement_request_body)
    def post(self):
        measurement = {
            'id': str(uuid.uuid4()),
            'timestamp': request.json['timestamp'],
            'temperature': request.json['temperature'],
            'notes': request.json.get('notes'),
        }
        measurements.append(measurement)

        return measurement


if __name__ == '__main__':
    app.run()

Чтобы определить модели для наших ответов и запросов, мы использовали api.model. Мы определили названия и соответствующие поля. Для каждого поля мы определили тип, описание, пример и то, требуется ли оно.

Swagger UI models

Чтобы добавить модели в конечные точки, мы использовали декоратор @api.doc. Параметр body определяет текст запроса, в то время как параметр model определяет текст ответа.

Swagger UI models

Теперь у вас должно быть общее представление о том, как документировать ваш Flask RESTful API с помощью Flask-RESTx. Это только начало. Ознакомьтесь с Документацией Swagger для получения подробной информации о том, как определить информацию для авторизации, параметры URL, коды состояния и многое другое.

Заключение

Большинство из нас, если не все, могут лучше справляться с написанием документации. К счастью, существует множество инструментов, упрощающих процесс ее написания. При написании пакетов и библиотек используйте Sphinx для организации и создания документации на основе строк документации. При работе с RESTful API используйте инструмент, который генерирует схему OpenAPI, поскольку эта схема может использоваться множеством инструментов - от средств проверки данных до генераторов кода. Ищете вдохновение? Stripe, Flask, Cypress и FastAPI являются отличными примерами хорошо выполненной документации.

Полное руководство по Python:

  1. Современные среды Python - управление зависимостями и рабочим пространством
  2. Тестирование на Python
  3. Современная разработка на основе тестирования на Python
  4. Качество кода на Python
  5. Проверка типа Python
  6. Документирование кода и проектов на Python (эта статья!)
  7. Рабочий процесс проекта на Python
Back to Top