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

Enable full toml configuration and pyproject.toml #534

Merged
merged 17 commits into from May 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/release_notes.rst
Expand Up @@ -4,6 +4,12 @@ Release Notes
**pydocstyle** version numbers follow the
`Semantic Versioning <http://semver.org/>`_ specification.

Current Development Version
---------------------------

New Features

* Enable full toml configuration and pyproject.toml (#534).

6.0.0 - March 18th, 2021
---------------------------
Expand Down
21 changes: 15 additions & 6 deletions docs/snippets/config.rst
@@ -1,18 +1,27 @@
``pydocstyle`` supports *ini*-like configuration files.
In order for ``pydocstyle`` to use it, it must be named one of the following
options, and have a ``[pydocstyle]`` section.
``pydocstyle`` supports *ini*-like and *toml* configuration files.
In order for ``pydocstyle`` to use a configuration file automatically, it must
be named one of the following options.

* ``setup.cfg``
* ``tox.ini``
* ``.pydocstyle``
* ``.pydocstyle.ini``
* ``.pydocstylerc``
* ``.pydocstylerc.ini``
* ``pyproject.toml``

When searching for a configuration file, ``pydocstyle`` looks for one of the
file specified above *in that exact order*. If a configuration file was not
found, it keeps looking for one up the directory tree until one is found or
uses the default configuration.
file specified above *in that exact order*. *ini*-like configuration files must
have a ``[pydocstyle]`` section while *toml* configuration files must have a
``[tool.pydocstyle]`` section. If a configuration file was not found,
``pydocstyle`` keeps looking for one up the directory tree until one is found
or uses the default configuration.

.. note::

*toml* configuration file support is only enabled if the ``toml`` python
package is installed. You can ensure that this is the case by installing
the ``pydocstyle[toml]`` optional dependency.

.. note::

Expand Down
1 change: 1 addition & 0 deletions requirements/runtime.txt
@@ -1 +1,2 @@
snowballstemmer==1.2.1
toml==0.10.2
4 changes: 4 additions & 0 deletions setup.py
Expand Up @@ -7,6 +7,9 @@
requirements = [
'snowballstemmer',
]
extra_requirements = {
'toml': ['toml'],
}


setup(
Expand Down Expand Up @@ -36,6 +39,7 @@
package_dir={'': 'src'},
package_data={'pydocstyle': ['data/*.txt']},
install_requires=requirements,
extras_require=extra_requirements,
entry_points={
'console_scripts': [
'pydocstyle = pydocstyle.cli:main',
Expand Down
127 changes: 123 additions & 4 deletions src/pydocstyle/config.py
Expand Up @@ -2,15 +2,22 @@

import copy
import itertools
import operator
import os
from collections import namedtuple
from collections.abc import Set
from configparser import RawConfigParser
from configparser import NoOptionError, NoSectionError, RawConfigParser
from functools import reduce
from re import compile as re

from .utils import __version__, log
from .violations import ErrorRegistry, conventions

try:
import toml
except ImportError: # pragma: no cover
toml = None # type: ignore


def check_initialized(method):
"""Check that the configuration object was initialized."""
Expand All @@ -23,6 +30,109 @@ def _decorator(self, *args, **kwargs):
return _decorator


class TomlParser:
"""ConfigParser that partially mimics RawConfigParser but for toml files.

See RawConfigParser for more info. Also, please note that not all
RawConfigParser functionality is implemented, but only the subset that is
currently used by pydocstyle.
"""

def __init__(self):
"""Create a toml parser."""
self._config = {}

def read(self, filenames, encoding=None):
"""Read and parse a filename or an iterable of filenames.

Files that cannot be opened are silently ignored; this is
designed so that you can specify an iterable of potential
configuration file locations (e.g. current directory, user's
home directory, systemwide directory), and all existing
configuration files in the iterable will be read. A single
filename may also be given.

Return list of successfully read files.
"""
if isinstance(filenames, (str, bytes, os.PathLike)):
filenames = [filenames]
read_ok = []
for filename in filenames:
try:
with open(filename, encoding=encoding) as fp:
if not toml:
log.warning(
"The %s configuration file was ignored, "
"because the `toml` package is not installed.",
filename,
)
continue
self._config.update(toml.load(fp))
except OSError:
continue
if isinstance(filename, os.PathLike):
filename = os.fspath(filename)
read_ok.append(filename)
return read_ok

def _get_section(self, section, allow_none=False):
try:
current = reduce(
operator.getitem,
section.split('.'),
self._config['tool'],
)
except KeyError:
current = None

if isinstance(current, dict):
return current
elif allow_none:
return None
else:
raise NoSectionError(section)

def has_section(self, section):
"""Indicate whether the named section is present in the configuration."""
return self._get_section(section, allow_none=True) is not None

def options(self, section):
"""Return a list of option names for the given section name."""
current = self._get_section(section)
return list(current.keys())

def get(self, section, option, *, _conv=None):
"""Get an option value for a given section."""
d = self._get_section(section)
option = option.lower()
try:
value = d[option]
except KeyError:
raise NoOptionError(option, section)

if isinstance(value, dict):
raise TypeError(
f"Expected {section}.{option} to be an option, not a section."
)

# toml should convert types automatically
# don't manually convert, just check, that the type is correct
if _conv is not None and not isinstance(value, _conv):
raise TypeError(
f"The type of {section}.{option} should be {_conv}"
)

return value

def getboolean(self, section, option):
"""Get a boolean option value for a given section."""
return self.get(section, option, _conv=bool)

def getint(self, section, option):
"""Get an integer option value for a given section."""
return self.get(section, option, _conv=int)


class ConfigurationParser:
"""Responsible for parsing configuration from files and CLI.

Expand Down Expand Up @@ -85,6 +195,7 @@ class ConfigurationParser:
'.pydocstyle.ini',
'.pydocstylerc',
'.pydocstylerc.ini',
'pyproject.toml',
# The following is deprecated, but remains for backwards compatibility.
'.pep257',
)
Expand Down Expand Up @@ -310,7 +421,10 @@ def _read_configuration_file(self, path):
Returns (options, should_inherit).

"""
parser = RawConfigParser(inline_comment_prefixes=('#', ';'))
if path.endswith('.toml'):
parser = TomlParser()
else:
parser = RawConfigParser(inline_comment_prefixes=('#', ';'))
options = None
should_inherit = True

Expand Down Expand Up @@ -433,7 +547,10 @@ def _get_config_file_in_folder(cls, path):
path = os.path.dirname(path)

for fn in cls.PROJECT_CONFIG_FILES:
config = RawConfigParser()
if fn.endswith('.toml'):
config = TomlParser()
else:
config = RawConfigParser(inline_comment_prefixes=('#', ';'))
full_path = os.path.join(path, fn)
if config.read(full_path) and cls._get_section_name(config):
return full_path
Expand Down Expand Up @@ -552,8 +669,10 @@ def _get_set(value_str):
file.

"""
if isinstance(value_str, str):
value_str = value_str.split(",")
return cls._expand_error_codes(
{x.strip() for x in value_str.split(",")} - {""}
{x.strip() for x in value_str} - {""}
)

for opt in optional_set_options:
Expand Down