Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Introduce convention class #589

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pydocstyle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Temporary hotfix for flake8-docstrings
from .checker import ConventionChecker, check
from .parser import AllError
from .violations import Error, conventions
from .violations import Error
15 changes: 8 additions & 7 deletions src/pydocstyle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from re import compile as re

from ._version import __version__
from .conventions import CONVENTION_NAMES, Convention
from .utils import log
from .violations import ErrorRegistry, conventions
from .violations import ErrorRegistry

if sys.version_info >= (3, 11):
import tomllib
Expand Down Expand Up @@ -194,7 +195,7 @@ class ConfigurationParser:
DEFAULT_PROPERTY_DECORATORS = (
"property,cached_property,functools.cached_property"
)
DEFAULT_CONVENTION = conventions.pep257
DEFAULT_CONVENTION = Convention()

PROJECT_CONFIG_FILES = (
'setup.cfg',
Expand Down Expand Up @@ -602,7 +603,7 @@ def _get_exclusive_error_codes(cls, options):
elif options.select is not None:
checked_codes = cls._expand_error_codes(options.select)
elif options.convention is not None:
checked_codes = getattr(conventions, options.convention)
checked_codes = Convention(options.convention).error_codes

# To not override the conventions nor the options - copy them.
return copy.deepcopy(checked_codes)
Expand Down Expand Up @@ -647,7 +648,7 @@ def _get_checked_errors(cls, options):
"""Extract the codes needed to be checked from `options`."""
checked_codes = cls._get_exclusive_error_codes(options)
if checked_codes is None:
checked_codes = cls.DEFAULT_CONVENTION
checked_codes = cls.DEFAULT_CONVENTION.error_codes

cls._set_add_options(checked_codes, options)

Expand All @@ -671,10 +672,10 @@ def _validate_options(cls, options):
)
return False

if options.convention and options.convention not in conventions:
if options.convention and options.convention not in CONVENTION_NAMES:
log.error(
"Illegal convention '{}'. Possible conventions: {}".format(
options.convention, ', '.join(conventions.keys())
options.convention, ', '.join(CONVENTION_NAMES)
)
)
return False
Expand Down Expand Up @@ -830,7 +831,7 @@ def _create_option_parser(cls):
default=None,
help='choose the basic list of checked errors by specifying '
'an existing convention. Possible conventions: {}.'.format(
', '.join(conventions)
', '.join(CONVENTION_NAMES)
),
)
add_check(
Expand Down
103 changes: 103 additions & 0 deletions src/pydocstyle/conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""This module contains the convention definitions."""

from typing import Set

from pydocstyle.violations import all_errors

CONVENTION_NAMES = ("pep257", "numpy", "google")


convention_errors = {
'pep257': all_errors
- {
'D203',
'D212',
'D213',
'D214',
'D215',
'D404',
'D405',
'D406',
'D407',
'D408',
'D409',
'D410',
'D411',
'D413',
'D415',
'D416',
'D417',
'D418',
},
'numpy': all_errors
- {
'D107',
'D203',
'D212',
'D213',
'D402',
'D413',
'D415',
'D416',
'D417',
},
'google': all_errors
- {
'D203',
'D204',
'D213',
'D215',
'D400',
'D401',
'D404',
'D406',
'D407',
'D408',
'D409',
'D413',
},
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there is a common subset of the errors being removed from each of the conventions that makes sense to name. E.g., something along the lines of "very strict rules".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't love the "all_errors - {subset}" logic, but not just because of code duplication. Any time anyone adds a new convention, we have to add it to this list. Personally, it makes more sense to me to make these additive instead of subtractive. But let's save this for another issue/PR since this PR doesn't change anything, just moves it around.



class Convention:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of using a class instead of just using an Enum and keeping the AttrDict?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was to eventually have the convention class itself check the code and select the appropriate checker function. You can probably also just throw the name of the convention into the convention checker and that's it.

"""This class defines the convention to use for checking docstrings."""

def __init__(self, name: str = "pep257") -> None:
"""Initialize the convention.

The convention has two purposes. First, it holds the error codes to be
checked. Second, it defines how to treat docstrings, eliminating the
need for extra logic to determine whether a docstring is a NumPy or
Google style docstring.
Comment on lines +68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second part seems especially useful to me. Going very far down the road to DWIM ("do what I mean") gets pretty painful. Users end up having to trick the tool into doing what they want instead of just specifying it as this approach allows.


Each convention has a set of error codes to check as a baseline.
Specific error codes can be added or removed via the
:code:`add_error_codes` or :codes:`remove_error_codes` methods.

Args:
name: The convention to use. Defaults to "pep257".

Raises:
ValueError: If an unsupported convention is specified.
"""
if name not in CONVENTION_NAMES:
raise ValueError(f"Unknown convention: '{name}'")

self.name = name
self.error_codes = convention_errors[name]

def add_error_codes(self, error_codes: Set[str]) -> None:
"""Add additional error codes to the convention.

Args:
error_codes: The error codes to also check.
"""
self.error_codes = self.error_codes.union(error_codes)

def remove_error_codes(self, error_codes: Set[str]) -> None:
Comment on lines +89 to +97

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you instead define __add__/__or__ and __sub__, you can run:

convention = Convention()
convention += {"D103"}
convention -= {"D101", "D102"}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's possible, but at least to me it makes the whole thing less obvious, without offering a real benefit.

"""Remove error codes from the convention.

Args:
error_codes: The error codes to ignore.
"""
self.error_codes = self.error_codes - error_codes
63 changes: 2 additions & 61 deletions src/pydocstyle/violations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from collections import namedtuple
from functools import partial
from itertools import dropwhile
from typing import Any, Callable, Iterable, List, Optional
from typing import Callable, Iterable, List, Optional

from .parser import Definition
from .utils import is_blank

__all__ = ('Error', 'ErrorRegistry', 'conventions')
__all__ = ('Error', 'ErrorRegistry')


ErrorParams = namedtuple('ErrorParams', ['code', 'short_desc', 'context'])
Expand Down Expand Up @@ -421,63 +421,4 @@ def to_rst(cls) -> str:
)


class AttrDict(dict):
def __getattr__(self, item: str) -> Any:
return self[item]


all_errors = set(ErrorRegistry.get_error_codes())


conventions = AttrDict(
{
'pep257': all_errors
- {
'D203',
'D212',
'D213',
'D214',
'D215',
'D404',
'D405',
'D406',
'D407',
'D408',
'D409',
'D410',
'D411',
'D413',
'D415',
'D416',
'D417',
'D418',
},
'numpy': all_errors
- {
'D107',
'D203',
'D212',
'D213',
'D402',
'D413',
'D415',
'D416',
'D417',
},
'google': all_errors
- {
'D203',
'D204',
'D213',
'D215',
'D400',
'D401',
'D404',
'D406',
'D407',
'D408',
'D409',
'D413',
},
}
)
78 changes: 78 additions & 0 deletions src/tests/test_conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""This module contains tests for the available conventions."""

import re

import pytest

from pydocstyle.conventions import CONVENTION_NAMES, Convention


def test_default_convention_is_pep257() -> None:
"""Test that pep257 is used as the default convention."""
convention = Convention()

assert convention.name == "pep257"


def test_invalid_convention_raises_error() -> None:
"""Test that using an invalid convention name raises an error."""
with pytest.raises(
ValueError, match="Unknown convention: 'invalid_convention'"
):
Convention("invalid_convention")


@pytest.mark.parametrize("convention_name", CONVENTION_NAMES)
def test_names_are_set_correctly(convention_name: str) -> None:
"""Test that the convention holds its name as an attribute."""
convention = Convention(convention_name)

assert convention.name == convention_name


@pytest.mark.parametrize("convention_name", CONVENTION_NAMES)
def test_conventions_keep_their_error_codes_as_attribute(
convention_name: str,
) -> None:
"""Check that conventions are initialized with a set of error codes."""
convention = Convention(convention_name)

assert len(convention.error_codes) > 0

for error_code in convention.error_codes:
assert len(error_code) == 4
assert re.compile(r"D[1-4]\d\d").match(error_code)


def test_can_add_error_codes() -> None:
"""Test that additional error codes can be added to a convention."""
convention = Convention()

n_errors_before_adding = len(convention.error_codes)

assert "D203" not in convention.error_codes
assert "D212" not in convention.error_codes

convention.add_error_codes({"D203", "D212"})

assert len(convention.error_codes) - n_errors_before_adding == 2

assert "D203" in convention.error_codes
assert "D212" in convention.error_codes


def test_can_remove_error_codes() -> None:
"""Test that specific error codes can be removed from a convention."""
convention = Convention()

n_errors_before_removal = len(convention.error_codes)

assert "D100" in convention.error_codes
assert "D102" in convention.error_codes

convention.remove_error_codes({"D100", "D102"})

assert n_errors_before_removal - len(convention.error_codes) == 2

assert "D100" not in convention.error_codes
assert "D102" not in convention.error_codes
17 changes: 8 additions & 9 deletions src/tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
"""Use tox or pytest to run the test-suite."""

from collections import namedtuple

import os
import pathlib
import shlex
import shutil
import pytest
import pathlib
import tempfile
import textwrap
import subprocess
import sys

import tempfile
import textwrap
from collections import namedtuple
from unittest import mock

from pydocstyle import checker, violations
import pytest

from pydocstyle import checker
from pydocstyle.conventions import Convention

__all__ = ()

Expand Down Expand Up @@ -187,7 +186,7 @@ def test_pep257_conformance():
if excluded not in path.parents)

ignored = {'D104', 'D105'}
select = violations.conventions.pep257 - ignored
select = Convention("pep257").error_codes - ignored
errors = list(checker.check(src_files, select=select))
assert errors == [], errors

Expand Down