From 801fc92acb0f501d68fb9dea5e4ec4a5d8050ff6 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sat, 14 Jan 2023 11:01:33 +0100 Subject: [PATCH 1/6] Add Convention class --- src/pydocstyle/conventions.py | 109 ++++++++++++++++++++++++++++++++++ src/tests/test_conventions.py | 78 ++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/pydocstyle/conventions.py create mode 100644 src/tests/test_conventions.py diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py new file mode 100644 index 0000000..e60bba4 --- /dev/null +++ b/src/pydocstyle/conventions.py @@ -0,0 +1,109 @@ +"""This module contains the convention definitions.""" + + +from typing import Literal, 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', + }, +} + + +class Convention: + """This class defines the convention to use for checking docstrings.""" + + def __init__( + self, name: Literal["pep257", "numpy", "google"] = "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. + + 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 (Literal["pep257", "numpy", "google"], optional): The convention to use. Defaults to "pep257". + + Raises: + ValueError: _description_ + """ + if name not in CONVENTION_NAMES: + raise ValueError( + f"Convention '{name}' is invalid. Must be one " + f"of {CONVENTION_NAMES}." + ) + + 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 (Set[str]): 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: + """Remove error codes from the convention. + + Args: + error_codes (Set[str]): The error codes to ignore. + """ + self.error_codes = self.error_codes - error_codes diff --git a/src/tests/test_conventions.py b/src/tests/test_conventions.py new file mode 100644 index 0000000..086245a --- /dev/null +++ b/src/tests/test_conventions.py @@ -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_only_specific_convention_names_are_allowed() -> None: + """Test that an error is raised if an invalid convention name is used.""" + with pytest.raises( + ValueError, match="Convention 'invalid_convention' is invalid" + ): + convention = Convention("invalid_convention") + + +def test_default_convention_is_pep257() -> None: + """Test that pep257 is used as the default convention.""" + convention = Convention() + + assert convention.name == "pep257" + + +@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 From 6d3a59db47d7704b21d59f46dfb3853ac96b5b68 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sat, 14 Jan 2023 11:02:33 +0100 Subject: [PATCH 2/6] Use convention class to retrieve error codes --- src/pydocstyle/__init__.py | 2 +- src/pydocstyle/config.py | 15 +++++---- src/pydocstyle/conventions.py | 5 +-- src/pydocstyle/violations.py | 63 ++--------------------------------- src/tests/test_conventions.py | 12 +++---- src/tests/test_integration.py | 17 +++++----- 6 files changed, 24 insertions(+), 90 deletions(-) diff --git a/src/pydocstyle/__init__.py b/src/pydocstyle/__init__.py index 363ea3f..290ca4c 100644 --- a/src/pydocstyle/__init__.py +++ b/src/pydocstyle/__init__.py @@ -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 diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index c05f7dc..142f22c 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -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 @@ -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', @@ -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) @@ -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) @@ -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 @@ -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( diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py index e60bba4..7a77487 100644 --- a/src/pydocstyle/conventions.py +++ b/src/pydocstyle/conventions.py @@ -84,10 +84,7 @@ def __init__( ValueError: _description_ """ if name not in CONVENTION_NAMES: - raise ValueError( - f"Convention '{name}' is invalid. Must be one " - f"of {CONVENTION_NAMES}." - ) + name = "pep257" self.name = name self.error_codes = convention_errors[name] diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 8156921..cc093fb 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -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']) @@ -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', - }, - } -) diff --git a/src/tests/test_conventions.py b/src/tests/test_conventions.py index 086245a..c9ab3bb 100644 --- a/src/tests/test_conventions.py +++ b/src/tests/test_conventions.py @@ -7,20 +7,16 @@ from pydocstyle.conventions import CONVENTION_NAMES, Convention -def test_only_specific_convention_names_are_allowed() -> None: - """Test that an error is raised if an invalid convention name is used.""" - with pytest.raises( - ValueError, match="Convention 'invalid_convention' is invalid" - ): - convention = Convention("invalid_convention") - - def test_default_convention_is_pep257() -> None: """Test that pep257 is used as the default convention.""" convention = Convention() assert convention.name == "pep257" + convention = Convention("invalid_convention") + + assert convention.name == "pep257" + @pytest.mark.parametrize("convention_name", CONVENTION_NAMES) def test_names_are_set_correctly(convention_name: str) -> None: diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index 2f2f57c..e6cd668 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -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__ = () @@ -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 From 9a54a2af7f0b44a3d17370b6b26bb399fe283ea3 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sat, 14 Jan 2023 11:03:39 +0100 Subject: [PATCH 3/6] Fix Literal typing for Python 3.7 --- src/pydocstyle/conventions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py index 7a77487..f1e8761 100644 --- a/src/pydocstyle/conventions.py +++ b/src/pydocstyle/conventions.py @@ -1,7 +1,8 @@ """This module contains the convention definitions.""" +from typing import Set -from typing import Literal, Set +from typing_extensions import Literal from pydocstyle.violations import all_errors From 8e9ef40e48fea13f3bf82ff6843390203ada2793 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sun, 15 Jan 2023 15:08:11 +0100 Subject: [PATCH 4/6] Address feedback from PR review --- src/pydocstyle/conventions.py | 8 +++----- src/tests/test_conventions.py | 8 ++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py index f1e8761..9c05c9d 100644 --- a/src/pydocstyle/conventions.py +++ b/src/pydocstyle/conventions.py @@ -64,9 +64,7 @@ class Convention: """This class defines the convention to use for checking docstrings.""" - def __init__( - self, name: Literal["pep257", "numpy", "google"] = "pep257" - ) -> None: + def __init__(self, name: str = "pep257") -> None: """Initialize the convention. The convention has two purposes. First, it holds the error codes to be @@ -79,13 +77,13 @@ def __init__( :code:`add_error_codes` or :codes:`remove_error_codes` methods. Args: - name (Literal["pep257", "numpy", "google"], optional): The convention to use. Defaults to "pep257". + name: The convention to use. Defaults to "pep257". Raises: ValueError: _description_ """ if name not in CONVENTION_NAMES: - name = "pep257" + raise ValueError(f"Unknown convention: '{name}'") self.name = name self.error_codes = convention_errors[name] diff --git a/src/tests/test_conventions.py b/src/tests/test_conventions.py index c9ab3bb..d4c118e 100644 --- a/src/tests/test_conventions.py +++ b/src/tests/test_conventions.py @@ -13,9 +13,13 @@ def test_default_convention_is_pep257() -> None: assert convention.name == "pep257" - convention = Convention("invalid_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) From 0a393a2c0b64293bf5961cf83a1cb908230bc9c8 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sun, 15 Jan 2023 20:18:12 +0100 Subject: [PATCH 5/6] Remove unnecessary import --- src/pydocstyle/conventions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py index 9c05c9d..80d50ab 100644 --- a/src/pydocstyle/conventions.py +++ b/src/pydocstyle/conventions.py @@ -2,8 +2,6 @@ from typing import Set -from typing_extensions import Literal - from pydocstyle.violations import all_errors CONVENTION_NAMES = ("pep257", "numpy", "google") From 0c9ba5bc72722b624f4c44e0bd864f82ef915fc9 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Sun, 15 Jan 2023 20:34:51 +0100 Subject: [PATCH 6/6] Fix docstrings --- src/pydocstyle/conventions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pydocstyle/conventions.py b/src/pydocstyle/conventions.py index 80d50ab..4c5cc05 100644 --- a/src/pydocstyle/conventions.py +++ b/src/pydocstyle/conventions.py @@ -78,7 +78,7 @@ def __init__(self, name: str = "pep257") -> None: name: The convention to use. Defaults to "pep257". Raises: - ValueError: _description_ + ValueError: If an unsupported convention is specified. """ if name not in CONVENTION_NAMES: raise ValueError(f"Unknown convention: '{name}'") @@ -90,7 +90,7 @@ def add_error_codes(self, error_codes: Set[str]) -> None: """Add additional error codes to the convention. Args: - error_codes (Set[str]): The error codes to also check. + error_codes: The error codes to also check. """ self.error_codes = self.error_codes.union(error_codes) @@ -98,6 +98,6 @@ def remove_error_codes(self, error_codes: Set[str]) -> None: """Remove error codes from the convention. Args: - error_codes (Set[str]): The error codes to ignore. + error_codes: The error codes to ignore. """ self.error_codes = self.error_codes - error_codes