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

Commit cb6f87c

Browse files
committed
Merge pull request #110 from Nurdok/errors-dry
DRY in error definitions
2 parents 7a2c452 + 7e5a017 commit cb6f87c

File tree

7 files changed

+185
-129
lines changed

7 files changed

+185
-129
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ docs/_*
4040

4141
# virtualenv
4242
venv/
43+
44+
# generated rst
45+
docs/snippets/error_code_table.rst

docs/conf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,10 @@
265265
# sphinxcontrib.issuetracker settings
266266
issuetracker = 'github'
267267
issuetracker_project = 'GreenSteam/pep257'
268+
269+
def generate_error_code_table():
270+
from ..pep257 import ErrorRegistry
271+
with open(os.path.join('snippets', 'error_code_table.rst'), 'wt') as outf:
272+
outf.write(ErrorRegistry.to_rst())
273+
274+
generate_error_code_table()

docs/snippets/error_code_table.rst

Lines changed: 0 additions & 51 deletions
This file was deleted.

pep257.py

Lines changed: 137 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -379,18 +379,34 @@ class Error(object):
379379

380380
"""Error in docstring style."""
381381

382+
# should be overridden by inheriting classes
383+
code = None
384+
short_desc = None
385+
context = None
386+
382387
# Options that define how errors are printed:
383388
explain = False
384389
source = False
385390

386-
def __init__(self, message=None, final=False):
387-
self.message, self.is_final = message, final
388-
self.definition, self.explanation = [None, None]
391+
def __init__(self, *parameters):
392+
self.parameters = parameters
393+
self.definition = None
394+
self.explanation = None
395+
396+
def set_context(self, definition, explanation):
397+
self.definition = definition
398+
self.explanation = explanation
389399

390-
code = property(lambda self: self.message.partition(':')[0])
391400
filename = property(lambda self: self.definition.module.name)
392401
line = property(lambda self: self.definition.start)
393402

403+
@property
404+
def message(self):
405+
ret = '%s: %s' % (self.code, self.short_desc)
406+
if self.context is not None:
407+
ret += ' (' + self.context % self.parameters + ')'
408+
return ret
409+
394410
@property
395411
def lines(self):
396412
source = ''
@@ -430,6 +446,100 @@ def __lt__(self, other):
430446
return (self.filename, self.line) < (other.filename, other.line)
431447

432448

449+
class ErrorRegistry(object):
450+
groups = []
451+
452+
class ErrorGroup(object):
453+
454+
def __init__(self, prefix, name):
455+
self.prefix = prefix
456+
self.name = name
457+
self.errors = []
458+
459+
def create_error(self, error_code, error_desc, error_context=None):
460+
# TODO: check prefix
461+
462+
class _Error(Error):
463+
code = error_code
464+
short_desc = error_desc
465+
context = error_context
466+
467+
self.errors.append(_Error)
468+
return _Error
469+
470+
@classmethod
471+
def create_group(cls, prefix, name):
472+
group = cls.ErrorGroup(prefix, name)
473+
cls.groups.append(group)
474+
return group
475+
476+
@classmethod
477+
def get_error_codes(cls):
478+
for group in cls.groups:
479+
for error in group.errors:
480+
yield error
481+
482+
@classmethod
483+
def to_rst(cls):
484+
sep_line = '+' + 6 * '-' + '+' + '-' * 71 + '+\n'
485+
blank_line = '|' + 78 * ' ' + '|\n'
486+
table = ''
487+
for group in cls.groups:
488+
table += sep_line
489+
table += blank_line
490+
table += '|' + ('**%s**' % group.name).center(78) + '|\n'
491+
table += blank_line
492+
for error in group.errors:
493+
table += sep_line
494+
table += ('|' + error.code.center(6) + '| ' +
495+
error.short_desc.ljust(70) + '|\n')
496+
table += sep_line
497+
return table
498+
499+
500+
D1xx = ErrorRegistry.create_group('D1', 'Missing Docstrings')
501+
D100 = D1xx.create_error('D100', 'Missing docstring in public module')
502+
D101 = D1xx.create_error('D101', 'Missing docstring in public class')
503+
D102 = D1xx.create_error('D102', 'Missing docstring in public method')
504+
D103 = D1xx.create_error('D103', 'Missing docstring in public function')
505+
506+
D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues')
507+
D200 = D2xx.create_error('D200', 'One-line docstring should fit on one line '
508+
'with quotes', 'found %s')
509+
D201 = D2xx.create_error('D201', 'No blank lines allowed before function '
510+
'docstring', 'found %s')
511+
D202 = D2xx.create_error('D202', 'No blank lines allowed after function '
512+
'docstring', 'found %s')
513+
D203 = D2xx.create_error('D203', '1 blank line required before class '
514+
'docstring', 'found %s')
515+
D204 = D2xx.create_error('D204', '1 blank line required after class '
516+
'docstring', 'found %s')
517+
D205 = D2xx.create_error('D205', '1 blank line required between summary line '
518+
'and description', 'found %s')
519+
D206 = D2xx.create_error('D206', 'Docstring should be indented with spaces, '
520+
'not tabs')
521+
D207 = D2xx.create_error('D207', 'Docstring is under-indented')
522+
D208 = D2xx.create_error('D208', 'Docstring is over-indented')
523+
D209 = D2xx.create_error('D209', 'Multi-line docstring closing quotes should '
524+
'be on a separate line')
525+
D210 = D2xx.create_error('D210', 'No whitespaces allowed surrounding '
526+
'docstring text')
527+
528+
D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues')
529+
D300 = D3xx.create_error('D300', 'Use """triple double quotes"""',
530+
'found %s-quotes')
531+
D301 = D3xx.create_error('D301', 'Use r""" if any backslashes in a docstring')
532+
D302 = D3xx.create_error('D302', 'Use u""" for Unicode docstrings')
533+
534+
D4xx = ErrorRegistry.create_group('D4', 'Docstring Content Issues')
535+
D400 = D4xx.create_error('D400', 'First line should end with a period',
536+
'not %r')
537+
D401 = D4xx.create_error('D401', 'First line should be in imperative mood',
538+
'%r, not %r')
539+
D402 = D4xx.create_error('D402', 'First line should not be the function\'s '
540+
'"signature"')
541+
542+
433543
def get_option_parser():
434544
parser = OptionParser(version=__version__,
435545
usage='Usage: pep257 [options] [<file|dir>...]')
@@ -634,10 +744,8 @@ def check_source(self, source, filename):
634744
if error is not None:
635745
partition = check.__doc__.partition('.\n')
636746
message, _, explanation = partition
637-
if error.message is None:
638-
error.message = message
639-
error.explanation = explanation
640-
error.definition = definition
747+
error.set_context(explanation=explanation,
748+
definition=definition)
641749
yield error
642750
if check._terminal:
643751
terminate = True
@@ -667,9 +775,9 @@ def check_docstring_missing(self, definition, docstring):
667775
"""
668776
if (not docstring and definition.is_public or
669777
docstring and is_blank(eval(docstring))):
670-
codes = {Module: 'D100', Class: 'D101', NestedClass: 'D101',
671-
Method: 'D102', Function: 'D103', NestedFunction: 'D103'}
672-
return Error('%s: Docstring missing' % codes[type(definition)])
778+
codes = {Module: D100, Class: D101, NestedClass: D101,
779+
Method: D102, Function: D103, NestedFunction: D103}
780+
return codes[type(definition)]()
673781

674782
@check_for(Definition)
675783
def check_one_liners(self, definition, docstring):
@@ -684,8 +792,7 @@ def check_one_liners(self, definition, docstring):
684792
if len(lines) > 1:
685793
non_empty_lines = sum(1 for l in lines if not is_blank(l))
686794
if non_empty_lines == 1:
687-
return Error('D200: One-line docstring should not occupy '
688-
'%s lines' % len(lines))
795+
return D200(len(lines))
689796

690797
@check_for(Function)
691798
def check_no_blank_before(self, function, docstring): # def
@@ -703,13 +810,9 @@ def check_no_blank_before(self, function, docstring): # def
703810
blanks_before_count = sum(takewhile(bool, reversed(blanks_before)))
704811
blanks_after_count = sum(takewhile(bool, blanks_after))
705812
if blanks_before_count != 0:
706-
yield Error('D201: No blank lines allowed *before* %s '
707-
'docstring, found %s'
708-
% (function.kind, blanks_before_count))
813+
yield D201(blanks_before_count)
709814
if not all(blanks_after) and blanks_after_count != 0:
710-
yield Error('D202: No blank lines allowed *after* %s '
711-
'docstring, found %s'
712-
% (function.kind, blanks_after_count))
815+
yield D202(blanks_after_count)
713816

714817
@check_for(Class)
715818
def check_blank_before_after_class(slef, class_, docstring):
@@ -723,7 +826,7 @@ def check_blank_before_after_class(slef, class_, docstring):
723826
docstring.
724827
725828
"""
726-
# NOTE: this gives flase-positive in this case
829+
# NOTE: this gives false-positive in this case
727830
# class Foo:
728831
#
729832
# """Docstring."""
@@ -738,11 +841,9 @@ def check_blank_before_after_class(slef, class_, docstring):
738841
blanks_before_count = sum(takewhile(bool, reversed(blanks_before)))
739842
blanks_after_count = sum(takewhile(bool, blanks_after))
740843
if blanks_before_count != 1:
741-
yield Error('D203: Expected 1 blank line *before* class '
742-
'docstring, found %s' % blanks_before_count)
844+
yield D203(blanks_before_count)
743845
if not all(blanks_after) and blanks_after_count != 1:
744-
yield Error('D204: Expected 1 blank line *after* class '
745-
'docstring, found %s' % blanks_after_count)
846+
yield D204(blanks_after_count)
746847

747848
@check_for(Definition)
748849
def check_blank_after_summary(self, definition, docstring):
@@ -761,9 +862,7 @@ def check_blank_after_summary(self, definition, docstring):
761862
post_summary_blanks = list(map(is_blank, lines[1:]))
762863
blanks_count = sum(takewhile(bool, post_summary_blanks))
763864
if blanks_count != 1:
764-
yield Error('D205: Expected 1 blank line between summary '
765-
'line and description, found %s' %
766-
blanks_count)
865+
return D205(blanks_count)
767866

768867
@check_for(Definition)
769868
def check_indent(self, definition, docstring):
@@ -781,13 +880,12 @@ def check_indent(self, definition, docstring):
781880
lines = lines[1:] # First line does not need indent.
782881
indents = [leading_space(l) for l in lines if not is_blank(l)]
783882
if set(' \t') == set(''.join(indents) + indent):
784-
return Error('D206: Docstring indented with both tabs and '
785-
'spaces')
883+
yield D206()
786884
if (len(indents) > 1 and min(indents[:-1]) > indent or
787885
indents[-1] > indent):
788-
return Error('D208: Docstring is over-indented')
886+
yield D208()
789887
if min(indents) < indent:
790-
return Error('D207: Docstring is under-indented')
888+
yield D207()
791889

792890
@check_for(Definition)
793891
def check_newline_after_last_paragraph(self, definition, docstring):
@@ -801,8 +899,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
801899
lines = [l for l in eval(docstring).split('\n') if not is_blank(l)]
802900
if len(lines) > 1:
803901
if docstring.split("\n")[-1].strip() not in ['"""', "'''"]:
804-
return Error('D209: Put multi-line docstring closing '
805-
'quotes on separate line')
902+
return D209()
806903

807904
@check_for(Definition)
808905
def check_surrounding_whitespaces(self, definition, docstring):
@@ -811,8 +908,7 @@ def check_surrounding_whitespaces(self, definition, docstring):
811908
lines = eval(docstring).split('\n')
812909
if lines[0].startswith(' ') or \
813910
len(lines) == 1 and lines[0].endswith(' '):
814-
return Error("D210: No whitespaces allowed surrounding "
815-
"docstring text.")
911+
return D210()
816912

817913
@check_for(Definition)
818914
def check_triple_double_quotes(self, definition, docstring):
@@ -834,7 +930,7 @@ def check_triple_double_quotes(self, definition, docstring):
834930
return
835931
if docstring and not docstring.startswith(('"""', 'r"""', 'u"""')):
836932
quotes = "'''" if "'''" in docstring[:4] else "'"
837-
return Error('D300: Expected """-quotes, got %s-quotes' % quotes)
933+
return D300(quotes)
838934

839935
@check_for(Definition)
840936
def check_backslashes(self, definition, docstring):
@@ -847,7 +943,7 @@ def check_backslashes(self, definition, docstring):
847943
# Just check that docstring is raw, check_triple_double_quotes
848944
# ensures the correct quotes.
849945
if docstring and '\\' in docstring and not docstring.startswith('r'):
850-
return Error()
946+
return D301()
851947

852948
@check_for(Definition)
853949
def check_unicode_docstring(self, definition, docstring):
@@ -860,7 +956,7 @@ def check_unicode_docstring(self, definition, docstring):
860956
# ensures the correct quotes.
861957
if docstring and sys.version_info[0] <= 2:
862958
if not is_ascii(docstring) and not docstring.startswith('u'):
863-
return Error()
959+
return D302()
864960

865961
@check_for(Definition)
866962
def check_ends_with_period(self, definition, docstring):
@@ -872,8 +968,7 @@ def check_ends_with_period(self, definition, docstring):
872968
if docstring:
873969
summary_line = eval(docstring).strip().split('\n')[0]
874970
if not summary_line.endswith('.'):
875-
return Error("D400: First line should end with '.', not %r"
876-
% summary_line[-1])
971+
return D400(summary_line[-1])
877972

878973
@check_for(Function)
879974
def check_imperative_mood(self, function, docstring): # def context
@@ -889,8 +984,7 @@ def check_imperative_mood(self, function, docstring): # def context
889984
if stripped:
890985
first_word = stripped.split()[0]
891986
if first_word.endswith('s') and not first_word.endswith('ss'):
892-
return Error('D401: First line should be imperative: '
893-
'%r, not %r' % (first_word[:-1], first_word))
987+
return D401(first_word[:-1], first_word)
894988

895989
@check_for(Function)
896990
def check_no_signature(self, function, docstring): # def context
@@ -903,8 +997,7 @@ def check_no_signature(self, function, docstring): # def context
903997
if docstring:
904998
first_line = eval(docstring).strip().split('\n')[0]
905999
if function.name + '(' in first_line.replace(' ', ''):
906-
return Error("D402: First line should not be %s's signature"
907-
% function.kind)
1000+
return D402()
9081001

9091002
# Somewhat hard to determine if return value is mentioned.
9101003
# @check(Function)

0 commit comments

Comments
 (0)