#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-parse-type. Emits `key=value' lines, one per line, in file order; the suite reads them back with grep. A python3 start on this arm is tens of seconds and every measurement here is a small `re' match or a `str.split' over strings under 64 characters, so the start is the entire cost of the suite and taking it once is the difference between a 30-second run and a ten-minute one. NOTHING HERE TOUCHES THE FILESYSTEM, THE NETWORK, A DEVICE OR A TERMINAL. parse_type BUILDS FUNCTIONS. Every case constructs a type converter in-process and calls it, or hands it to `parse.parse' and reads the Result. There is no fixture to ship and none is built: the inputs are string literals in this file. That is the whole reason this suite is deterministic on a target with no clock, no writable HOME and no peer. WHAT THE PACKAGE IS. Two layers, and the suite keeps them apart on purpose: parse_type.cardinality Cardinality (an enum of 1, 0..1, 0..*, 1..*) and a TypeBuilder that derives a converter for T-with-a- cardinality from the converter for one T. The derived converter carries a `pattern' built from the item pattern, so the PARSER rejects the wrong shape. parse_type.builder TypeBuilder (a subclass of the above) with the composite constructors -- make_enum, make_choice, make_choice2, make_variant, make_list -- plus build_type_dict and parse_anything. Only the SECOND layer's `make_list' throws the item pattern away and accepts `.+?', which is why this suite asserts a generated `pattern' for every converter it builds rather than only the callable's return value: the two layers disagree about what the parser is allowed to match, and a caller cannot see that from the return type. THREE RULES THE VALUES OBEY. The runner parses its input line by line, so no value may contain a newline -- every value below is a single line. No value may depend on the encoding stdout happens to be in: every one is ASCII. And no value is derived from the environment -- there is no getenv here at all. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def emitc(key, thunk): """emit(key, thunk()) -- but a raise becomes `!'. Every case that CALLS a converter goes through here, so that a regression in one of them costs that case and not the rest of the probe. A converter this suite expects to return is expected to return: a `!' value from here is a red, and reads as one. Without it a single unexpected raise ends the probe and every later case scores against an empty string -- 140 of the 256 below, measured, when the make_enum case fallback was removed as a mutation control.""" emit(key, call(thunk)) def call(fn, *args, **kw): """The result, or `!' on a raise. The exception NAME rather than its message wherever the message is CPython's rather than parse_type's -- `int()' has reworded its `invalid literal' text between releases while the type has not. Where the message IS parse_type's own it is emitted as its own case.""" try: return fn(*args, **kw) except Exception as exc: return '!%s' % type(exc).__name__ def msg(fn, *args, **kw): """The exception's `str()', or `' where the call returned.""" try: fn(*args, **kw) return '' except Exception as exc: return str(exc) try: import enum import parse import parse_type from parse_type import TypeBuilder, Cardinality, build_type_dict from parse_type.builder import parse_anything from parse_type.cardinality import pattern_group_count from parse_type.cardinality import TypeBuilder as CardinalityTypeBuilder except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) # ---------------------------------------------------------------- identity --- emit('module_file', getattr(parse_type, '__file__', '')) emit('is_package', 'yes' if hasattr(parse_type, '__path__') else 'no') emit('version_attr', parse_type.__version__) emit('dunder_all', ','.join(parse_type.__all__)) emit('parse_version', parse.__version__) # WHICH `parse' ANSWERED. parse_type SHIPS ITS OWN VENDORED COPY of the parse # module (`parse_type/parse.py', a fork of parse 1.19.1 kept for the unnamed- # field group-index fix its own docstring points at), so there are TWO parse # modules on this rootfs once both packages are installed. A top-level # `import parse' must reach python3-parse's, NOT the vendored one, or every # expectation below is about a different library than the one the recipe # RDEPENDS on. emit('parse_file', getattr(parse, '__file__', '')) emit('parse_is_vendored', 'yes' if 'parse_type' in getattr(parse, '__file__', '') else 'no') import parse_type.parse as vendored_parse emit('vendored_version', vendored_parse.__version__) emit('vendored_is_older', 'yes' if vendored_parse.__version__ != parse.__version__ else 'no') # The TypeBuilder the package exports is builder's, which SUBCLASSES # cardinality's. A release that flattened the two would keep every behavioural # case below green and change what `from parse_type import TypeBuilder' means. emit('builder_subclasses_cardinality', 'yes' if issubclass(TypeBuilder, CardinalityTypeBuilder) else 'no') emit('builder_is_not_base', 'yes' if TypeBuilder is not CardinalityTypeBuilder else 'no') emit('default_strict', 'yes' if TypeBuilder.default_strict else 'no') emit('anything_pattern', TypeBuilder.anything_pattern) emit('default_pattern', TypeBuilder.default_pattern) emit('parse_anything_pattern', parse_anything.pattern) emitc('parse_anything_identity', lambda: parse_anything('a b,c')) # `default_re_opts' is IGNORECASE|DOTALL, and it is the reason make_enum has a # `text.lower()' fallback at all -- the parser matches `YES' against the # lowercase alternation, so the converter is handed text in a case the mapping # does not have. Named here because the fallback's failure mode (below) is # unreadable without it. emit('re_opts_ignorecase', 'yes' if TypeBuilder.default_re_opts & 2 else 'no') emit('re_opts_dotall', 'yes' if TypeBuilder.default_re_opts & 16 else 'no') # ------------------------------------------------------------- Cardinality --- # FOUR MEMBERS AND SEVEN NAMES. `optional', `many' and `many0' are ALIASES, # not members: iterating the enum yields four, `__members__' holds seven, and # `Cardinality.many.name' is `one_or_more'. A caller who logs the `.name' of # the cardinality they asked for gets a different word back. emit('card_members', ','.join(c.name for c in Cardinality)) emit('card_member_count', len(list(Cardinality))) emit('card_name_count', len(Cardinality.__members__)) emit('card_names', ','.join(sorted(Cardinality.__members__))) emit('card_alias_optional', 'yes' if Cardinality.optional is Cardinality.zero_or_one else 'no') emit('card_alias_many', 'yes' if Cardinality.many is Cardinality.one_or_more else 'no') emit('card_alias_many0', 'yes' if Cardinality.many0 is Cardinality.zero_or_more else 'no') emit('card_many_name', Cardinality.many.name) emit('card_optional_name', Cardinality.optional.name) emit('card_many0_name', Cardinality.many0.name) emit('card_one_schema', 'None' if Cardinality.one.schema is None else Cardinality.one.schema) emit('card_zoo_schema', Cardinality.zero_or_one.schema) emit('card_zom_schema', Cardinality.zero_or_more.schema) emit('card_oom_schema', Cardinality.one_or_more.schema) emit('card_one_gc', Cardinality.one.group_count) emit('card_zoo_gc', Cardinality.zero_or_one.group_count) emit('card_zom_gc', Cardinality.zero_or_more.group_count) emit('card_oom_gc', Cardinality.one_or_more.group_count) emit('card_one_is_many', 'yes' if Cardinality.one.is_many() else 'no') emit('card_zoo_is_many', 'yes' if Cardinality.zero_or_one.is_many() else 'no') emit('card_zom_is_many', 'yes' if Cardinality.zero_or_more.is_many() else 'no') emit('card_oom_is_many', 'yes' if Cardinality.one_or_more.is_many() else 'no') # make_pattern: cardinality one is the IDENTITY, and the other three splice the # item pattern into the schema. This is the function every generated `pattern' # below is built by, so it is measured directly as well as through them. NUM = r'\d+' emit('mkpat_one', Cardinality.one.make_pattern(NUM)) emit('mkpat_zoo', Cardinality.zero_or_one.make_pattern(NUM)) emit('mkpat_zom', Cardinality.zero_or_more.make_pattern(NUM)) emit('mkpat_oom', Cardinality.one_or_more.make_pattern(NUM)) emit('mkpat_oom_semi', Cardinality.one_or_more.make_pattern(NUM, listsep=';')) emit('mkpat_zom_semi', Cardinality.zero_or_more.make_pattern(NUM, listsep=';')) # pattern_group_count COUNTS `(' AND DISCOUNTS `\(' -- and it is text-only, so # a non-capturing `(?:' counts as a group it will never produce. That # over-count propagates into `regex_group_count', which parse reads. emit('pgc_none', pattern_group_count(NUM)) emit('pgc_two', pattern_group_count(r'(a)(b)')) emit('pgc_escaped', pattern_group_count(r'\(a\)')) emit('pgc_mixed', pattern_group_count(r'(a)\((b)')) emit('pgc_noncapturing', pattern_group_count(r'(?:a)')) emit('cgc_one', Cardinality.one.compute_group_count(NUM)) emit('cgc_zoo', Cardinality.zero_or_one.compute_group_count(NUM)) emit('cgc_zom', Cardinality.zero_or_more.compute_group_count(NUM)) emit('cgc_oom', Cardinality.one_or_more.compute_group_count(NUM)) emit('cgc_zoo_grouped', Cardinality.zero_or_one.compute_group_count(r'(a)')) emit('cgc_oom_grouped', Cardinality.one_or_more.compute_group_count(r'(a)')) # ------------------------------------------------- the item converter used --- # One converter, used by every cardinality case below. `with_pattern' is # parse's own decorator and it is what puts `.pattern' on the function; a # converter without one falls back to `default_pattern' (`.+?'), which is # asserted separately at `nopat_*'. @parse.with_pattern(NUM) def parse_number(text): return int(text) parse_number.name = 'Number' emit('item_pattern', parse_number.pattern) emitc('item_call', lambda: parse_number('42')) emit('item_call_bad', call(parse_number, 'abc')) # --------------------------------------------- with_cardinality dispatching --- # CARDINALITY ONE RETURNS THE CONVERTER ITSELF -- the same object, not a # wrapper. So a caller who sets an attribute on the "derived" converter has # set it on the original. one = TypeBuilder.with_cardinality(Cardinality.one, parse_number) emit('wc_one_is_same', 'yes' if one is parse_number else 'no') opt = TypeBuilder.with_cardinality(Cardinality.zero_or_one, parse_number) many0 = TypeBuilder.with_cardinality(Cardinality.zero_or_more, parse_number) many = TypeBuilder.with_cardinality(Cardinality.one_or_more, parse_number) emit('wc_opt_is_new', 'yes' if opt is not parse_number else 'no') emit('wc_opt_matches_alias', 'yes' if opt.pattern == TypeBuilder.with_optional(parse_number).pattern else 'no') emit('wc_many_matches_alias', 'yes' if many.pattern == TypeBuilder.with_many(parse_number).pattern else 'no') emit('wc_many0_matches_alias', 'yes' if many0.pattern == TypeBuilder.with_many0(parse_number).pattern else 'no') # ------------------------------------------------------ optional (0..1) ----- emit('opt_pattern', opt.pattern) emit('opt_group_count', opt.regex_group_count) emitc('opt_value', lambda: opt('42')) emitc('opt_value_type', lambda: type(opt('42')).__name__) # ABSENT IS None, AND SO IS WHITESPACE -- the converter strips first and treats # the result as absent, so a field that matched only spaces is indistinguishable # from one that matched nothing. emitc('opt_empty', lambda: 'None' if opt('') is None else opt('')) emitc('opt_spaces', lambda: 'None' if opt(' ') is None else opt(' ')) emitc('opt_none_arg', lambda: 'None' if opt(None) is None else opt(None)) emitc('opt_stripped', lambda: opt(' 42 ')) emit('opt_bad', call(opt, 'abc')) # ------------------------------------------------------ many0 (0..*) ----- emit('many0_pattern', many0.pattern) emit('many0_group_count', many0.regex_group_count) emitc('many0_empty', lambda: repr(many0(''))) emitc('many0_spaces', lambda: repr(many0(' '))) emitc('many0_one', lambda: repr(many0('1'))) emitc('many0_three', lambda: repr(many0('1, 2, 3'))) emitc('many0_type', lambda: type(many0('1')).__name__) emit('many0_bad', call(many0, 'a')) # ------------------------------------------------------ many (1..*) ----- emit('many_pattern', many.pattern) emit('many_group_count', many.regex_group_count) emitc('many_one', lambda: repr(many('1'))) emitc('many_three', lambda: repr(many('1, 2, 3'))) emitc('many_tight', lambda: repr(many('1,2,3'))) emitc('many_padded', lambda: repr(many(' 1 , 2 '))) # ...AND EMPTY IS NOT `[]' HERE. `with_one_or_more' has NO empty guard -- it # splits unconditionally -- so the empty string becomes a ONE-item list holding # whatever the item converter makes of `'. With `int' that is a raise; with a # converter that accepts anything it is `['']'. `many0' guards and returns # `[]'. This asymmetry is the single most likely thing for a caller to get # wrong, and `mklist_empty' below shows it reaching the public constructor. emit('many_empty', call(many, '')) many_semi = TypeBuilder.with_many(parse_number, listsep=';') emit('many_semi_pattern', many_semi.pattern) emitc('many_semi_value', lambda: repr(many_semi('1; 2; 3'))) many0_semi = TypeBuilder.with_many0(parse_number, listsep=';') emit('many0_semi_pattern', many0_semi.pattern) emitc('many0_semi_value', lambda: repr(many0_semi('1; 2'))) # ----------------------------------------- a converter with NO .pattern ------ # The fallback is `default_pattern' (`.+?'), NOT a failure -- so a plain # undecorated function silently gets a converter whose parser matches anything. def bare_int(text): return int(text) nopat_opt = TypeBuilder.with_optional(bare_int) nopat_many = TypeBuilder.with_many(bare_int) emit('nopat_has_pattern', 'yes' if hasattr(bare_int, 'pattern') else 'no') emit('nopat_opt_pattern', nopat_opt.pattern) emit('nopat_many_pattern', nopat_many.pattern) emit('nopat_explicit_pattern', TypeBuilder.with_optional(bare_int, r'\d+').pattern) # --------------------------------------------------------------- make_list --- # make_list IS NOT with_many. It passes `pattern=anything_pattern', so the # generated pattern is `.+?'-based WHATEVER the item converter's own pattern # is: the parser accepts any text and the ITEM CONVERTER is left to fail. The # two patterns are emitted side by side because that is the difference. mklist = TypeBuilder.make_list(parse_number) emit('mklist_pattern', mklist.pattern) emit('mklist_group_count', mklist.regex_group_count) emit('mklist_pattern_differs_from_many', 'yes' if mklist.pattern != many.pattern else 'no') emit('mklist_pattern_is_anything_based', 'yes' if mklist.pattern == TypeBuilder.with_many( parse_number, pattern=TypeBuilder.anything_pattern).pattern else 'no') emitc('mklist_one', lambda: repr(mklist('1'))) emitc('mklist_three', lambda: repr(mklist('1, 2, 3'))) emit('mklist_bad', call(mklist, 'a, b')) # EMPTY IS `[]' FROM NOBODY. make_list is built on one_or_more, so the empty # string is a one-item list -- and with the default (`parse_anything') item # converter that item is the EMPTY STRING, not nothing. A caller who reads # `make_list' as "a list, possibly empty" gets `[]' only from `with_many0'. mklist_any = TypeBuilder.make_list() emit('mklist_default_pattern', mklist_any.pattern) emitc('mklist_empty', lambda: repr(mklist_any(''))) emitc('mklist_empty_len', lambda: len(mklist_any(''))) emitc('mklist_any_one', lambda: repr(mklist_any('a'))) emitc('mklist_any_three', lambda: repr(mklist_any('a, b, c'))) emitc('many0_empty_len', lambda: len(many0(''))) mklist_semi = TypeBuilder.make_list(parse_number, listsep=';') emit('mklist_semi_pattern', mklist_semi.pattern) emitc('mklist_semi_value', lambda: repr(mklist_semi('1; 2; 3'))) # ...and the DEFAULT separator is not honoured once a custom one is set: a comma # in the text is then part of an item, which `int' rejects. emit('mklist_semi_comma', call(mklist_semi, '1, 2')) # --------------------------------------------------------------- make_enum --- # OVER A DICT. The pattern is the keys joined with `|', in DICT ORDER, and the # converter returns the mapped VALUE. yesno = TypeBuilder.make_enum({'yes': True, 'no': False}) emit('enum_dict_pattern', yesno.pattern) emit('enum_dict_mappings', ','.join(sorted(yesno.mappings))) emitc('enum_dict_yes', lambda: yesno('yes')) emitc('enum_dict_no', lambda: yesno('no')) emitc('enum_dict_yes_type', lambda: type(yesno('yes')).__name__) # A NON-MEMBER RAISES KeyError, NOT ValueError. make_choice next door raises # ValueError for the same shape of mistake, so a caller who wrote one `except' # for both is catching half of them. emit('enum_dict_missing', call(yesno, 'maybe')) emit('enum_dict_missing_msg', msg(yesno, 'maybe')) # THE CASE FALLBACK, AND WHERE IT STOPS. The converter retries `text.lower()' # on a miss, because the parser matched under re.IGNORECASE and may hand back # text in a case the mapping does not hold. So an ALL-LOWERCASE mapping is # case-insensitive... emitc('enum_dict_upper', lambda: yesno('YES')) emitc('enum_dict_mixed', lambda: yesno('Yes')) # ...and a mapping with an UPPERCASE key is not: lowering `Yes' finds nothing, # and there is no second retry. A caller who names their members `Yes'/`No' # has built a converter the parser can match and the converter cannot convert. upper = TypeBuilder.make_enum({'Yes': True, 'No': False}) emit('enum_upper_pattern', upper.pattern) emitc('enum_upper_exact', lambda: upper('Yes')) emit('enum_upper_lower', call(upper, 'yes')) emit('enum_upper_upper', call(upper, 'YES')) # OVER AN enum.Enum CLASS. `__members__' is taken, so the pattern is the member # NAMES and the converted value is the MEMBER, not its `.value'. class Color(enum.Enum): red = 1 green = 2 blue = 3 ecolor = TypeBuilder.make_enum(Color) emit('enum_cls_pattern', ecolor.pattern) emit('enum_cls_mappings_type', type(ecolor.mappings).__name__) emitc('enum_cls_red', lambda: ecolor('red').name) emitc('enum_cls_red_value', lambda: ecolor('red').value) emitc('enum_cls_green', lambda: ecolor('green').name) emitc('enum_cls_blue', lambda: ecolor('blue').name) emitc('enum_cls_is_member', lambda: 'yes' if isinstance(ecolor('red'), Color) else 'no') emitc('enum_cls_identity', lambda: 'yes' if ecolor('red') is Color.red else 'no') emit('enum_cls_missing', call(ecolor, 'pink')) emitc('enum_cls_upper', lambda: ecolor('RED').name) # The mapping is the class's own `__members__' proxy, so it is READ-ONLY -- # a caller cannot extend the converter after the fact. emit('enum_cls_mappings_readonly', call(ecolor.mappings.__setitem__, 'pink', 4) if hasattr(ecolor.mappings, '__setitem__') else '!TypeError') # make_enum is a STATIC method and takes no cardinality: there is no # `make_enum(..., many)'. Cardinality is layered on afterwards, which is what # the next case shows and what `Number+' in cfparse automates. enum_many = TypeBuilder.with_many(yesno, pattern=yesno.pattern) emit('enum_many_pattern', enum_many.pattern) emitc('enum_many_value', lambda: repr(enum_many('yes, no, yes'))) # ------------------------------------------------------------- make_choice --- # THE CONVERTER RETURNS THE TEXT, not an index -- make_choice2 returns the pair. choice = TypeBuilder.make_choice(['red', 'green', 'blue']) emit('choice_pattern', choice.pattern) emit('choice_choices', ','.join(choice.choices)) emit('choice_choices_type', type(choice.choices).__name__) emitc('choice_red', lambda: choice('red')) emitc('choice_green', lambda: choice('green')) emitc('choice_blue', lambda: choice('blue')) emitc('choice_return_type', lambda: type(choice('red')).__name__) # A NON-CHOICE RAISES ValueError, and the message names every choice. This is # the SAME mistake make_enum answers with KeyError -- see `enum_dict_missing'. emit('choice_missing', call(choice, 'pink')) emit('choice_missing_msg', msg(choice, 'pink')) # STRICTNESS IS ON BY DEFAULT and can be turned off, in which case the # converter is the identity and the ONLY thing rejecting a non-choice is the # parser's regex. Called directly, as here, nothing rejects it at all. loose = TypeBuilder.make_choice(['red', 'green'], strict=False) emitc('choice_loose_pink', lambda: loose('pink')) emit('choice_loose_pattern', loose.pattern) # THE PARSER MATCHES CASE-INSENSITIVELY AND THE CONVERTER DOES NOT, which is # what `transform' is for. Without one, `RED' reaches a converter whose list # holds only `red' and is rejected -- the converter is stricter than the # pattern that fed it. emit('choice_upper', call(choice, 'RED')) lowered = TypeBuilder.make_choice(['red', 'green'], transform=str.lower) emitc('choice_transform_upper', lambda: lowered('RED')) emitc('choice_transform_mixed', lambda: lowered('Green')) emit('choice_transform_pattern', lowered.pattern) # `transform' is applied to the CHOICES at build time as well as to the text, # so an uppercase choice list plus `str.lower' yields a lowercase pattern. uppered = TypeBuilder.make_choice(['RED', 'GREEN'], transform=str.lower) emit('choice_transform_choices', ','.join(uppered.choices)) emit('choice_transform_choices_pattern', uppered.pattern) # AN EMPTY CHOICE LIST BUILDS AN EMPTY PATTERN, and does not raise. `r""' # matches at every position, so such a converter would let the parser match the # empty string and then reject it -- a converter that can never succeed and # says so nowhere. empty_choice = TypeBuilder.make_choice([]) emit('choice_empty_pattern_len', len(empty_choice.pattern)) emit('choice_empty_call', call(empty_choice, 'red')) choice2 = TypeBuilder.make_choice2(['red', 'green', 'blue']) emit('choice2_pattern', choice2.pattern) emitc('choice2_red', lambda: repr(choice2('red'))) emitc('choice2_green', lambda: repr(choice2('green'))) emitc('choice2_blue_index', lambda: choice2('blue')[0]) emitc('choice2_blue_text', lambda: choice2('blue')[1]) emitc('choice2_type', lambda: type(choice2('red')).__name__) emit('choice2_missing', call(choice2, 'pink')) # ------------------------------------------------------------ make_variant --- # THE PATTERN IS THE ALTERNATIVES, EACH PARENTHESISED. `regex_group_count' is # one per alternative PLUS the groups inside each alternative's own pattern. variant = TypeBuilder.make_variant([parse_number, yesno]) emit('variant_pattern', variant.pattern) emit('variant_group_count', variant.regex_group_count) emit('variant_converters_len', len(variant.converters)) emit('variant_converters_type', type(variant.converters).__name__) emitc('variant_number', lambda: variant('42')) emitc('variant_number_type', lambda: type(variant('42')).__name__) emitc('variant_yes', lambda: variant('yes')) emitc('variant_yes_type', lambda: type(variant('yes')).__name__) # FIRST MATCH WINS, IN LIST ORDER. With the order reversed the enum is tried # first, so a text matching both goes to the enum -- and `no' is a member, so # it is the pair (`\d+', enum) that makes order visible rather than a # contrived overlap. variant_rev = TypeBuilder.make_variant([yesno, parse_number]) emit('variant_rev_pattern', variant_rev.pattern) emitc('variant_rev_number', lambda: variant_rev('42')) # NO ALTERNATIVE MATCHES -> AssertionError, WHICH `python3 -O' DELETES. The # strict arm is a bare `assert', so under -O the function falls through to # `return None' and a mismatch becomes a silent None. Both arms are measured: # `variant_nomatch' is this interpreter, `variant_nomatch_optimized' is what # the same call does with assertions off. emit('variant_nomatch', call(variant, 'zzz')) emit('variant_nomatch_msg', msg(variant, 'zzz')) loose_variant = TypeBuilder.make_variant([parse_number, yesno], strict=False) emitc('variant_loose_nomatch', lambda: 'None' if loose_variant('zzz') is None else loose_variant('zzz')) emit('variant_optimize_flag', sys.flags.optimize) # A SINGLE-CONVERTER VARIANT IS THE CONVERTER ITSELF. No wrapper, no # `.converters', no `regex_group_count' -- so code that reads those attributes # off the result of make_variant works for two alternatives and raises for one. single = TypeBuilder.make_variant([parse_number]) emit('variant_single_is_same', 'yes' if single is parse_number else 'no') emit('variant_single_has_converters', 'yes' if hasattr(single, 'converters') else 'no') emit('variant_empty', call(TypeBuilder.make_variant, [])) # THE COMPILED ARM MUTATES THE CALLER'S CONVERTERS. `compiled=True' caches a # `matcher' attribute ON EACH CONVERTER FUNCTION -- shared, module-level # objects in ordinary use -- so a second variant built over the same converter # with different `re_opts' silently reuses the FIRST one's compiled matcher. emit('variant_matcher_before', 'yes' if hasattr(parse_anything, 'matcher') else 'no') compiled = TypeBuilder.make_variant([parse_anything, yesno], compiled=True) emit('variant_matcher_after', 'yes' if hasattr(parse_anything, 'matcher') else 'no') emit('variant_compiled_pattern', compiled.pattern) emitc('variant_compiled_value', lambda: compiled('anything at all')) # ---------------------------------------------------------- build_type_dict --- # KEYED BY THE CONVERTER'S `.name' ATTRIBUTE, which nothing sets for you: a # converter without one raises AttributeError here rather than being skipped. many.name = 'Number+' opt.name = 'Number?' many0.name = 'Number*' type_dict = build_type_dict([parse_number, many, opt, many0]) emit('typedict_keys', ','.join(sorted(type_dict))) emit('typedict_len', len(type_dict)) emit('typedict_number_is_item', 'yes' if type_dict['Number'] is parse_number else 'no') emit('typedict_nameless', call(build_type_dict, [bare_int])) emit('typedict_not_callable', call(build_type_dict, ['x'])) # ------------------------------------------- the generated pattern, in parse -- # THIS IS THE HALF A DIRECT CALL CANNOT SHOW. Everything above calls the # converter itself; these hand it to `parse.parse', where the generated pattern # decides whether the converter is reached at all. def P(fmt, text, types=None): r = parse.parse(fmt, text, types if types is not None else type_dict) if r is None: return 'None' return repr(r.named) emitc('p_number_ok', lambda: P('N: {n:Number}', 'N: 42')) emitc('p_number_letters', lambda: P('N: {n:Number}', 'N: abc')) emitc('p_many_ok', lambda: P('L: {n:Number+}', 'L: 1, 2, 3')) emitc('p_many_one', lambda: P('L: {n:Number+}', 'L: 7')) # THE PATTERN IS WHAT REJECTS THE WRONG TYPE. `Number+' generates `\d+'-based # alternation, so letters never reach `int()' -- `parse' simply does not match # and returns None. That is the case the ticket calls "a converter built for # one type does not accept another type's input". emitc('p_many_letters', lambda: P('L: {n:Number+}', 'L: a, b')) emitc('p_many_mixed', lambda: P('L: {n:Number+}', 'L: 1, b')) emitc('p_opt_present', lambda: P('x={n:Number?}y', 'x=42y')) emitc('p_opt_absent', lambda: P('x={n:Number?}y', 'x=y')) emitc('p_many0_absent', lambda: P('x={n:Number*}y', 'x=y')) emitc('p_many0_two', lambda: P('x={n:Number*}y', 'x=1, 2y')) # ...AND make_list THROWS THAT PROTECTION AWAY. Same input, same item # converter, but the pattern is `.+?'-based, so the text matches, `int()' is # reached, and its ValueError PROPAGATES OUT OF parse.parse -- a raise where # the `Number+' spelling returns None. Two spellings of one intent, two # different failure modes. mklist.name = 'NumberList' list_types = {'NumberList': mklist} emitc('p_mklist_ok', lambda: P('L: {n:NumberList}', 'L: 1, 2', list_types)) try: emitc('p_mklist_letters', lambda: P('L: {n:NumberList}', 'L: a, b', list_types)) except Exception as exc: emit('p_mklist_letters', '!%s' % type(exc).__name__) # The enum and the choice through the parser, including the IGNORECASE match # that the enum's `lower()' fallback exists to absorb and the choice's does not. enum_types = {'YesNo': yesno, 'Color': choice, 'ColorL': lowered} emitc('p_enum_yes', lambda: P('A: {a:YesNo}', 'A: yes', enum_types)) emitc('p_enum_upper', lambda: P('A: {a:YesNo}', 'A: YES', enum_types)) emitc('p_enum_nonmember', lambda: P('A: {a:YesNo}', 'A: maybe', enum_types)) emitc('p_choice_red', lambda: P('C: {c:Color}', 'C: red', enum_types)) emitc('p_choice_pink', lambda: P('C: {c:Color}', 'C: pink', enum_types)) # THE PARSER MATCHES `RED' AND THE CONVERTER THEN RAISES -- the ValueError # escapes parse.parse. With `transform=str.lower' the same input converts. try: emitc('p_choice_upper', lambda: P('C: {c:Color}', 'C: RED', enum_types)) except Exception as exc: emit('p_choice_upper', '!%s' % type(exc).__name__) emitc('p_choice_transform_upper', lambda: P('C: {c:ColorL}', 'C: RED', enum_types)) variant.name = 'NumberOrYesNo' emitc('p_variant_number', lambda: P('V: {v:NumberOrYesNo}', 'V: 42', {'NumberOrYesNo': variant})) emitc('p_variant_yes', lambda: P('V: {v:NumberOrYesNo}', 'V: yes', {'NumberOrYesNo': variant})) emitc('p_variant_nomatch', lambda: P('V: {v:NumberOrYesNo}', 'V: zzz', {'NumberOrYesNo': variant})) # `parse.search' and `parse.findall' take the same type dict -- the derived # converters are ordinary parse types and nothing about them is parse()-only. sr = parse.search('L: {n:Number+}', 'noise L: 1, 2 more', extra_types=type_dict) emit('p_search', 'None' if sr is None else repr(sr.named)) emit('p_findall', repr([r.named['n'] for r in parse.findall('<{n:Number}>', '<1> and <2>', extra_types=type_dict)])) # ------------------------------------------------------- cardinality_field --- # THIS LAYER IMPORTS `six', WHICH THE RECIPE DOES NOT DECLARE -- see the suite # header. It is imported HERE rather than at the top so that an image without # python3-six turns these cases red and leaves every case above green, which is # what makes the missing RDEPENDS legible instead of turning the suite blank. try: from parse_type.cardinality_field import (CardinalityField, CardinalityFieldTypeBuilder, MissingTypeError) emit('cf_import', 'ok') except Exception as exc: emit('cf_import', '!%s' % type(exc).__name__) CardinalityField = None if CardinalityField is not None: emit('cf_pattern_chars', CardinalityField.pattern_chars) emit('cf_matches_plus', 'yes' if CardinalityField.matches_type('Number+') else 'no') emit('cf_matches_star', 'yes' if CardinalityField.matches_type('Number*') else 'no') emit('cf_matches_question', 'yes' if CardinalityField.matches_type('Number?') else 'no') emit('cf_matches_bare', 'yes' if CardinalityField.matches_type('Number') else 'no') # An EMPTY type name is falsy, so `matches_type' returns `''' -- not False. # Emitted as the boolean the caller almost certainly wanted. emit('cf_matches_empty', 'yes' if CardinalityField.matches_type('') else 'no') emit('cf_split_plus_name', CardinalityField.split_type('Number+')[0]) emit('cf_split_plus_card', CardinalityField.split_type('Number+')[1].name) emit('cf_split_star_name', CardinalityField.split_type('Number*')[0]) emit('cf_split_star_card', CardinalityField.split_type('Number*')[1].name) emit('cf_split_question_card', CardinalityField.split_type('Number?')[1].name) emit('cf_split_bare_name', CardinalityField.split_type('Number')[0]) emit('cf_split_bare_card', CardinalityField.split_type('Number')[1].name) emit('cf_make_one', CardinalityField.make_type('Number', Cardinality.one)) emit('cf_make_opt', CardinalityField.make_type('Number', Cardinality.optional)) emit('cf_make_many', CardinalityField.make_type('Number', Cardinality.many)) emit('cf_make_many0', CardinalityField.make_type('Number', Cardinality.many0)) emit('cf_roundtrip', 'yes' if CardinalityField.split_type( CardinalityField.make_type('Number', Cardinality.many))[1] is Cardinality.many else 'no') tv = CardinalityFieldTypeBuilder.create_type_variant( 'Number*', dict(Number=parse_number)) emit('cfb_variant_name', tv.name) emit('cfb_variant_pattern', tv.pattern) emitc('cfb_variant_value', lambda: repr(tv('1, 2'))) tvc = CardinalityFieldTypeBuilder.create_type_variant('Number+', parse_number) emit('cfb_direct_converter_name', tvc.name) emitc('cfb_direct_converter_value', lambda: repr(tvc('3, 4'))) # A bare name has no cardinality field -> ValueError, and a name whose base # is not in the dict -> MissingTypeError, which SUBCLASSES KeyError, so its # `str()' is the repr-quoted name rather than a sentence. emit('cfb_no_field', call(CardinalityFieldTypeBuilder.create_type_variant, 'Number', dict(Number=parse_number))) emit('cfb_no_field_msg', msg(CardinalityFieldTypeBuilder.create_type_variant, 'Number', dict(Number=parse_number))) emit('cfb_missing', call(CardinalityFieldTypeBuilder.create_type_variant, 'Other+', dict(Number=parse_number))) emit('cfb_missing_is_keyerror', 'yes' if issubclass(MissingTypeError, KeyError) else 'no') variants = CardinalityFieldTypeBuilder.create_type_variants( ['Number?', 'Number+'], dict(Number=parse_number)) emit('cfb_variants_keys', ','.join(sorted(variants))) missing_variants = CardinalityFieldTypeBuilder.create_missing_type_variants( ['Number', 'Number+'], dict(Number=parse_number)) emit('cfb_missing_variants_keys', ','.join(sorted(missing_variants))) emit('cfb_listsep', CardinalityFieldTypeBuilder.listsep) # ------------------------------------------------------------------ cfparse -- # The Parser that ties it together: it reads the cardinality suffix out of the # FORMAT and creates the variant itself, so a caller writes `{n:Number+}' and # supplies only `Number'. try: from parse_type.cfparse import Parser as CFParser emit('cfparse_import', 'ok') except Exception as exc: emit('cfparse_import', '!%s' % type(exc).__name__) CFParser = None if CFParser is not None: cfp = CFParser('L: {n:Number+}', dict(Number=parse_number)) emit('cfparse_many', repr(cfp.parse('L: 1, 2, 3').named['n'])) emit('cfparse_no_match', 'None' if cfp.parse('L: a, b') is None else 'matched') cfp_opt = CFParser('x={n:Number?}y', dict(Number=parse_number)) emit('cfparse_opt_present', repr(cfp_opt.parse('x=42y').named['n'])) emit('cfparse_opt_absent', repr(cfp_opt.parse('x=y').named['n'])) cfp0 = CFParser('x={n:Number*}y', dict(Number=parse_number)) emit('cfparse_many0_absent', repr(cfp0.parse('x=y').named['n'])) # An UNKNOWN base type is a MissingTypeError at Parser construction, not at # parse time -- the variant is created eagerly. emit('cfparse_missing_type', call(CFParser, 'L: {n:Other+}', dict(Number=parse_number))) # A format with NO cardinality suffix is an ordinary parse format and the # Parser is a plain parse.Parser over it. cfp_one = CFParser('N: {n:Number}', dict(Number=parse_number)) emit('cfparse_one', repr(cfp_one.parse('N: 42').named['n'])) # --------------------------------------------------------------- bookends ---- emit('emitted_count', len(emitted) + 2) emit('probe_done', 'yes')