18
18
from re import compile as re
19
19
import itertools
20
20
from collections import defaultdict , namedtuple , Set
21
+ from functools import partial
21
22
22
23
try : # Python 3.x
23
24
from ConfigParser import RawConfigParser
@@ -119,6 +120,7 @@ class Definition(Value):
119
120
all = property (lambda self : self .module .all )
120
121
_slice = property (lambda self : slice (self .start - 1 , self .end ))
121
122
is_class = False
123
+ is_function = False
122
124
123
125
def __iter__ (self ):
124
126
return chain ([self ], * self .children )
@@ -161,6 +163,9 @@ class Package(Module):
161
163
162
164
class Function (Definition ):
163
165
166
+ is_function = True
167
+ _fields = ('name' , '_source' , 'start' , 'end' , 'decorators' , 'docstring' ,
168
+ 'children' , 'parent' , 'parameters' )
164
169
_nest = staticmethod (lambda s : {'def' : NestedFunction ,
165
170
'class' : NestedClass }[s ])
166
171
@@ -448,6 +453,8 @@ def parse_definition(self, class_):
448
453
self .stream .move ()
449
454
if self .current .kind == tk .OP and self .current .value == '(' :
450
455
parenthesis_level = 0
456
+ arguments = []
457
+ is_default_assignment = False
451
458
while True :
452
459
if self .current .kind == tk .OP :
453
460
if self .current .value == '(' :
@@ -456,6 +463,14 @@ def parse_definition(self, class_):
456
463
parenthesis_level -= 1
457
464
if parenthesis_level == 0 :
458
465
break
466
+ elif self .current .value == '=' :
467
+ is_default_assignment = True
468
+ elif self .current .kind == tk .NAME :
469
+ if is_default_assignment :
470
+ is_default_assignment = False
471
+ else :
472
+ arguments .append (self .current .value )
473
+
459
474
self .stream .move ()
460
475
if self .current .kind != tk .OP or self .current .value != ':' :
461
476
self .leapfrog (tk .OP , value = ":" )
@@ -477,8 +492,11 @@ def parse_definition(self, class_):
477
492
children = []
478
493
end = self .line
479
494
self .leapfrog (tk .NEWLINE )
480
- definition = class_ (name , self .source , start , end ,
481
- decorators , docstring , children , None )
495
+
496
+ creator = partial (class_ , name , self .source , start , end ,
497
+ decorators , docstring , children , None )
498
+
499
+ definition = creator (arguments ) if class_ .is_function else creator ()
482
500
for child in definition .children :
483
501
child .parent = definition
484
502
log .debug ("finished parsing %s '%s'. Next token is %r (%s)" ,
@@ -683,6 +701,8 @@ def to_rst(cls):
683
701
'at the first line' )
684
702
D213 = D2xx .create_error ('D213' , 'Multi-line docstring summary should start '
685
703
'at the second line' )
704
+ D214 = D2xx .create_error ('D214' , 'Section or section underline is '
705
+ 'over-indented' , 'in section %r' )
686
706
687
707
D3xx = ErrorRegistry .create_group ('D3' , 'Quotes Issues' )
688
708
D300 = D3xx .create_error ('D300' , 'Use """triple double quotes"""' ,
@@ -701,6 +721,12 @@ def to_rst(cls):
701
721
'properly capitalized' , '%r, not %r' )
702
722
D404 = D4xx .create_error ('D404' , 'First word of the docstring should not '
703
723
'be `This`' )
724
+ D405 = D4xx .create_error ('D405' , 'Section name should be properly capitalized' ,
725
+ '%r, not %r' )
726
+ D406 = D4xx .create_error ('D406' , 'Section name should not end with a colon' ,
727
+ '%r, not %r' )
728
+ D407 = D4xx .create_error ('D407' , 'Section underline should match the length '
729
+ 'of the section\' s name' , 'len(%r) == %r' )
704
730
705
731
706
732
class AttrDict (dict ):
@@ -709,10 +735,12 @@ def __getattr__(self, item):
709
735
710
736
711
737
conventions = AttrDict ({
712
- 'pep257' : set (ErrorRegistry .get_error_codes ()) - set (['D203' ,
713
- 'D212' ,
714
- 'D213' ,
715
- 'D404' ])
738
+ 'pep257' : set (ErrorRegistry .get_error_codes ()) - set (['D203' , 'D212' ,
739
+ 'D213' , 'D214' ,
740
+ 'D404' , 'D405' ,
741
+ 'D406' , 'D407' ]),
742
+ 'numpy' : set (ErrorRegistry .get_error_codes ()) - set (['D203' , 'D212' ,
743
+ 'D213' , 'D402' ])
716
744
})
717
745
718
746
@@ -1264,7 +1292,7 @@ def check(filenames, select=None, ignore=None):
1264
1292
try :
1265
1293
with tokenize_open (filename ) as file :
1266
1294
source = file .read ()
1267
- for error in PEP257Checker ().check_source (source , filename ):
1295
+ for error in ConventionChecker ().check_source (source , filename ):
1268
1296
code = getattr (error , 'code' , None )
1269
1297
if code in checked_codes :
1270
1298
yield error
@@ -1354,7 +1382,7 @@ def decorator(f):
1354
1382
return decorator
1355
1383
1356
1384
1357
- class PEP257Checker (object ):
1385
+ class ConventionChecker (object ):
1358
1386
"""Checker for PEP 257.
1359
1387
1360
1388
D10x: Missing docstrings
@@ -1364,13 +1392,27 @@ class PEP257Checker(object):
1364
1392
1365
1393
"""
1366
1394
1395
+ ALL_NUMPY_SECTIONS = ['Short Summary' ,
1396
+ 'Extended Summary' ,
1397
+ 'Parameters' ,
1398
+ 'Returns' ,
1399
+ 'Yields' ,
1400
+ 'Other Parameters' ,
1401
+ 'Raises' ,
1402
+ 'See Also' ,
1403
+ 'Notes' ,
1404
+ 'References' ,
1405
+ 'Examples' ,
1406
+ 'Attributes' ,
1407
+ 'Methods' ]
1408
+
1367
1409
def check_source (self , source , filename ):
1368
1410
module = parse (StringIO (source ), filename )
1369
1411
for definition in module :
1370
1412
for check in self .checks :
1371
1413
terminate = False
1372
1414
if isinstance (definition , check ._check_for ):
1373
- error = check (None , definition , definition .docstring )
1415
+ error = check (self , definition , definition .docstring )
1374
1416
errors = error if hasattr (error , '__iter__' ) else [error ]
1375
1417
for error in errors :
1376
1418
if error is not None :
@@ -1498,6 +1540,13 @@ def check_blank_after_summary(self, definition, docstring):
1498
1540
if blanks_count != 1 :
1499
1541
return D205 (blanks_count )
1500
1542
1543
+ @staticmethod
1544
+ def _get_docstring_indent (definition , docstring ):
1545
+ """Return the indentation of the docstring's opening quotes."""
1546
+ before_docstring , _ , _ = definition .source .partition (docstring )
1547
+ _ , _ , indent = before_docstring .rpartition ('\n ' )
1548
+ return indent
1549
+
1501
1550
@check_for (Definition )
1502
1551
def check_indent (self , definition , docstring ):
1503
1552
"""D20{6,7,8}: The entire docstring should be indented same as code.
@@ -1507,8 +1556,7 @@ def check_indent(self, definition, docstring):
1507
1556
1508
1557
"""
1509
1558
if docstring :
1510
- before_docstring , _ , _ = definition .source .partition (docstring )
1511
- _ , _ , indent = before_docstring .rpartition ('\n ' )
1559
+ indent = self ._get_docstring_indent (definition , docstring )
1512
1560
lines = docstring .split ('\n ' )
1513
1561
if len (lines ) > 1 :
1514
1562
lines = lines [1 :] # First line does not need indent.
@@ -1709,6 +1757,145 @@ def SKIP_check_return_type(self, function, docstring):
1709
1757
if 'return' not in docstring .lower ():
1710
1758
return Error ()
1711
1759
1760
+ @check_for (Definition )
1761
+ def check_numpy_content (self , definition , docstring ):
1762
+ """Check the content of the docstring for numpy conventions."""
1763
+ pass
1764
+
1765
+ def check_numpy_parameters (self , section , content , definition , docstring ):
1766
+ print "LALALAL"
1767
+ yield
1768
+
1769
+ def _check_numpy_section (self , section , content , definition , docstring ):
1770
+ """Check the content of the docstring for numpy conventions."""
1771
+ method_name = "check_numpy_%s" % section
1772
+ if hasattr (self , method_name ):
1773
+ gen_func = getattr (self , method_name )
1774
+
1775
+ for err in gen_func (section , content , definition , docstring ):
1776
+ yield err
1777
+ else :
1778
+ print "Now checking numpy section %s" % section
1779
+ for l in content :
1780
+ print "##" , l
1781
+
1782
+
1783
+ @check_for (Definition )
1784
+ def check_numpy (self , definition , docstring ):
1785
+ """Parse the general structure of a numpy docstring and check it."""
1786
+ if not docstring :
1787
+ return
1788
+
1789
+ lines = docstring .split ("\n " )
1790
+ if len (lines ) < 2 :
1791
+ # It's not a multiple lined docstring
1792
+ return
1793
+
1794
+ lines_generator = ScrollableGenerator (lines [1 :]) # Skipping first line
1795
+ indent = self ._get_docstring_indent (definition , docstring )
1796
+
1797
+ current_section = None
1798
+ curr_section_lines = []
1799
+ start_collecting_lines = False
1800
+
1801
+ for line in lines_generator :
1802
+ for section in self .ALL_NUMPY_SECTIONS :
1803
+ with_colon = section .lower () + ':'
1804
+ if line .strip ().lower () in [section .lower (), with_colon ]:
1805
+ # There's a chance that this line is a numpy section
1806
+ try :
1807
+ next_line = lines_generator .next ()
1808
+ except StopIteration :
1809
+ # It probably isn't :)
1810
+ return
1811
+
1812
+ if '' .join (set (next_line .strip ())) == '-' :
1813
+ # The next line contains only dashes, there's a good
1814
+ # chance that it's a numpy section
1815
+
1816
+ if (leading_space (line ) > indent or
1817
+ leading_space (next_line ) > indent ):
1818
+ yield D214 (section )
1819
+
1820
+ if section not in line :
1821
+ # The capitalized section string is not in the line,
1822
+ # meaning that the word appears there but not
1823
+ # properly capitalized.
1824
+ yield D405 (section , line .strip ())
1825
+ elif line .strip ().lower () == with_colon :
1826
+ # The section name should not end with a colon.
1827
+ yield D406 (section , line .strip ())
1828
+
1829
+ if next_line .strip () != "-" * len (section ):
1830
+ # The length of the underlining dashes does not
1831
+ # match the length of the section name.
1832
+ yield D407 (section , len (section ))
1833
+
1834
+ # At this point, we're done with the structured part of
1835
+ # the section and its underline.
1836
+ # We will not collect the content of each section and
1837
+ # let section handlers deal with it.
1838
+
1839
+ if current_section is not None :
1840
+ for err in self ._check_numpy_section (
1841
+ current_section ,
1842
+ curr_section_lines ,
1843
+ definition ,
1844
+ docstring ):
1845
+ yield err
1846
+
1847
+ start_collecting_lines = True
1848
+ current_section = section .lower ()
1849
+ curr_section_lines = []
1850
+ else :
1851
+ # The next line does not contain only dashes, so it's
1852
+ # not likely to be a section header.
1853
+ lines_generator .scroll_back ()
1854
+
1855
+ if current_section is not None :
1856
+ if start_collecting_lines :
1857
+ start_collecting_lines = False
1858
+ else :
1859
+ curr_section_lines .append (line )
1860
+
1861
+ if current_section is not None :
1862
+ for err in self ._check_numpy_section (current_section ,
1863
+ curr_section_lines ,
1864
+ definition ,
1865
+ docstring ):
1866
+ yield err
1867
+
1868
+
1869
+ class ScrollableGenerator (object ):
1870
+ """A generator over a list that can be moved back during iteration."""
1871
+
1872
+ def __init__ (self , list_like ):
1873
+ self ._list_like = list_like
1874
+ self ._index = 0
1875
+
1876
+ def __iter__ (self ):
1877
+ return self
1878
+
1879
+ def next (self ):
1880
+ """Generate the next item or raise StopIteration."""
1881
+ try :
1882
+ return self ._list_like [self ._index ]
1883
+ except IndexError :
1884
+ raise StopIteration ()
1885
+ finally :
1886
+ self ._index += 1
1887
+
1888
+ def scroll_back (self , num = 1 ):
1889
+ """Move the generator `num` items backwards."""
1890
+ if num < 0 :
1891
+ raise ValueError ('num cannot be a negative number' )
1892
+ self ._index = max (0 , self ._index - num )
1893
+
1894
+ def clone (self ):
1895
+ """Return a copy of the generator set to the same item index."""
1896
+ obj_copy = self .__class__ (self ._list_like )
1897
+ obj_copy ._index = self ._index
1898
+
1712
1899
1713
1900
def main (use_pep257 = False ):
1714
1901
try :
@@ -1721,5 +1908,19 @@ def main_pep257():
1721
1908
main (use_pep257 = True )
1722
1909
1723
1910
1911
+ def foo ():
1912
+ """A.
1913
+
1914
+ Parameters
1915
+ ----------
1916
+
1917
+ This is a string that defines some things.
1918
+
1919
+ Returns
1920
+ -------
1921
+
1922
+ Nothing.
1923
+ """
1924
+
1724
1925
if __name__ == '__main__' :
1725
1926
main ()
0 commit comments