18
18
import os
19
19
import re
20
20
from typing import (
21
+ TYPE_CHECKING ,
21
22
Dict ,
22
23
List ,
23
24
Optional ,
30
31
from .source import QAPISourceInfo
31
32
32
33
34
+ if TYPE_CHECKING :
35
+ # pylint: disable=cyclic-import
36
+ # TODO: Remove cycle. [schema -> expr -> parser -> schema]
37
+ from .schema import QAPISchemaFeature , QAPISchemaMember
38
+
39
+
40
+ #: Represents a single Top Level QAPI schema expression.
41
+ TopLevelExpr = Dict [str , object ]
42
+
33
43
# Return value alias for get_expr().
34
44
_ExprValue = Union [List [object ], Dict [str , object ], str , bool ]
35
45
46
+ # FIXME: Consolidate and centralize definitions for TopLevelExpr,
47
+ # _ExprValue, _JSONValue, and _JSONObject; currently scattered across
48
+ # several modules.
49
+
36
50
37
51
class QAPIParseError (QAPISourceError ):
38
52
"""Error class for all QAPI schema parsing errors."""
@@ -447,7 +461,10 @@ class QAPIDoc:
447
461
"""
448
462
449
463
class Section :
450
- def __init__ (self , parser , name = None , indent = 0 ):
464
+ # pylint: disable=too-few-public-methods
465
+ def __init__ (self , parser : QAPISchemaParser ,
466
+ name : Optional [str ] = None , indent : int = 0 ):
467
+
451
468
# parser, for error messages about indentation
452
469
self ._parser = parser
453
470
# optional section name (argument/member or section name)
@@ -456,7 +473,7 @@ def __init__(self, parser, name=None, indent=0):
456
473
# the expected indent level of the text of this section
457
474
self ._indent = indent
458
475
459
- def append (self , line ) :
476
+ def append (self , line : str ) -> None :
460
477
# Strip leading spaces corresponding to the expected indent level
461
478
# Blank lines are always OK.
462
479
if line :
@@ -471,39 +488,47 @@ def append(self, line):
471
488
self .text += line .rstrip () + '\n '
472
489
473
490
class ArgSection (Section ):
474
- def __init__ (self , parser , name , indent = 0 ):
491
+ def __init__ (self , parser : QAPISchemaParser ,
492
+ name : str , indent : int = 0 ):
475
493
super ().__init__ (parser , name , indent )
476
- self .member = None
494
+ self .member : Optional [ 'QAPISchemaMember' ] = None
477
495
478
- def connect (self , member ) :
496
+ def connect (self , member : 'QAPISchemaMember' ) -> None :
479
497
self .member = member
480
498
481
- def __init__ (self , parser , info ):
499
+ class NullSection (Section ):
500
+ """
501
+ Immutable dummy section for use at the end of a doc block.
502
+ """
503
+ # pylint: disable=too-few-public-methods
504
+ def append (self , line : str ) -> None :
505
+ assert False , "Text appended after end_comment() called."
506
+
507
+ def __init__ (self , parser : QAPISchemaParser , info : QAPISourceInfo ):
482
508
# self._parser is used to report errors with QAPIParseError. The
483
509
# resulting error position depends on the state of the parser.
484
510
# It happens to be the beginning of the comment. More or less
485
511
# servicable, but action at a distance.
486
512
self ._parser = parser
487
513
self .info = info
488
- self .symbol = None
514
+ self .symbol : Optional [ str ] = None
489
515
self .body = QAPIDoc .Section (parser )
490
- # dict mapping parameter name to ArgSection
491
- self .args = OrderedDict ()
492
- self .features = OrderedDict ()
493
- # a list of Section
494
- self .sections = []
516
+ # dicts mapping parameter/feature names to their ArgSection
517
+ self .args : Dict [str , QAPIDoc .ArgSection ] = OrderedDict ()
518
+ self .features : Dict [str , QAPIDoc .ArgSection ] = OrderedDict ()
519
+ self .sections : List [QAPIDoc .Section ] = []
495
520
# the current section
496
521
self ._section = self .body
497
522
self ._append_line = self ._append_body_line
498
523
499
- def has_section (self , name ) :
524
+ def has_section (self , name : str ) -> bool :
500
525
"""Return True if we have a section with this name."""
501
526
for i in self .sections :
502
527
if i .name == name :
503
528
return True
504
529
return False
505
530
506
- def append (self , line ) :
531
+ def append (self , line : str ) -> None :
507
532
"""
508
533
Parse a comment line and add it to the documentation.
509
534
@@ -524,18 +549,18 @@ def append(self, line):
524
549
line = line [1 :]
525
550
self ._append_line (line )
526
551
527
- def end_comment (self ):
528
- self ._end_section ( )
552
+ def end_comment (self ) -> None :
553
+ self ._switch_section ( QAPIDoc . NullSection ( self . _parser ) )
529
554
530
555
@staticmethod
531
- def _is_section_tag (name ) :
556
+ def _is_section_tag (name : str ) -> bool :
532
557
return name in ('Returns:' , 'Since:' ,
533
558
# those are often singular or plural
534
559
'Note:' , 'Notes:' ,
535
560
'Example:' , 'Examples:' ,
536
561
'TODO:' )
537
562
538
- def _append_body_line (self , line ) :
563
+ def _append_body_line (self , line : str ) -> None :
539
564
"""
540
565
Process a line of documentation text in the body section.
541
566
@@ -556,9 +581,11 @@ def _append_body_line(self, line):
556
581
if not line .endswith (':' ):
557
582
raise QAPIParseError (self ._parser , "line should end with ':'" )
558
583
self .symbol = line [1 :- 1 ]
559
- # FIXME invalid names other than the empty string aren't flagged
584
+ # Invalid names are not checked here, but the name provided MUST
585
+ # match the following definition, which *is* validated in expr.py.
560
586
if not self .symbol :
561
- raise QAPIParseError (self ._parser , "invalid name" )
587
+ raise QAPIParseError (
588
+ self ._parser , "name required after '@'" )
562
589
elif self .symbol :
563
590
# This is a definition documentation block
564
591
if name .startswith ('@' ) and name .endswith (':' ):
@@ -575,7 +602,7 @@ def _append_body_line(self, line):
575
602
# This is a free-form documentation block
576
603
self ._append_freeform (line )
577
604
578
- def _append_args_line (self , line ) :
605
+ def _append_args_line (self , line : str ) -> None :
579
606
"""
580
607
Process a line of documentation text in an argument section.
581
608
@@ -621,7 +648,7 @@ def _append_args_line(self, line):
621
648
622
649
self ._append_freeform (line )
623
650
624
- def _append_features_line (self , line ) :
651
+ def _append_features_line (self , line : str ) -> None :
625
652
name = line .split (' ' , 1 )[0 ]
626
653
627
654
if name .startswith ('@' ) and name .endswith (':' ):
@@ -653,7 +680,7 @@ def _append_features_line(self, line):
653
680
654
681
self ._append_freeform (line )
655
682
656
- def _append_various_line (self , line ) :
683
+ def _append_various_line (self , line : str ) -> None :
657
684
"""
658
685
Process a line of documentation text in an additional section.
659
686
@@ -689,80 +716,95 @@ def _append_various_line(self, line):
689
716
690
717
self ._append_freeform (line )
691
718
692
- def _start_symbol_section (self , symbols_dict , name , indent ):
719
+ def _start_symbol_section (
720
+ self ,
721
+ symbols_dict : Dict [str , 'QAPIDoc.ArgSection' ],
722
+ name : str ,
723
+ indent : int ) -> None :
693
724
# FIXME invalid names other than the empty string aren't flagged
694
725
if not name :
695
726
raise QAPIParseError (self ._parser , "invalid parameter name" )
696
727
if name in symbols_dict :
697
728
raise QAPIParseError (self ._parser ,
698
729
"'%s' parameter name duplicated" % name )
699
730
assert not self .sections
700
- self . _end_section ( )
701
- self ._section = QAPIDoc . ArgSection ( self . _parser , name , indent )
702
- symbols_dict [name ] = self . _section
731
+ new_section = QAPIDoc . ArgSection ( self . _parser , name , indent )
732
+ self ._switch_section ( new_section )
733
+ symbols_dict [name ] = new_section
703
734
704
- def _start_args_section (self , name , indent ) :
735
+ def _start_args_section (self , name : str , indent : int ) -> None :
705
736
self ._start_symbol_section (self .args , name , indent )
706
737
707
- def _start_features_section (self , name , indent ) :
738
+ def _start_features_section (self , name : str , indent : int ) -> None :
708
739
self ._start_symbol_section (self .features , name , indent )
709
740
710
- def _start_section (self , name = None , indent = 0 ):
741
+ def _start_section (self , name : Optional [str ] = None ,
742
+ indent : int = 0 ) -> None :
711
743
if name in ('Returns' , 'Since' ) and self .has_section (name ):
712
744
raise QAPIParseError (self ._parser ,
713
745
"duplicated '%s' section" % name )
714
- self ._end_section ()
715
- self ._section = QAPIDoc .Section (self ._parser , name , indent )
716
- self .sections .append (self ._section )
717
-
718
- def _end_section (self ):
719
- if self ._section :
720
- text = self ._section .text = self ._section .text .strip ()
721
- if self ._section .name and (not text or text .isspace ()):
722
- raise QAPIParseError (
723
- self ._parser ,
724
- "empty doc section '%s'" % self ._section .name )
725
- self ._section = None
746
+ new_section = QAPIDoc .Section (self ._parser , name , indent )
747
+ self ._switch_section (new_section )
748
+ self .sections .append (new_section )
749
+
750
+ def _switch_section (self , new_section : 'QAPIDoc.Section' ) -> None :
751
+ text = self ._section .text = self ._section .text .strip ()
752
+
753
+ # Only the 'body' section is allowed to have an empty body.
754
+ # All other sections, including anonymous ones, must have text.
755
+ if self ._section != self .body and not text :
756
+ # We do not create anonymous sections unless there is
757
+ # something to put in them; this is a parser bug.
758
+ assert self ._section .name
759
+ raise QAPIParseError (
760
+ self ._parser ,
761
+ "empty doc section '%s'" % self ._section .name )
762
+
763
+ self ._section = new_section
726
764
727
- def _append_freeform (self , line ) :
765
+ def _append_freeform (self , line : str ) -> None :
728
766
match = re .match (r'(@\S+:)' , line )
729
767
if match :
730
768
raise QAPIParseError (self ._parser ,
731
769
"'%s' not allowed in free-form documentation"
732
770
% match .group (1 ))
733
771
self ._section .append (line )
734
772
735
- def connect_member (self , member ) :
773
+ def connect_member (self , member : 'QAPISchemaMember' ) -> None :
736
774
if member .name not in self .args :
737
775
# Undocumented TODO outlaw
738
776
self .args [member .name ] = QAPIDoc .ArgSection (self ._parser ,
739
777
member .name )
740
778
self .args [member .name ].connect (member )
741
779
742
- def connect_feature (self , feature ) :
780
+ def connect_feature (self , feature : 'QAPISchemaFeature' ) -> None :
743
781
if feature .name not in self .features :
744
782
raise QAPISemError (feature .info ,
745
783
"feature '%s' lacks documentation"
746
784
% feature .name )
747
785
self .features [feature .name ].connect (feature )
748
786
749
- def check_expr (self , expr ) :
787
+ def check_expr (self , expr : TopLevelExpr ) -> None :
750
788
if self .has_section ('Returns' ) and 'command' not in expr :
751
789
raise QAPISemError (self .info ,
752
790
"'Returns:' is only valid for commands" )
753
791
754
- def check (self ):
792
+ def check (self ) -> None :
755
793
756
- def check_args_section (args , info , what ):
794
+ def check_args_section (
795
+ args : Dict [str , QAPIDoc .ArgSection ], what : str
796
+ ) -> None :
757
797
bogus = [name for name , section in args .items ()
758
798
if not section .member ]
759
799
if bogus :
760
800
raise QAPISemError (
761
801
self .info ,
762
- "documented member%s '%s' %s not exist"
763
- % ("s" if len (bogus ) > 1 else "" ,
764
- "', '" .join (bogus ),
765
- "do" if len (bogus ) > 1 else "does" ))
766
-
767
- check_args_section (self .args , self .info , 'members' )
768
- check_args_section (self .features , self .info , 'features' )
802
+ "documented %s%s '%s' %s not exist" % (
803
+ what ,
804
+ "s" if len (bogus ) > 1 else "" ,
805
+ "', '" .join (bogus ),
806
+ "do" if len (bogus ) > 1 else "does"
807
+ ))
808
+
809
+ check_args_section (self .args , 'member' )
810
+ check_args_section (self .features , 'feature' )
0 commit comments