1"""Docstring violation definition.""" 2 3from collections import namedtuple 4from functools import partial 5from itertools import dropwhile 6from typing import Any, Callable, Iterable, List, Optional 7 8from .parser import Definition 9from .utils import is_blank 10 11__all__ = ('Error', 'ErrorRegistry', 'conventions') 12 13 14ErrorParams = namedtuple('ErrorParams', ['code', 'short_desc', 'context']) 15 16 17class Error: 18 """Error in docstring style.""" 19 20 # Options that define how errors are printed: 21 explain = False 22 source = False 23 24 def __init__( 25 self, 26 code: str, 27 short_desc: str, 28 context: str, 29 *parameters: Iterable[str], 30 ) -> None: 31 """Initialize the object. 32 33 `parameters` are specific to the created error. 34 35 """ 36 self.code = code 37 self.short_desc = short_desc 38 self.context = context 39 self.parameters = parameters 40 self.definition = None # type: Optional[Definition] 41 self.explanation = None # type: Optional[str] 42 43 def set_context(self, definition: Definition, explanation: str) -> None: 44 """Set the source code context for this error.""" 45 self.definition = definition 46 self.explanation = explanation 47 48 filename = property(lambda self: self.definition.module.name) 49 line = property(lambda self: self.definition.error_lineno) 50 51 @property 52 def message(self) -> str: 53 """Return the message to print to the user.""" 54 ret = f'{self.code}: {self.short_desc}' 55 if self.context is not None: 56 specific_error_msg = self.context.format(*self.parameters) 57 ret += f' ({specific_error_msg})' 58 return ret 59 60 @property 61 def lines(self) -> str: 62 """Return the source code lines for this error.""" 63 if self.definition is None: 64 return '' 65 source = '' 66 lines = self.definition.source.splitlines(keepends=True) 67 offset = self.definition.start # type: ignore 68 lines_stripped = list( 69 reversed(list(dropwhile(is_blank, reversed(lines)))) 70 ) 71 numbers_width = len(str(offset + len(lines_stripped))) 72 line_format = f'{{:{numbers_width}}}:{{}}' 73 for n, line in enumerate(lines_stripped): 74 if line: 75 line = ' ' + line 76 source += line_format.format(n + offset, line) 77 if n > 5: 78 source += ' ...\n' 79 break 80 return source 81 82 def __str__(self) -> str: 83 if self.explanation: 84 self.explanation = '\n'.join( 85 l for l in self.explanation.split('\n') if not is_blank(l) 86 ) 87 template = '{filename}:{line} {definition}:\n {message}' 88 if self.source and self.explain: 89 template += '\n\n{explanation}\n\n{lines}\n' 90 elif self.source and not self.explain: 91 template += '\n\n{lines}\n' 92 elif self.explain and not self.source: 93 template += '\n\n{explanation}\n\n' 94 return template.format( 95 **{ 96 name: getattr(self, name) 97 for name in [ 98 'filename', 99 'line', 100 'definition', 101 'message', 102 'explanation', 103 'lines', 104 ] 105 } 106 ) 107 108 def __repr__(self) -> str: 109 return str(self) 110 111 def __lt__(self, other: 'Error') -> bool: 112 return (self.filename, self.line) < (other.filename, other.line) 113 114 115class ErrorRegistry: 116 """A registry of all error codes, divided to groups.""" 117 118 groups = [] # type: ignore 119 120 class ErrorGroup: 121 """A group of similarly themed errors.""" 122 123 def __init__(self, prefix: str, name: str) -> None: 124 """Initialize the object. 125 126 `Prefix` should be the common prefix for errors in this group, 127 e.g., "D1". 128 `name` is the name of the group (its subject). 129 130 """ 131 self.prefix = prefix 132 self.name = name 133 self.errors = [] # type: List[ErrorParams] 134 135 def create_error( 136 self, 137 error_code: str, 138 error_desc: str, 139 error_context: Optional[str] = None, 140 ) -> Callable[[Iterable[str]], Error]: 141 """Create an error, register it to this group and return it.""" 142 # TODO: check prefix 143 144 error_params = ErrorParams(error_code, error_desc, error_context) 145 factory = partial(Error, *error_params) 146 self.errors.append(error_params) 147 return factory 148 149 @classmethod 150 def create_group(cls, prefix: str, name: str) -> ErrorGroup: 151 """Create a new error group and return it.""" 152 group = cls.ErrorGroup(prefix, name) 153 cls.groups.append(group) 154 return group 155 156 @classmethod 157 def get_error_codes(cls) -> Iterable[str]: 158 """Yield all registered codes.""" 159 for group in cls.groups: 160 for error in group.errors: 161 yield error.code 162 163 @classmethod 164 def to_rst(cls) -> str: 165 """Output the registry as reStructuredText, for documentation.""" 166 max_len = max( 167 len(error.short_desc) 168 for group in cls.groups 169 for error in group.errors 170 ) 171 sep_line = '+' + 6 * '-' + '+' + '-' * (max_len + 2) + '+\n' 172 blank_line = '|' + (max_len + 9) * ' ' + '|\n' 173 table = '' 174 for group in cls.groups: 175 table += sep_line 176 table += blank_line 177 table += '|' + f'**{group.name}**'.center(max_len + 9) + '|\n' 178 table += blank_line 179 for error in group.errors: 180 table += sep_line 181 table += ( 182 '|' 183 + error.code.center(6) 184 + '| ' 185 + error.short_desc.ljust(max_len + 1) 186 + '|\n' 187 ) 188 table += sep_line 189 return table 190 191 192D1xx = ErrorRegistry.create_group('D1', 'Missing Docstrings') 193D100 = D1xx.create_error( 194 'D100', 195 'Missing docstring in public module', 196) 197D101 = D1xx.create_error( 198 'D101', 199 'Missing docstring in public class', 200) 201D102 = D1xx.create_error( 202 'D102', 203 'Missing docstring in public method', 204) 205D103 = D1xx.create_error( 206 'D103', 207 'Missing docstring in public function', 208) 209D104 = D1xx.create_error( 210 'D104', 211 'Missing docstring in public package', 212) 213D105 = D1xx.create_error( 214 'D105', 215 'Missing docstring in magic method', 216) 217D106 = D1xx.create_error( 218 'D106', 219 'Missing docstring in public nested class', 220) 221D107 = D1xx.create_error( 222 'D107', 223 'Missing docstring in __init__', 224) 225 226D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues') 227D200 = D2xx.create_error( 228 'D200', 229 'One-line docstring should fit on one line ' 'with quotes', 230 'found {0}', 231) 232D201 = D2xx.create_error( 233 'D201', 234 'No blank lines allowed before function docstring', 235 'found {0}', 236) 237D202 = D2xx.create_error( 238 'D202', 239 'No blank lines allowed after function docstring', 240 'found {0}', 241) 242D203 = D2xx.create_error( 243 'D203', 244 '1 blank line required before class docstring', 245 'found {0}', 246) 247D204 = D2xx.create_error( 248 'D204', 249 '1 blank line required after class docstring', 250 'found {0}', 251) 252D205 = D2xx.create_error( 253 'D205', 254 '1 blank line required between summary line and description', 255 'found {0}', 256) 257D206 = D2xx.create_error( 258 'D206', 259 'Docstring should be indented with spaces, not tabs', 260) 261D207 = D2xx.create_error( 262 'D207', 263 'Docstring is under-indented', 264) 265D208 = D2xx.create_error( 266 'D208', 267 'Docstring is over-indented', 268) 269D209 = D2xx.create_error( 270 'D209', 271 'Multi-line docstring closing quotes should be on a separate line', 272) 273D210 = D2xx.create_error( 274 'D210', 275 'No whitespaces allowed surrounding docstring text', 276) 277D211 = D2xx.create_error( 278 'D211', 279 'No blank lines allowed before class docstring', 280 'found {0}', 281) 282D212 = D2xx.create_error( 283 'D212', 284 'Multi-line docstring summary should start at the first line', 285) 286D213 = D2xx.create_error( 287 'D213', 288 'Multi-line docstring summary should start at the second line', 289) 290D214 = D2xx.create_error( 291 'D214', 292 'Section is over-indented', 293 '{0!r}', 294) 295D215 = D2xx.create_error( 296 'D215', 297 'Section underline is over-indented', 298 'in section {0!r}', 299) 300 301D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues') 302D300 = D3xx.create_error( 303 'D300', 304 'Use """triple double quotes"""', 305 'found {0}-quotes', 306) 307D301 = D3xx.create_error( 308 'D301', 309 'Use r""" if any backslashes in a docstring', 310) 311D302 = D3xx.create_error( 312 'D302', 313 'Deprecated: Use u""" for Unicode docstrings', 314) 315 316D4xx = ErrorRegistry.create_group('D4', 'Docstring Content Issues') 317D400 = D4xx.create_error( 318 'D400', 319 'First line should end with a period', 320 'not {0!r}', 321) 322D401 = D4xx.create_error( 323 'D401', 324 'First line should be in imperative mood', 325 "perhaps '{0}', not '{1}'", 326) 327D401b = D4xx.create_error( 328 'D401', 329 'First line should be in imperative mood; try rephrasing', 330 "found '{0}'", 331) 332D402 = D4xx.create_error( 333 'D402', 334 'First line should not be the function\'s "signature"', 335) 336D403 = D4xx.create_error( 337 'D403', 338 'First word of the first line should be properly capitalized', 339 '{0!r}, not {1!r}', 340) 341D404 = D4xx.create_error( 342 'D404', 343 'First word of the docstring should not be `This`', 344) 345D405 = D4xx.create_error( 346 'D405', 347 'Section name should be properly capitalized', 348 '{0!r}, not {1!r}', 349) 350D406 = D4xx.create_error( 351 'D406', 352 'Section name should end with a newline', 353 '{0!r}, not {1!r}', 354) 355D407 = D4xx.create_error( 356 'D407', 357 'Missing dashed underline after section', 358 '{0!r}', 359) 360D408 = D4xx.create_error( 361 'D408', 362 'Section underline should be in the line following the section\'s name', 363 '{0!r}', 364) 365D409 = D4xx.create_error( 366 'D409', 367 'Section underline should match the length of its name', 368 'Expected {0!r} dashes in section {1!r}, got {2!r}', 369) 370D410 = D4xx.create_error( 371 'D410', 372 'Missing blank line after section', 373 '{0!r}', 374) 375D411 = D4xx.create_error( 376 'D411', 377 'Missing blank line before section', 378 '{0!r}', 379) 380D412 = D4xx.create_error( 381 'D412', 382 'No blank lines allowed between a section header and its content', 383 '{0!r}', 384) 385D413 = D4xx.create_error( 386 'D413', 387 'Missing blank line after last section', 388 '{0!r}', 389) 390D414 = D4xx.create_error( 391 'D414', 392 'Section has no content', 393 '{0!r}', 394) 395D415 = D4xx.create_error( 396 'D415', 397 ( 398 'First line should end with a period, question ' 399 'mark, or exclamation point' 400 ), 401 'not {0!r}', 402) 403D416 = D4xx.create_error( 404 'D416', 405 'Section name should end with a colon', 406 '{0!r}, not {1!r}', 407) 408D417 = D4xx.create_error( 409 'D417', 410 'Missing argument descriptions in the docstring', 411 'argument(s) {0} are missing descriptions in {1!r} docstring', 412) 413 414D418 = D4xx.create_error( 415 'D418', 416 'Function/ Method decorated with @overload shouldn\'t contain a docstring', 417) 418 419 420class AttrDict(dict): 421 def __getattr__(self, item: str) -> Any: 422 return self[item] 423 424 425all_errors = set(ErrorRegistry.get_error_codes()) 426 427 428conventions = AttrDict( 429 { 430 'pep257': all_errors 431 - { 432 'D203', 433 'D212', 434 'D213', 435 'D214', 436 'D215', 437 'D404', 438 'D405', 439 'D406', 440 'D407', 441 'D408', 442 'D409', 443 'D410', 444 'D411', 445 'D413', 446 'D415', 447 'D416', 448 'D417', 449 'D418', 450 }, 451 'numpy': all_errors 452 - { 453 'D107', 454 'D203', 455 'D212', 456 'D213', 457 'D402', 458 'D413', 459 'D415', 460 'D416', 461 'D417', 462 }, 463 'google': all_errors 464 - { 465 'D203', 466 'D204', 467 'D213', 468 'D215', 469 'D400', 470 'D401', 471 'D404', 472 'D406', 473 'D407', 474 'D408', 475 'D409', 476 'D413', 477 }, 478 } 479) 480