Не удается исправить ошибку pyparsing…

обзор

Итак, я в середине рефакторинга проекта, и я отделяю кучу кода синтаксического анализа. Код, который меня интересует, - это pyparsing.

у меня очень плохое понимание pyparsing, даже после того, как я потратил много времени на чтение официальной документации. У меня проблемы, потому что (1) pyparsing использует (намеренно) неортодоксальный подход к синтаксическому анализу, и (2) я работаю над кодом, который я не писал, с плохими комментариями и неэлементарный набор существующих грамматик.

(я тоже не могу связаться с оригинальным автором.)

Непройденного Теста

Я использую PyVows чтобы проверить мой код. Один из моих тестов выглядит следующим образом (я думаю, это ясно, даже если вы не знакомы с PyVows; дайте мне знать, если это не так):

def test_multiline_command_ends(self, topic):
                output = parsed_input('multiline command endsnn',topic)
                expect(output).to_equal(
r'''['multiline', 'command ends', 'n', 'n']
- args: command ends
- multiline_command: multiline
- statement: ['multiline', 'command ends', 'n', 'n']
  - args: command ends
  - multiline_command: multiline
  - terminator: ['n', 'n']
- terminator: ['n', 'n']''')

но когда я запускаю тест, я получаю следующее в терминале:

Неудачный Тест Результаты

Expected topic("['multiline', 'command ends']n- args: command endsn- command: multilinen- statement: ['multiline', 'command ends']n  - args: command endsn  - command: multiline") 
      to equal "['multiline', 'command ends', 'n', 'n']n- args: command endsn- multiline_command: multilinen- statement: ['multiline', 'command ends', 'n', 'n']n  - args: command endsn  - multiline_command: multilinen  - terminator: ['n', 'n']n- terminator: ['n', 'n']"


Примечание:

поскольку выход на терминал, ожидаемый выход (второй) имеет дополнительные обратные косые черты. Это нормально. Тест прошел без проблем, прежде чем началась эта часть рефакторинга.

Ожидаемое Поведение

первая строка вывода должна соответствовать второй, но это не так. В частности, он не включает два символа новой строки в этот первый объект списка.

Итак, я получаю это:

"['multiline', 'command ends']n- args: command endsn- command: multilinen- statement: ['multiline', 'command ends']n  - args: command endsn  - command: multiline"

Когда Я должны тут:

"['multiline', 'command ends', 'n', 'n']n- args: command endsn- multiline_command: multilinen- statement: ['multiline', 'command ends', 'n', 'n']n  - args: command endsn  - multiline_command: multilinen  - terminator: ['n', 'n']n- terminator: ['n', 'n']"

ранее в коде, есть также это утверждение:

pyparsing.ParserElement.setDefaultWhitespaceChars(' t')

...Что Я думаю должен предотвратить именно такую ошибку. Но я не уверен.


даже если проблема не может быть идентифицирована с уверенностью, просто сужение, где проблема, было бы огромной помощью.

пожалуйста, дайте мне знать, как я могу сделать шаг или два к исправлению этого.


Edit: Итак, я должен разместить код парсера для этого, не так ли? (Спасибо за совет, @andrew cooke !)

парсер кода

здесь __init__ для моего объекта парсера.

Я знаю это кошмар. Вот почему я рефакторинг проекта. ☺

def __init__(self, Cmd_object=None, *args, **kwargs):
        #   @NOTE
        #   This is one of the biggest pain points of the existing code.
        #   To aid in readability, I CAPITALIZED all variables that are
        #   not set on `self`.
        #
        #   That means that CAPITALIZED variables aren't
        #   used outside of this method.
        #
        #   Doing this has allowed me to more easily read what
        #   variables become a part of other variables during the
        #   building-up of the various parsers.
        #
        #   I realize the capitalized variables is unorthodox
        #   and potentially anti-convention.  But after reaching out
        #   to the project's creator several times over roughly 5
        #   months, I'm still working on this project alone...
        #   And without help, this is the only way I can move forward.
        #
        #   I have a very poor understanding of the parser's
        #   control flow when the user types a command and hits ENTER,
        #   and until the author (or another pyparsing expert)
        #   explains what's happening to me, I have to do silly
        #   things like this. :-|
        #
        #   Of course, if the impossible happens and this code
        #   gets cleaned up, then the variables will be restored to
        #   proper capitalization.
        #
        #   —Zearin
        #   http://github.com/zearin/
        #   2012 Mar 26

        if Cmd_object is not None:
            self.Cmd_object = Cmd_object
        else:
            raise Exception('Cmd_object be provided to Parser.__init__().')

        #   @FIXME
        #       Refactor methods into this class later
        preparse    = self.Cmd_object.preparse
        postparse   = self.Cmd_object.postparse

        self._allow_blank_lines  =  False

        self.abbrev              =  True       # Recognize abbreviated commands
        self.case_insensitive    =  True       # Commands recognized regardless of case
        # make sure your terminators are not in legal_chars!
        self.legal_chars         =  u'!#$%.:?@_' + PYP.alphanums + PYP.alphas8bit
        self.multiln_commands    =  [] if 'multiline_commands' not in kwargs else kwargs['multiln_commands']
        self.no_special_parse    =  {'ed','edit','exit','set'}
        self.redirector          =  '>'         # for sending output to file
        self.reserved_words      =  []
        self.shortcuts           =  { '?' : 'help' ,
                                      '!' : 'shell',
                                      '@' : 'load' ,
                                      '@@': '_relative_load'
                                    }
#         self._init_grammars()
#         
#     def _init_grammars(self):
        #   @FIXME
        #       Add Docstring

        #   ----------------------------
        #   Tell PYP how to parse
        #   file input from '< filename'
        #   ----------------------------
        FILENAME    = PYP.Word(self.legal_chars + '/')
        INPUT_MARK  = PYP.Literal('<')
        INPUT_MARK.setParseAction(lambda x: '')
        INPUT_FROM  = FILENAME('INPUT_FROM')
        INPUT_FROM.setParseAction( self.Cmd_object.replace_with_file_contents )
        #   ----------------------------

        #OUTPUT_PARSER = (PYP.Literal('>>') | (PYP.WordStart() + '>') | PYP.Regex('[^=]>'))('output')
        OUTPUT_PARSER           =  (PYP.Literal(   2 * self.redirector) | 
                                   (PYP.WordStart()  + self.redirector) | 
                                    PYP.Regex('[^=]' + self.redirector))('output')

        PIPE                    =   PYP.Keyword('|', identChars='|')

        STRING_END              =   PYP.stringEnd ^ 'nEOF'

        TERMINATORS             =  [';']
        TERMINATOR_PARSER       =   PYP.Or([
                                        (hasattr(t, 'parseString') and t)
                                        or 
                                        PYP.Literal(t) for t in TERMINATORS
                                    ])('terminator')

        self.comment_grammars    =  PYP.Or([  PYP.pythonStyleComment,
                                              PYP.cStyleComment ])
        self.comment_grammars.ignore(PYP.quotedString)
        self.comment_grammars.setParseAction(lambda x: '')
        self.comment_grammars.addParseAction(lambda x: '')

        self.comment_in_progress =  '/*' + PYP.SkipTo(PYP.stringEnd ^ '*/')

        #   QuickRef: Pyparsing Operators
        #   ----------------------------
        #   ~   creates NotAny using the expression after the operator
        #
        #   +   creates And using the expressions before and after the operator
        #
        #   |   creates MatchFirst (first left-to-right match) using the
        #       expressions before and after the operator
        #
        #   ^   creates Or (longest match) using the expressions before and
        #       after the operator
        #
        #   &   creates Each using the expressions before and after the operator
        #
        #   *   creates And by multiplying the expression by the integer operand;
        #       if expression is multiplied by a 2-tuple, creates an And of
        #       (min,max) expressions (similar to "{min,max}" form in
        #       regular expressions); if min is None, intepret as (0,max);
        #       if max is None, interpret as expr*min + ZeroOrMore(expr)
        #
        #   -   like + but with no backup and retry of alternatives
        #
        #   *   repetition of expression
        #
        #   ==  matching expression to string; returns True if the string
        #       matches the given expression
        #
        #   <<  inserts the expression following the operator as the body of the
        #       Forward expression before the operator
        #   ----------------------------


        DO_NOT_PARSE            =   self.comment_grammars       |   
                                    self.comment_in_progress    |   
                                    PYP.quotedString

        #   moved here from class-level variable
        self.URLRE              =   re.compile('(https?://[-w./]+)')

        self.keywords           =   self.reserved_words + [fname[3:] for fname in dir( self.Cmd_object ) if fname.startswith('do_')]

        #   not to be confused with `multiln_parser` (below)
        self.multiln_command  =   PYP.Or([
                                        PYP.Keyword(c, caseless=self.case_insensitive)
                                        for c in self.multiln_commands
                                    ])('multiline_command')

        ONELN_COMMAND           =   (   ~self.multiln_command +
                                        PYP.Word(self.legal_chars)
                                    )('command')


        #self.multiln_command.setDebug(True)


        #   Configure according to `allow_blank_lines` setting
        if self._allow_blank_lines:
            self.blankln_termination_parser = PYP.NoMatch
        else:
            BLANKLN_TERMINATOR  = (2 * PYP.lineEnd)('terminator')
            #BLANKLN_TERMINATOR('terminator')
            self.blankln_termination_parser = (
                                                (self.multiln_command ^ ONELN_COMMAND)
                                                + PYP.SkipTo(
                                                    BLANKLN_TERMINATOR,
                                                    ignore=DO_NOT_PARSE
                                                ).setParseAction(lambda x: x[0].strip())('args')
                                                + BLANKLN_TERMINATOR
                                              )('statement')

        #   CASE SENSITIVITY for
        #   ONELN_COMMAND and self.multiln_command
        if self.case_insensitive:
            #   Set parsers to account for case insensitivity (if appropriate)
            self.multiln_command.setParseAction(lambda x: x[0].lower())
            ONELN_COMMAND.setParseAction(lambda x: x[0].lower())


        self.save_parser        = ( PYP.Optional(PYP.Word(PYP.nums)^'*')('idx')
                                  + PYP.Optional(PYP.Word(self.legal_chars + '/'))('fname')
                                  + PYP.stringEnd)

        AFTER_ELEMENTS          =   PYP.Optional(PIPE +
                                                    PYP.SkipTo(
                                                        OUTPUT_PARSER ^ STRING_END,
                                                        ignore=DO_NOT_PARSE
                                                    )('pipeTo')
                                                ) + 
                                    PYP.Optional(OUTPUT_PARSER +
                                                 PYP.SkipTo(
                                                     STRING_END,
                                                     ignore=DO_NOT_PARSE
                                                 ).setParseAction(lambda x: x[0].strip())('outputTo')
                                            )

        self.multiln_parser = (((self.multiln_command ^ ONELN_COMMAND)
                                +   PYP.SkipTo(
                                        TERMINATOR_PARSER,
                                        ignore=DO_NOT_PARSE
                                    ).setParseAction(lambda x: x[0].strip())('args')
                                +   TERMINATOR_PARSER)('statement')
                                +   PYP.SkipTo(
                                        OUTPUT_PARSER ^ PIPE ^ STRING_END,
                                        ignore=DO_NOT_PARSE
                                    ).setParseAction(lambda x: x[0].strip())('suffix')
                                + AFTER_ELEMENTS
                             )

        #self.multiln_parser.setDebug(True)

        self.multiln_parser.ignore(self.comment_in_progress)

        self.singleln_parser  = (
                                    (   ONELN_COMMAND + PYP.SkipTo(
                                        TERMINATOR_PARSER
                                        ^ STRING_END
                                        ^ PIPE
                                        ^ OUTPUT_PARSER,
                                        ignore=DO_NOT_PARSE
                                    ).setParseAction(lambda x:x[0].strip())('args'))('statement')
                                + PYP.Optional(TERMINATOR_PARSER)
                                + AFTER_ELEMENTS)
        #self.multiln_parser  = self.multiln_parser('multiln_parser')
        #self.singleln_parser = self.singleln_parser('singleln_parser')

        self.prefix_parser       =  PYP.Empty()

        self.parser = self.prefix_parser + (STRING_END                      |
                                            self.multiln_parser             |
                                            self.singleln_parser            |
                                            self.blankln_termination_parser |
                                            self.multiln_command            +
                                            PYP.SkipTo(
                                                STRING_END,
                                                ignore=DO_NOT_PARSE)
                                            )

        self.parser.ignore(self.comment_grammars)

        # a not-entirely-satisfactory way of distinguishing
        # '<' as in "import from" from
        # '<' as in "lesser than"
        self.input_parser = INPUT_MARK                + 
                            PYP.Optional(INPUT_FROM)  + 
                            PYP.Optional('>')         + 
                            PYP.Optional(FILENAME)    + 
                            (PYP.stringEnd | '|')

        self.input_parser.ignore(self.comment_in_progress)

2 ответов


я подозреваю, что проблема заключается в пропуске встроенных пробелов pyparsing, которые по умолчанию будут пропускать новые строки. Хотя setDefaultWhitespaceChars используется, чтобы сообщить pyparsing, что новые строки значимы, этот параметр влияет только на все созданные выражения после вызов setDefaultWhitespaceChars. Проблема в том, что pyparsing пытается помочь, определяя ряд выражений удобства при импорте, например empty на Empty(), lineEnd на LineEnd() и так далее. Но так все они создаются во время импорта, они определяются исходными символами пробелов по умолчанию, которые включают '\n'.

я, наверное, просто сделать это в setDefaultWhitespaceChars, но вы можете очистить это для себя тоже. Сразу после звонка setDefaultWhitespaceChars, переопределите эти выражения уровня модуля в pyparsing:

PYP.ParserElement.setDefaultWhitespaceChars(' \t')
# redefine module-level constants to use new default whitespace chars
PYP.empty = PYP.Empty()
PYP.lineEnd = PYP.LineEnd()
PYP.stringEnd = PYP.StringEnd()

я думаю, что это поможет восстановить значение ваших встроенных новых строк.

некоторые другие биты на вашем парсере код:

        self.blankln_termination_parser = PYP.NoMatch 

должно быть

        self.blankln_termination_parser = PYP.NoMatch() 

ваш первоначальный автор, возможно, был слишком агрессивным с использованием ' ^ 'over'|'. Используйте только"^", если есть некоторый потенциал для случайного анализа одного выражения, когда вы действительно проанализировали бы более длинный, который следует позже в списке альтернатив. Например, здесь:

    self.save_parser        = ( PYP.Optional(PYP.Word(PYP.nums)^'*')('idx') 

нет никакой возможной путаницы между словом числовых цифр или одиноким '*'. Or (или '^' оператор) говорит pyparsing попытаться оценить все альтернативы, а затем выбрать самый длинный подходящий - в случае галстука выберите самую левую альтернативу в списке. Если вы разбираете '*', нет необходимости видеть, может ли это также соответствовать более длинному целому числу, или если вы анализируете целое число, не нужно видеть, может ли оно также пройти как lone '*'. Поэтому измените это на:

    self.save_parser        = ( PYP.Optional(PYP.Word(PYP.nums)|'*')('idx') 

использование действия разбора для замены строки на " более просто написано с помощью PYP.Suppress фантик, или если вы предпочитаете, позвоните expr.suppress() возвращает Suppress(expr). В сочетании с предпочтением " / "над"^", Это:

    self.comment_grammars    =  PYP.Or([  PYP.pythonStyleComment, 
                                          PYP.cStyleComment ]) 
    self.comment_grammars.ignore(PYP.quotedString) 
    self.comment_grammars.setParseAction(lambda x: '') 

becomse:

    self.comment_grammars    =  (PYP.pythonStyleComment | PYP.cStyleComment
                                ).ignore(PYP.quotedString).suppress()

ключевые слова имеют встроенную логику, чтобы автоматически избежать двусмысленности, поэтому или совершенно не нужны с ними:

    self.multiln_command  =   PYP.Or([ 
                                    PYP.Keyword(c, caseless=self.case_insensitive) 
                                    for c in self.multiln_commands 
                                ])('multiline_command') 

должно быть:

    self.multiln_command  =   PYP.MatchFirst([
                                    PYP.Keyword(c, caseless=self.case_insensitive) 
                                    for c in self.multiln_commands 
                                ])('multiline_command')

(в следующем выпуске я ослаблю эти инициализаторы, чтобы принять выражения генератора, чтобы [] стали ненужный.)

это все, что я вижу сейчас. Надеюсь, это поможет.


Я все исправила!

Pyparsing не виноват!

Я был. ☹

разделив код синтаксического анализа на другой объект, я создал проблему. Первоначально атрибут, используемый для" обновления себя " на основе содержимого второго атрибута. Поскольку все это содержалось в одном "классе Бога", это работало нормально.

просто разделив код на другой объект, первый атрибут был установлен при создании экземпляра, но больше не "обновился", если второй атрибут зависел от измененного.


особенности

атрибут multiln_command (Не путать с multiln_commands - арагх, какое запутанное название!) определение грамматики, когда pyparsing. The multiln_command атрибут должен был обновить свою грамматику, если multiln_commands не изменилось.

хотя я знал, что эти два атрибута имели похожие имена, но очень разные цели, сходство определенно затрудняло отслеживание проблема вниз. У меня нет переименованных multiln_command до multiln_grammar.