22
33import copy
44import itertools
5+ import operator
56import os
67from collections import namedtuple
78from collections .abc import Set
8- from configparser import RawConfigParser
9+ from configparser import NoOptionError , NoSectionError , RawConfigParser
10+ from functools import reduce
911from re import compile as re
1012
1113from .utils import __version__ , log
1214from .violations import ErrorRegistry , conventions
1315
16+ try :
17+ import toml
18+ except ImportError : # pragma: no cover
19+ toml = None # type: ignore
20+
1421
1522def check_initialized (method ):
1623 """Check that the configuration object was initialized."""
@@ -23,6 +30,109 @@ def _decorator(self, *args, **kwargs):
2330 return _decorator
2431
2532
33+ class TomlParser :
34+ """ConfigParser that partially mimics RawConfigParser but for toml files.
35+
36+ See RawConfigParser for more info. Also, please note that not all
37+ RawConfigParser functionality is implemented, but only the subset that is
38+ currently used by pydocstyle.
39+ """
40+
41+ def __init__ (self ):
42+ """Create a toml parser."""
43+ self ._config = {}
44+
45+ def read (self , filenames , encoding = None ):
46+ """Read and parse a filename or an iterable of filenames.
47+
48+ Files that cannot be opened are silently ignored; this is
49+ designed so that you can specify an iterable of potential
50+ configuration file locations (e.g. current directory, user's
51+ home directory, systemwide directory), and all existing
52+ configuration files in the iterable will be read. A single
53+ filename may also be given.
54+
55+ Return list of successfully read files.
56+ """
57+ if isinstance (filenames , (str , bytes , os .PathLike )):
58+ filenames = [filenames ]
59+ read_ok = []
60+ for filename in filenames :
61+ try :
62+ with open (filename , encoding = encoding ) as fp :
63+ if not toml :
64+ log .warning (
65+ "The %s configuration file was ignored, "
66+ "because the `toml` package is not installed." ,
67+ filename ,
68+ )
69+ continue
70+ self ._config .update (toml .load (fp ))
71+ except OSError :
72+ continue
73+ if isinstance (filename , os .PathLike ):
74+ filename = os .fspath (filename )
75+ read_ok .append (filename )
76+ return read_ok
77+
78+ def _get_section (self , section , allow_none = False ):
79+ try :
80+ current = reduce (
81+ operator .getitem ,
82+ section .split ('.' ),
83+ self ._config ['tool' ],
84+ )
85+ except KeyError :
86+ current = None
87+
88+ if isinstance (current , dict ):
89+ return current
90+ elif allow_none :
91+ return None
92+ else :
93+ raise NoSectionError (section )
94+
95+ def has_section (self , section ):
96+ """Indicate whether the named section is present in the configuration."""
97+ return self ._get_section (section , allow_none = True ) is not None
98+
99+ def options (self , section ):
100+ """Return a list of option names for the given section name."""
101+ current = self ._get_section (section )
102+ return list (current .keys ())
103+
104+ def get (self , section , option , * , _conv = None ):
105+ """Get an option value for a given section."""
106+ d = self ._get_section (section )
107+ option = option .lower ()
108+ try :
109+ value = d [option ]
110+ except KeyError :
111+ raise NoOptionError (option , section )
112+
113+ if isinstance (value , dict ):
114+ raise TypeError (
115+ f"Expected { section } .{ option } to be an option, not a section."
116+ )
117+
118+ # toml should convert types automatically
119+ # don't manually convert, just check, that the type is correct
120+ if _conv is not None and not isinstance (value , _conv ):
121+ raise TypeError (
122+ f"The type of { section } .{ option } should be { _conv } "
123+ )
124+
125+ return value
126+
127+ def getboolean (self , section , option ):
128+ """Get a boolean option value for a given section."""
129+ return self .get (section , option , _conv = bool )
130+
131+ def getint (self , section , option ):
132+ """Get an integer option value for a given section."""
133+ return self .get (section , option , _conv = int )
134+
135+
26136class ConfigurationParser :
27137 """Responsible for parsing configuration from files and CLI.
28138
@@ -85,6 +195,7 @@ class ConfigurationParser:
85195 '.pydocstyle.ini' ,
86196 '.pydocstylerc' ,
87197 '.pydocstylerc.ini' ,
198+ 'pyproject.toml' ,
88199 # The following is deprecated, but remains for backwards compatibility.
89200 '.pep257' ,
90201 )
@@ -310,7 +421,10 @@ def _read_configuration_file(self, path):
310421 Returns (options, should_inherit).
311422
312423 """
313- parser = RawConfigParser (inline_comment_prefixes = ('#' , ';' ))
424+ if path .endswith ('.toml' ):
425+ parser = TomlParser ()
426+ else :
427+ parser = RawConfigParser (inline_comment_prefixes = ('#' , ';' ))
314428 options = None
315429 should_inherit = True
316430
@@ -433,7 +547,10 @@ def _get_config_file_in_folder(cls, path):
433547 path = os .path .dirname (path )
434548
435549 for fn in cls .PROJECT_CONFIG_FILES :
436- config = RawConfigParser ()
550+ if fn .endswith ('.toml' ):
551+ config = TomlParser ()
552+ else :
553+ config = RawConfigParser (inline_comment_prefixes = ('#' , ';' ))
437554 full_path = os .path .join (path , fn )
438555 if config .read (full_path ) and cls ._get_section_name (config ):
439556 return full_path
@@ -552,8 +669,10 @@ def _get_set(value_str):
552669 file.
553670
554671 """
672+ if isinstance (value_str , str ):
673+ value_str = value_str .split ("," )
555674 return cls ._expand_error_codes (
556- {x .strip () for x in value_str . split ( "," ) } - {"" }
675+ {x .strip () for x in value_str } - {"" }
557676 )
558677
559678 for opt in optional_set_options :
0 commit comments