# HG changeset patch # User David A. Holland # Date 1648948381 14400 # Node ID c0be30249ffe32f6e552dc1f763d0fe0c41bb641 # Parent ef6d572c4e1e8ebffff4c34a4d04e1c9705bb799 Fix the argument handling. diff -r ef6d572c4e1e -r c0be30249ffe shelltools/query-pr/query.py --- a/shelltools/query-pr/query.py Sat Apr 02 18:14:40 2022 -0400 +++ b/shelltools/query-pr/query.py Sat Apr 02 21:13:01 2022 -0400 @@ -4,9 +4,6 @@ import argparse import psycopg2 -program_description = """ -Search for and retrieve problem reports. -""" program_version = "@VERSION@" ############################################################ @@ -17,6 +14,20 @@ outformat = "text" ############################################################ +# simple dump widget + +class Dumper: + def __init__(self, f): + self.f = f + self.indentation = 0 + def indent(self): + self.indentation += 3 + def unindent(self): + self.indentation -= 3 + def write(self, msg): + self.f.write(" " * self.indentation + msg + "\n") + +############################################################ # database field access # @@ -667,62 +678,44 @@ # end print_attachment ############################################################ -# AST for query +# AST for input query class Invocation: + Q_TERM = 1 + Q_SQL = 2 class Query: - Q_TERM = 1 # XXX unused so far - Q_SQL = 2 - Q_AND = 3 - Q_OR = 4 - Q_TSTRING = 5 - Q_QSTRING = 6 - def __init__(self, type): self.type = type - def doterm(term): - self = Query(Q_TERM) - self.term = term - return self - def dosql(s): - self = Query(Q_SQL) - self.sql = s - return self - def doand(qs): - self = Query(Q_AND) - self.args = qs - return self - def door(qs): - self = Query(Q_OR) - self.args = qs - return self - # query term string - def dotstring(q): - self = Query(Q_TSTRING) - self.string = q - return self - # whole query string - def doqstring(q): - self = Query(Q_QSTRING) - self.string = q - return self - # end class Query + def dump(self, d): + if self.type == Invocation.Q_TERM: + d.write("query.term({})".format(self.term)) + else: + d.write("query.sql({})".format(self.sql)) + def mkterm(term): + self = Invocation.Query(Invocation.Q_TERM) + self.term = term + return self + def mksql(s): + self = Invocation.Query(Invocation.Q_SQL) + self.sql = s + return self class Order: def __init__(self, field, rev = False): self.field = field self.rev = rev - def dooldest(ign): - return Order("number") - def donewest(ign): - return Order("number", True) - def dostaleness(ign): - return Order("modified_date", True) - def dofield(field): - return Order(field) - def dorevfield(field): - return Order(field, True) - # end class Order + def dump(self, d): + d.write("order({}, {})".format(self.field, self.rev)) + def mkoldest(): + return Invocation.Order("number") + def mknewest(): + return Invocation.Order("number", True) + def mkstaleness(): + return Invocation.Order("modified_date", True) + def mkfield(field): + return Invocation.Order(field) + def mkrevfield(field): + return Invocation.Order(field, True) class Search: def __init__(self, qs, openonly, publiconly, os): @@ -730,59 +723,98 @@ self.openonly = openonly self.publiconly = publiconly self.orders = os - # end class Search + def dump(self, d): + d.write("search({}, {})".format( + "open" if self.openonly else "closed", + "public" if self.publiconly else "privileged")) + d.indent() + d.write("queries") + d.indent() + for query in self.queries: + query.dump(d) + d.unindent() + d.write("orders") + d.indent() + for order in self.orders: + order.dump(d) + d.unindent() + d.unindent() + S_PR = 1 + S_MESSAGE = 2 + S_ATTACHMENT = 3 class Selection: - S_PR = 1 - S_MESSAGE = 2 - S_ATTACHMENT = 3 - def __init__(self, type): self.type = type - def dopr(output, outformat): - self = Selection(S_PR) - self.output = output - self.outformat = outformat - return self - def domessage(arg): - self = Selection(S_MESSAGE) - self.message = arg - return self - def doattachment(arg): - self = Selection(S_ATTACHMENT) - self.attachment = arg - return self - # end class Selection + def dump(self, d): + if self.type == Invocation.S_PR: + d.write("selection.pr({}, {})".format( + self.output, self.outformat)) + elif self.type == Invocation.S_MESSAGE: + d.write("selection.message({})".format( + self.message)) + else: + d.write("selection.attachment({})".format( + self.attachment)) + def mkpr(output, outformat): + self = Invocation.Selection(Invocation.S_PR) + self.output = output + self.outformat = outformat + return self + def mkmessage(arg): + self = Invocation.Selection(Invocation.S_MESSAGE) + self.message = arg + return self + def mkattachment(arg): + self = Invocation.Selection(Invocation.S_ATTACHMENT) + self.attachment = arg + return self + OP_FIELDS = 1 + OP_SHOW = 2 + OP_RANGE = 3 + OP_SEARCH = 4 class Op: - # operation codes - OP_FIELDS = 1 - OP_SHOW = 2 - OP_RANGE = 3 - OP_SEARCH = 4 - def __init__(self, type): self.type = type - - def dofields(): - return Op(OP_FIELDS) - def doshow(field): - self = Op(OP_SHOW) - self.field = field - return self - def dorange(field): - self = Op(OP_RANGE) - self.field = field - return self - def dosearch(s, sels): - self = Op(OP_SEARCH) - self.search = s - self.sels = sels - return self - # end class Op + def dump(self, d): + if self.type == Invocation.OP_FIELDS: + d.write("op.fields") + elif self.type == Invocation.OP_SHOW: + d.write("op.show({})".format(self.field)) + elif self.type == Invocation.OP_RANGE: + d.write("op.range({})".format(self.field)) + else: + d.write("op.search:") + d.indent() + self.search.dump(d) + for sel in self.sels: + sel.dump(d) + d.unindent() + def mkfields(): + return Invocation.Op(Invocation.OP_FIELDS) + def mkshow(field): + self = Invocation.Op(Invocation.OP_SHOW) + self.field = field + return self + def mkrange(field): + self = Invocation.Op(Invocation.OP_RANGE) + self.field = field + return self + def mksearch(s, sels): + self = Invocation.Op(Invocation.OP_SEARCH) + self.search = s + self.sels = sels + return self def __init__(self, ops): self.ops = ops + def dump(self, d): + d.write("invocation: {} ops".format(len(self.ops))) + d.indent() + for op in self.ops: + op.dump(d) + d.unindent() # end class Invocation ############################################################ @@ -969,183 +1001,341 @@ # arg handling # -# I swear, all getopt interfaces suck. -# -# Provide an argparse action for constructing something out of the -# argument value and appending that somewhere, since it can't do this -# on its own. -# -# The way you use this: -# p.add_argument("--foo", action = CtorAppend, dest = 'mylist', -# const = ctor) -# where ctor is a function taking the option arg(s) and producing -# a value to append to mylist. -# -# This itself is mangy even for what it is -- it seems like we should -# be able to pass action=CtorAppend(ctor), but since it has to be a -# class that doesn't work... unless you make a new class for every -# ctor you want to use, which seems completely insane. +# I swear, all getopt interfaces suck. You have to write your own to +# not go mad. # -class CtorAppend(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - items = getattr(namespace, self.dest) - item = self.const(values) - items.append(item) - setattr(namespace, self.dest, items) -def getargs(): - p = argparse.ArgumentParser(program_description) - - # note: -h/--help is built in by default - p.add_argument("-v", "--version", - action='version', version=program_version, - help="Print program version and exit") - - p.add_argument("--show", nargs=1, - action=CtorAppend, dest='ops', - const=Invocation.Op.doshow, - help="Show description of field") - p.add_argument("--range", nargs=1, - action=CtorAppend, dest='ops', - const=Invocation.Op.dorange, - help="Show range of extant values for field") +# Usage. +def usage(): + sys.stderr.write(""" +query-pr: search for and retrieve problem reports +usage: query-pr [options] [searchterms] Query the database. + query-pr [options] --sql QUERY Execute QUERY as the search. + query-pr [options] -s QUERY Same as --sql. + query-pr --fields List database fields. + query-pr --show FIELD Print info about database field. + query-pr --range FIELD Print extant range for database field. + query-pr --help / -h Print this message. + query-pr --version / -v Print version and exit. +options: + --search-string STRING Forcibly treat STRING as a search term. + --message NUM / -m NUM Print a message by its ID number. + --attachment NUM / -a NUM Print an attachment by its ID number. + --paranoid Deny unsafe settings. +filter options: + --open Exclude closed PRs. (default) + --closed Include closed PRs. + --public Exclude confidential PRs. + --privileged Include confidential PRs. (default) +sort options: + --oldest Sort with oldest PRs first. (default) + --newest Sort with newest PRs first. + --staleness Sort by last modification time. + --orderby FIELD Sort by specific field. +output options: + --raw / -r Print raw SQL output. + --list / -l Print in list form. + --headers Print headers only. + --meta / --metadata Print all metadata. + --full / -f Print entire PR. + --text Print text. (default) + --csv Print CSV. + --xml Print XML. + --json Print JSON. + --rdf Print RDF. + --rdflike Print RDF-like text. +search terms: + NUM Single PR by number. + NUM-[NUM] Range of PRs by number. + TEXT Search string + FIELD:TEXT Search string for a particular field. + FIELD: Use the field's default search string. +derived fields: + arrived-before:DATE Arrival date before DATE. + arrived-after:DATE Arrival date after DATE. + closed-before:DATE Close date before DATE. + closed-after:DATE Close date after DATE, or none. + last-modified-before:DATE Last modified before DATE. + last-modified-after:DATE Last modified after DATE. + stale:TIME Last modified at least TIME ago. +Explicit SQL queries should return lists of PR numbers (only). +""") - p.add_argument("--search", nargs=1, - action=CtorAppend, dest='queries', - const=Invocation.Query.doqstring, - help="Force string to be read as a search string") - p.add_argument("-s", "--sql", nargs=1, - action=CtorAppend, dest='queries', - const=Invocation.Query.dosql, - help="Supply explicit sql query as search") +# Widget to hold argv and allow peeling args off one at a time. +class ArgHolder: + def __init__(self, argv): + self.argc = len(argv) + self.argv = argv + self.pos = 1 + def next(self): + if self.pos >= self.argc: + return None + ret = self.argv[self.pos] + self.pos += 1 + return ret + def getarg(self, opt): + ret = self.next() + if ret is None: + msg = "Option {} requires an argument\n".format(opt) + sys.stderr.write(msg) + exit(1) + return ret + +# Read the argument list and convert it into an Invocation. +def getargs(argv): + # Results + ops = [] + orders = [] + queries = [] + selections = [] + output = "LIST" + outformat = "TEXT" + openonly = True + publiconly = False + paranoid = False + nomoreoptions = False + + # + # Functions for the options + # + + def do_paranoid(): + nonlocal paranoid, publiconly + paranoid = True + publiconly = True - p.add_argument("--open", - action='store_const', dest='openonly', const="True", - help="Exclude closed PRs (default)") - p.add_argument("--closed", - action='store_const', dest='openonly', const="False", - help="Include closed PRs in search") - p.add_argument("--public", - action='store_const', dest='publiconly', const="True", - help="Exclude confidential PRs") - p.add_argument("--privileged", - action='store_const', dest='publiconly', const="False", - help="Allow confidential PRs (default)") + def do_help(): + usage() + exit(0) + def do_version(): + msg = "query-pr {}\n".format(program_version) + sys.stdout.write(msg) + exit(0) + def do_fields(): + ops.append(Invocation.mkfields()) + def do_show(field): + ops.append(Invocation.mkshow(field)) + def do_range(field): + ops.append(Invocation.mkrange(field)) + + def do_search(term): + queries.append(Invocation.mkterm(term)) + def do_sql(text): + assert(not paranoid) + queries.append(Invocation.mksql(text)) - p.add_argument("--oldest", - action=CtorAppend, dest='orders', - const=Invocation.Order.dooldest, - help="Sort output with oldest PRs first") - p.add_argument("--newest", - action=CtorAppend, dest='orders', - const=Invocation.Order.donewest, - help="Sort output with newest PRs first") - p.add_argument("--staleness", - action=CtorAppend, dest='orders', - const=Invocation.Order.dostaleness, - help="Sort output by time since last modification") - p.add_argument("--orderby", nargs=1, - action=CtorAppend, dest='orders', - const=Invocation.Order.dofield, - help="Sort output by specific field") - p.add_argument("--revorderby", nargs=1, - action=CtorAppend, dest='orders', - const=Invocation.Order.dorevfield, - help="Sort output by specific field, reversed") + def do_open(): + nonlocal openonly + openonly = True + def do_closed(): + nonlocal openonly + openonly = False + def do_public(): + nonlocal publiconly + publiconly = True + def do_privileged(): + nonlocal publiconly + assert(not paranoid) + publiconly = False + + def do_oldest(): + orders.append(Invocation.mkoldest()) + def do_newest(): + orders.append(Invocation.mknewest()) + def do_staleness(): + orders.append(Invocation.mkstaleness()) + def do_orderby(field): + orders.append(Invocation.mkfield(field)) + def do_revorderby(field): + orders.append(Invocation.mkrevfield(field)) + + def do_message(n): + selections.append(Invocation.mkmessage(n)) + def do_attachment(n): + selections.append(Invocation.mkattachment(n)) - p.add_argument("-m", "--message", nargs=1, - action=CtorAppend, dest='selections', - const=Invocation.Selection.domessage, - help="Print selected message (single PR only)") - p.add_argument("-a", "--attachment", nargs=1, - action=CtorAppend, dest='selections', - const=Invocation.Selection.doattachment, - help="Print selected attachment (single PR only)") + def do_raw(): + nonlocal output + output = "RAW" + def do_list(): + nonlocal output + output = "LIST" + def do_headers(): + nonlocal output + output = "HEADERS" + def do_meta(): + nonlocal output + output = "META" + def do_full(): + nonlocal output + output = "FULL" - p.add_argument("-r", "--raw", - action = 'store_const', const="RAW", - dest = 'output', - help="Print exactly what the database returns") - p.add_argument("-l", "--list", - action = 'store_const', const="LIST", - dest = 'output', - help="Print in list form (default)") - p.add_argument("--headers", - action = 'store_const', const="HEADERS", - dest = 'output', - help="Print header information only") - p.add_argument("--meta", - action = 'store_const', const="META", - dest = 'output', - help="Print all metadata") - p.add_argument("--metadata", - action = 'store_const', const="META", - dest = 'output') - p.add_argument("-f", "--full", - action = 'store_const', const="FULL", - dest = 'output', - help="Print everything") + def do_text(): + nonlocal outformat + outformat = "TEXT" + def do_csv(): + nonlocal outformat + outformat = "CSV" + def do_xml(): + nonlocal outformat + outformat = "XML" + def do_json(): + nonlocal outformat + outformat = "JSON" + def do_rdf(): + nonlocal outformat + outformat = "RDF" + def do_rdflike(): + nonlocal outformat + outformat = "RDFLIKE" + + def do_unknown(opt): + sys.stderr.write("Unknown option {}\n".format(opt)) + exit(1) + + args = ArgHolder(argv) + while True: + arg = args.next() + if arg is None: + break - p.add_argument("--text", - action = 'store_const', const="TEXT", - dest = 'outformat', - help="Print in text format (default)") - p.add_argument("--csv", - action = 'store_const', const="CSV", - dest = 'outformat', - help="Print a CSV file") - p.add_argument("--xml", - action = 'store_const', const="XML", - dest = 'outformat', - help="Print in XML") - p.add_argument("--json", - action = 'store_const', const="JSON", - dest = 'outformat', - help="Print in JSON") - p.add_argument("--rdf", - action = 'store_const', const="RDF", - dest = 'outbformat', - help="Print in RDF") - p.add_argument("--rdflike", - action = 'store_const', const="RDFLIKE", - dest = 'outformat', - help="Print RDF-like text") - - p.add_argument("TERM", nargs='*', - action=CtorAppend, dest='queries', - const=Invocation.Query.doqstring, - help="Search term") + if nomoreoptions or arg[0] != "-": + do_search(arg) + elif arg == "--": + nomoreoptions = True + # Long options + elif arg == "--attachment": + do_attachment(args.getarg(arg)) + elif arg.startswith("--attachment="): + do_message(arg[13:]) + elif arg == "--closed": + do_closed() + elif arg == "--csv": + do_csv() + elif arg == "--fields": + do_fields() + elif arg == "--full": + do_full() + elif arg == "--headers": + do_headers() + elif arg == "--help": + do_help() + elif arg == "--json": + do_json() + elif arg == "--list": + do_list() + elif arg == "--message": + do_message(args.getarg(arg)) + elif arg.startswith("--message="): + do_message(arg[10:]) + elif arg == "--meta": + do_meta() + elif arg == "--metadata": + do_meta() + elif arg == "--newest": + do_newest() + elif arg == "--oldest": + do_oldest() + elif arg == "--orderby": + do_orderby(args.getarg(arg)) + elif arg.startswith("--orderby="): + do_orderby(arg[10:]) + elif arg == "--open": + do_open() + elif arg == "--paranoid": + do_paranoid() + elif arg == "--public": + do_public() + elif arg == "--privileged" and not paranoid: + do_privileged() + elif arg == "--range": + do_range(args.getarg(arg)) + elif arg.startswith("--range="): + do_range(arg[8:]) + elif arg == "--raw": + do_raw() + elif arg == "--rdf": + do_rdf() + elif arg == "--rdflike": + do_rdflike() + elif arg == "--revorderby": + do_revorderby(args.getarg(arg)) + elif arg.startswith("--revorderby="): + do_revorderby(arg[13:]) + elif arg == "--search": + do_search(args.getarg(arg)) + elif arg.startswith("--search="): + do_search(arg[9:]) + elif arg == "--show": + do_show(args.getarg(arg)) + elif arg.startswith("--show="): + do_show(arg[7:]) + elif arg == "--sql" and not paranoid: + do_sql(args.getarg(arg)) + elif arg.startswith("--sql=") and not paranoid: + do_sql(arg[7:]) + elif arg == "--staleness": + do_staleness() + elif arg == "--text": + do_text() + elif arg == "--version": + do_version() + elif arg == "--xml": + do_xml() + elif arg.startswith("--"): + do_unknown(arg) + else: + # short options + i = 1 + n = len(arg) + while i < n: + opt = arg[i] + i += 1 + def getarg(): + nonlocal i + if i < n: + ret = arg[i:] + else: + ret = args.getarg("-" + opt) + i = n + return ret - args = p.parse_args() + if opt == "a": + do_attachment(getarg()) + elif opt == "f": + do_full() + elif opt == "h": + do_help() + elif opt == "l": + do_list() + elif opt == "m": + do_message(getarg()) + elif opt == "s" and not paranoid: + do_sql(getarg()) + elif opt == "r": + do_raw() + elif opt == "v": + do_version() + else: + do_unknown("-" + opt) - ops = args.ops - if ops is None: - ops = [] - queries = args.queries - if queries is not None: - openonly = args.openonly - if openonly is None: - openonly = True - publiconly = args.publiconly - if publiconly is None: - publiconly = False - orders = args.orders - if orders is None: - orders = [Invocation.Order.dooldest(None)] - output = args.output - if output is None: - output = "LIST" - outformat = args.outformat - if outformat is None: - outformat = "TEXT" - selections = args.selections - if selections is None: - sel = Invocation.Selection.dopr(output, outformat) - selections = [sel] - search = Search(queries, openonly, publiconly, orders) - op = dosearch(search, selections) - ops.append(op) - # endif + # Now convert what we got to a single thing. + if queries != []: + if orders == []: + orders = [Invocation.mkoldest()] + if selections == []: + selections = [Invocation.mkpr(output, outformat)] + search = Invocation.Search(queries, openonly, publiconly, orders) + ops.append(Invocation.mksearch(search, selections)) + else: + if orders != []: + msg = "No queries given for requested orderings\n" + sys.stderr.write(msg) + exit(1) + if selections != []: + msg = "No queries given for requested selections\n" + sys.stderr.write(msg) + exit(1) return Invocation(ops) # end getargs @@ -1153,7 +1343,9 @@ ############################################################ # main -todo = getargs() +todo = getargs(sys.argv) +#todo.dump(Dumper(sys.stdout)) + opendb() fetch_classifications() todo = compile(todo)