Quodlibet: Alternative query syntax

Created on 5 Mar 2018  路  5Comments  路  Source: quodlibet/quodlibet

The syntax of the query language in QL is IMO a bit unintuitive. This is mostly due to the kind of mixed infix and prefix notation used.

The following are some examples of what I think an alternative syntax could look like (just to give an impression, not any formal spec).

| Current syntax | Alternative syntax |
|---|---|
| artist = \|(Townshend, Who) | artist = Townshend \| Who |
| genre = &(\|(Disco, Jazz), !Acid Jazz) | genre = (Disco \| Jazz) & !Acid Jazz |
| #(10 <= track < 100) | 10 <= #track < 100 |
| &(#(rating>=0.75), ~playlists="") | #rating>=0.75 & ~playlists="" |

This more closely resembles the syntax of conditional statements in languages like Python and C.

The implemention will probably have to be a bit more elaborate than the current one. This may be the reason that the current syntax was chosen?

The alternative syntax could be implemented as a plugin, to allow people to choose which one they want to use.

Kind of related to #821.

enhancement plugins search

All 5 comments

I like it... but yes, I think this would be quite a lot harder than it first seems.

The alternative syntax could be implemented as a plugin, to allow people to choose which one they want to use.

Currently this is not possible, but it's something I've thought about adding (for #821 mainly). That itself (pluggable query syntax) needs quite a lot of work - there are lots of static dependencies on those classes IIRC, although I removed a fair few at one point.

The syntax of the query language in QL is IMO a bit unintuitive.

I'm with you. Even after years of using QL I don't find it natural. Your proposed syntax style would be ideal IMO.

The thing to remember when doing any query, is that plain text (non-structured) queries have to co-exist with almost any structured search, and there are a lot of tests and use-cases to avoid breaking.

Beware also "special" characters being allowed in these plain text searches, e.g.

  • that _Earth, Wind & Fire_ is probably not a conjunction between terms Earth, Wind and Fire
  • _Snap!_ is no weird negation, nor is the title _Oh! Darling_

    • who knows what to do wtih the artist )E|3( (aka _Bad Company_)...

Not saying it's impossible, but the parser will get more complex without the unambiguity of the parentheses.

Good points. A solution may perhaps be to require (single) quotes around sentences that contain special characters, or at least some kind of escaping.

I agree that this will not be easy, but I also think having a more user-friendly query syntax justifies having a more complex parser.

Would be cool, if we could somehow make use of Python's ast module (abstract syntax trees) to get a query language that's mostly based on Python's syntax. (The ast.parse function calls compile which is written in C, so it should be pretty fast.)

I don't know how the query syntax system works, but I'd definitely guess that the hard part would be making use of the resulting syntax tree. (Not that parsing is easy.) So this is likely just for fun:

def pp(input):
    # the following replace would have to be smarter, of course, 
    # since one might want to search for "month ago"
    input = input.replace('month ago', '* 30').strip()
    # pprint from https://github.com/asottile/astpretty
    pprint(ast.parse(input).body[0]) 

Examples:

# I guess min(~playcount) could also work.
>>> pp('''  playcount.min == 0 and added < 5 month ago   ''')
Expr(
    lineno=1,
    col_offset=0,
    value=BoolOp(
        lineno=1,
        col_offset=0,
        op=And(),
        values=[
            Compare(
                lineno=1,
                col_offset=0,
                left=Attribute(
                    lineno=1,
                    col_offset=0,
                    value=Name(lineno=1, col_offset=0, id='playcount', ctx=Load()),
                    attr='min',
                    ctx=Load(),
                ),
                ops=[Eq()],
                comparators=[Num(lineno=1, col_offset=17, n=0)],
            ),
            Compare(
                lineno=1,
                col_offset=23,
                left=Name(lineno=1, col_offset=23, id='added', ctx=Load()),
                ops=[Lt()],
                comparators=[
                    BinOp(
                        lineno=1,
                        col_offset=31,
                        left=Num(lineno=1, col_offset=31, n=5),
                        op=Mult(),
                        right=Num(lineno=1, col_offset=34, n=30),
                    ),
                ],
            ),
        ],
    ),
)


>>> pp('''  genre in {disco, jazz, not "acid jazz"}   ''')
Expr(
    lineno=1,
    col_offset=0,
    value=Compare(
        lineno=1,
        col_offset=0,
        left=Name(lineno=1, col_offset=0, id='genre', ctx=Load()),
        ops=[In()],
        comparators=[
            Set(
                lineno=1,
                col_offset=9,
                elts=[
                    Name(lineno=1, col_offset=10, id='disco', ctx=Load()),
                    Name(lineno=1, col_offset=17, id='jazz', ctx=Load()),
                    UnaryOp(
                        lineno=1,
                        col_offset=23,
                        op=Not(),
                        operand=Str(lineno=1, col_offset=27, s='acid jazz'),
                    ),
                ],
            ),
        ],
    ),
)


>>> pp('''  10 <= ~track < 100   ''')
Expr(
    lineno=1,
    col_offset=0,
    value=Compare(
        lineno=1,
        col_offset=0,
        left=Num(lineno=1, col_offset=0, n=10),
        ops=[
            LtE(),
            Lt(),
        ],
        comparators=[
            UnaryOp(
                lineno=1,
                col_offset=6,
                op=Invert(),
                operand=Name(lineno=1, col_offset=7, id='track', ctx=Load()),
            ),
            Num(lineno=1, col_offset=15, n=100),
        ],
    ),
)

And some more ideas (unrelated to the above) for a new query system: #3429 #3430

Was this page helpful?
0 / 5 - 0 ratings