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

Обзор

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

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

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

Неудачный тест

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

def test_multiline_command_ends(self, topic):
                output = parsed_input('multiline command ends\n\n',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 ends\n- command: multiline\n- statement: ['multiline', 'command ends']\n  - args: command ends\n  - command: multiline") 
      to equal "['multiline', 'command ends', '\\n', '\\n']\n- args: command ends\n- multiline_command: multiline\n- statement: ['multiline', 'command ends', '\\n', '\\n']\n  - args: command ends\n  - multiline_command: multiline\n  - terminator: ['\\n', '\\n']\n- terminator: ['\\n', '\\n']"


Примечание.

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

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

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

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

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

Когда я должен получить это:

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

Ранее в коде также есть этот оператор:

pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')

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


Даже если проблема не может быть определена с уверенностью, простое определение места, где она находится, было бы ОГРОМНОЙ помощью.

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


Редактировать: Итак, я должен опубликовать код синтаксического анализатора для этого, не так ли? (Спасибо за совет, @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)

person Zearin    schedule 10.04.2012    source источник


Ответы (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() 

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

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

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

    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: '') 

стать:

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

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

    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')

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

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

person PaulMcG    schedule 11.04.2012
comment
Вау! Пол Макгуайр, отец pyparsing. ☺ Спасибо за необычайную помощь! Срочно попробую... :) - person Zearin; 11.04.2012
comment
P.S.: Один быстрый вопрос: этот код был отделен от основного файла. (Раньше это был однофайловый модуль, а я превращаю его в многомодульный пакет.) Раньше, когда весь код был в одном файле, тест работал корректно. На данный момент это не очень важно, но мне любопытно… в чем может быть разница, которая сломала тест? - person Zearin; 11.04.2012
comment
Это чисто предположение, так как обычно не должно быть никаких проблем с разделением кода на несколько файлов, если импорт остается прямым. Но если у вас есть ссылки на константы, определенные в области модуля в исходном файле, а затем вынесены в отдельный файл, возможно, вы ошибочно решили проблему со ссылками, определив новую локальную переменную. Возможно, вы реализовали это неправильно, ИЛИ переменная уровня модуля содержит некоторое изменяемое состояние, которое ранее было общим для одного модуля, но теперь является локальным для вашей локальной копии. - person PaulMcG; 11.04.2012
comment
Еще один совет: запишите БНФ для этой грамматики. Это поможет вам лучше понять, к какой цели стремился первоначальный автор, лучше организует ваше собственное мышление, даст вам дорожную карту для синтаксического анализатора и поможет вам не сбиться с пути, куда должны идти изменения, если вам нужно добавить новые функции синтаксиса или исправление ошибок в существующем. - person PaulMcG; 11.04.2012
comment
И последний совет по анализу: вы можете заглянуть в работу парсера, если пометите некоторые ключевые выражения знаком setDebug(). Выражения с поддержкой отладки будут сообщать, когда они собираются попытаться выполнить синтаксический анализ, и если они завершились неудачей или успехом, а если удалось, то каким был совпадающий текст. setName() также полезен при использовании setDebug, так как вы можете дать выражению значимое имя, например integer = Word(nums).setName('integer'), тогда, когда оно сообщит об отладке, вы увидите, что вы сопоставляете «целое число», а не «W: (0123...)». . - person PaulMcG; 11.04.2012

Я починил это!

Pyparsing не виноват!

Я был. ☹

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

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


Особенности

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

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

Однако! ☺

Я благодарен @Paul McGuire за отличный ответ и надеюсь, что он избавит меня (и других) от горя в будущем. Хотя я чувствую себя немного глупо из-за того, что я вызвал проблему (и ошибочно диагностировал ее как проблему pyparsing), я рад, что этот вопрос принес пользу (в виде совета Пола).


Всем удачного разбора. :)

person Zearin    schedule 11.04.2012
comment
В pyparsing 3.0 вызов setDefaultWhitespaceChars также обновит пробельные символы для всех определенных помощников pyparsing. - person PaulMcG; 18.06.2021