Add non-strict parsing and the "strict" argument.

This commit is contained in:
Barry Downes 2023-08-05 15:29:39 +10:00
parent 277e75f66f
commit 5a673ee930
3 changed files with 66 additions and 25 deletions

View File

@ -6,7 +6,7 @@ import random
import comfy.parse import comfy.parse
from comfy.parse import ParseError from comfy.parse import ParseError
class LogicError(Exception): class ParseLogicError(ParseError):
# something that shouldn't be possible occurred in the code # something that shouldn't be possible occurred in the code
# not the user's fault # not the user's fault
pass pass
@ -15,7 +15,7 @@ class LogicError(Exception):
def get_random_seed(): def get_random_seed():
return int.from_bytes(os.urandom(8)) return int.from_bytes(os.urandom(8))
def translate(text, seed=None, reescape=frozenset()): def translate(text, seed=None, strict=True, reescape=frozenset()):
''' '''
Parses the text, translating "{A|B|C}" choices into a single chosen option. Parses the text, translating "{A|B|C}" choices into a single chosen option.
An option is chosen randomly from the available options. An option is chosen randomly from the available options.
@ -26,6 +26,8 @@ def translate(text, seed=None, reescape=frozenset()):
"a woman wearing a realistic police uniform". "a woman wearing a realistic police uniform".
All random choices are governed by the supplied random seed value, ensuring repeatability. All random choices are governed by the supplied random seed value, ensuring repeatability.
If strict is True, exceptions will be thrown if the input doesn't conform to expectations.
reescape indicates the set of metacharacters that, if escaped with a backslash in the input, should be re-escaped in the output. reescape indicates the set of metacharacters that, if escaped with a backslash in the input, should be re-escaped in the output.
This is useful to avoid need for multi-escaping when incorporating this parser as a single phase in a multi-phase parsing operation. This is useful to avoid need for multi-escaping when incorporating this parser as a single phase in a multi-phase parsing operation.
Note that while the default is a frozenset, you can pass anything that works with the "in" operator, such as a string or a set. Note that while the default is a frozenset, you can pass anything that works with the "in" operator, such as a string or a set.
@ -35,14 +37,14 @@ def translate(text, seed=None, reescape=frozenset()):
options = [] options = []
while True: while True:
options.append(parse_text_with_choices(input)) options.append(parse_text_with_choices(input))
if 0: pass if m := input.match(r'\|'):
elif m := input.match(r'\|'):
# loop around for another choice # loop around for another choice
pass pass
elif m := input.match(r'\}'):
break
else: else:
raise ParseError(input, f"Expected '|' or '}}' after choice text") # at this point, the input must be }
# although for incorrectly-formed input, it could be end of input too
# regardless, the correct action here is to break and return to the caller
break
# choose one of the options # choose one of the options
text = rng.choice(options) text = rng.choice(options)
@ -53,35 +55,47 @@ def translate(text, seed=None, reescape=frozenset()):
while True: while True:
if 0: pass if 0: pass
elif m := input.match(r'\\'): # \ = escape character elif m := input.match(r'\\'):
# \ = escape character
if m := input.match(r'.'): if m := input.match(r'.'):
ch = m.group(0) ch = m.group(0)
if ch in reescape: if ch in reescape:
out.append('\\') out.append('\\')
out.append(ch) out.append(ch)
else: else:
raise ParseError(input, f'Unexpected end of input after backslash') if strict:
elif m := input.match(r'\{'): # { raise ParseError(input, f'Unexpected end of input after backslash')
# choice elif m := input.match(r'\{'):
# { ... | ... } choice
openbrace = input.prior()
chosen_text = parse_choice(input) chosen_text = parse_choice(input)
if not input.match(r'\}'):
if strict:
raise ParseError(openbrace, f"Missing matching closing brace '}}' for earlier open brace '{{'")
out.append(chosen_text) out.append(chosen_text)
elif m := input.match(r'\/'): # / elif m := input.match(r'\/'):
# C-style block "/* */" and line "//" comments # C-style block "/* */" and line "//" comments
comment = input.prior()
if 0: pass if 0: pass
elif m := input.match(r'\/'): # / elif m := input.match(r'\/'):
# line comment # // line comment
if not input.match(r'.*?(?:\n|$)'): if not input.match(r'.*?(?:\n|$)'):
raise ParseError(input, f"Failed to find end of comment") if strict:
raise ParseLogicError(comment, f"Failed to find end of C-style // line comment")
input.match(r'.*') # consume unterminated comment (however that might be possible)
out.append('\n') out.append('\n')
elif m := input.match(r'\*'): # / elif m := input.match(r'\*'):
# block comment # /* ... */ block comment
if not input.match(r'.*?\*\/'): if not input.match(r'.*?\*\/'):
raise ParseError(input, f"Unterminated comment") if strict:
raise ParseError(comment, f"Unterminated C-style /* ... */ block comment")
input.match(r'.*') # consume unterminated comment
out.append(' ') out.append(' ')
else: else:
# it was a literal /, not a comment after all # it was a literal /, not a comment after all
out.append('/'); out.append('/');
elif m := input.match(r'[^\\\{\}\|\/]+'): # 1 or more non-metacharacters elif m := input.match(r'[^\\\{\}\|\/]+'):
# 1 or more non-metacharacters
out.append(m.group(0)) out.append(m.group(0))
else: else:
# didn't match \, {, / or non-metacharacters # didn't match \, {, / or non-metacharacters
@ -90,6 +104,29 @@ def translate(text, seed=None, reescape=frozenset()):
return ''.join(out) return ''.join(out)
def parse_text_with_choices_outer(input):
# this function and the contained loop is required to support the non-strict parsing mode
# it catches the case where we exit parse_text_with_choices upon encountering | or }, and don't find ourselves withing a calling instance of parse_choice
out = []
while True:
out.append(parse_text_with_choices(input))
if 0:pass
elif input.match(r'$'):
break
elif input.match(r'\|'):
if strict:
raise ParseError(input.prior(), f"Encountered a choice delimiter '|' outside any choice block")
elif input.match(r'\}'):
if strict:
raise ParseError(input.prior(), f"Encountered a closing brace '}}' without a matching open brace")
else:
if strict:
raise ParseLogicError(input, f'Failed to parse up to the end of the prompt text')
break
return ''.join(out)
if seed == None: if seed == None:
seed = get_random_seed() seed = get_random_seed()
@ -98,12 +135,9 @@ def translate(text, seed=None, reescape=frozenset()):
try: try:
input = comfy.parse.Cursor(text) input = comfy.parse.Cursor(text)
out = parse_text_with_choices(input) out = parse_text_with_choices_outer(input)
if not input.match(r'$'):
raise ParseError(input, f'Failed to parse up to the end of the prompt text')
return out return out
except (ParseError, LogicError) as e: except (ParseError) as e:
# alternative: re-throw the error # alternative: re-throw the error
stdout.write(f'Error parsing prompt: {e}'); stdout.write(f'Error parsing prompt: {e}');
return text return text

View File

@ -24,6 +24,13 @@ class Cursor:
self.consume = consume self.consume = consume
self.space = space self.space = space
def prior(self):
# returns a cursor pointing at the position prior to the last match
prior = self.clone()
prior.end = prior.start
prior.pos = prior.start
return prior
def loc(self): def loc(self):
# describe the cursor position in a human-readable form, suitable for error messages # describe the cursor position in a human-readable form, suitable for error messages

View File

@ -1479,7 +1479,7 @@ class DynamicPrompt:
CATEGORY = "conditioning" CATEGORY = "conditioning"
def dynamic_prompt(self, text, seed): def dynamic_prompt(self, text, seed):
translated_prompt_text = comfy.choices.translate(text, seed=seed, reescape=r'\()') translated_prompt_text = comfy.choices.translate(text, seed=seed, strict=False, reescape=r'\()')
return (translated_prompt_text,) return (translated_prompt_text,)