Issue: Support frozenset for IN / NOT IN queries
Author: conqp
State: closed
Created: 2018-09-18 14:15:54

So, today I encountered, that `frozenset`s seems not to be supported as type for `<<` (`IN`)  queries:

```
>>> list(BaseChart.select().join(GroupBaseChart).where((BaseChart.trashed == 0) & (GroupBaseChart.group << frozenset())))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 5896, in __iter__
    self.execute()
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1587, in inner
    return method(self, database, *args, **kwargs)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1658, in execute
    return self._execute(database)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1809, in _execute
    cursor = database.execute(self)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2665, in execute
    sql, params = ctx.sql(query).query()
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 558, in sql
    return obj.__sql__(self)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 2028, in __sql__
    ctx.literal(' WHERE ').sql(self._where)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 558, in sql
    return obj.__sql__(self)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1291, in __sql__
    .sql(self.rhs))
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 558, in sql
    return obj.__sql__(self)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1291, in __sql__
    .sql(self.rhs))
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 562, in sql
    return self.sql(Value(obj))
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 558, in sql
    return obj.__sql__(self)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 1215, in __sql__
    return ctx.value(self.value, self.converter)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 576, in value
    value = self.state.converter(value)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 4454, in db_value
    return self.rel_field.db_value(value)
  File "/usr/local/lib/python3.4/dist-packages/peewee.py", line 3907, in db_value
    return value if value is None else self.adapt(value)
TypeError: int() argument must be a string or a number, not 'frozenset'
>>> list(BaseChart.select().join(GroupBaseChart).where((BaseChart.trashed == 0) & (GroupBaseChart.group << set())))
[]
>>> 
```
The actual use case code, which threw the error in my web application is:
```
class Presentation:
    """Accumulates content for a terminal."""

    def __init__(self, terminal):
        """Sets the respective terminal."""
        self.terminal = terminal
        self.cache = {}

    @property
    def _direct_groups(self):
        """Yields groups this terminal is a member of."""
        for gmt in GroupMemberTerminal.select().where(
                GroupMemberTerminal.terminal == self.terminal):
            yield gmt.group

    @property
    def grouplevels(self):
        """Yields group levels in a breadth-first search."""
        level = frozenset(self._direct_groups)

        while level:
            yield level
            level = frozenset(group.parent for group in level if group.parent)

    @property
    @cached_method()
    @coerce(frozenset)
    def groups(self):
        """Yields all groups in a breadth-first search."""
        for level in self.grouplevels:
            for group in level:
                yield group

    @property
    def groupconfigs(self):
        """Returns a configuration for the terminal's groups."""
        for index, level in enumerate(self.grouplevels):
            try:
                configuration, *superfluous = level_configs(level)
            except ValueError:
                continue

            if superfluous:
                raise AmbiguousConfigurationsError(level, index)

            yield configuration

        raise NoConfigurationFound()

    @property
    def configuration(self):
        """Returns the terminal's configuration."""
        for configuration in Configuration.select().join(
                TerminalConfiguration).where(
                    TerminalConfiguration.terminal == self.terminal):
            return configuration

        for configuration in self.groupconfigs:
            return configuration

        raise NoConfigurationFound()

    @property
    @cached_method()
    @coerce(frozenset)
    def menus(self):
        """Yields menus of this terminal."""
        # Menus directly attached to the terminal.
        yield from Menu.select().join(TerminalMenu).where(
            TerminalMenu.terminal == self.terminal)

        # Menus attached to groups the terminal is a member of.
        yield from Menu.select().join(GroupMenu).where(
            GroupMenu.group << self.groups)

    @property
    @cached_method()
    @coerce(frozenset)
    def base_charts(self):
        """Yields the terminal's base charts."""
        # Charts directy attached to the terminal.
        yield from BaseChart.select().join(TerminalBaseChart).where(
            (BaseChart.trashed == 0)
            & (TerminalBaseChart.terminal == self.terminal))

        # Charts attached to groups, the terminal is a member of.
        yield from BaseChart.select().join(GroupBaseChart).where(
            (BaseChart.trashed == 0) & (GroupBaseChart.group << self.groups))

        # Charts attached to menus of this terminal.
        yield from BaseChart.select().join(MenuItemChart).join(MenuItem).where(
            (BaseChart.trashed == 0) & (MenuItem.menu << self.menus))

    @property
    @cached_method()
    @coerce(frozenset)
    def charts(self):
        """Yields the terminal's charts."""
        for base_chart in self.base_charts:
            try:
                yield base_chart.chart
            except OrphanedBaseChart:
                LOGGER.error('Base chart is orphaned: %s.', base_chart)
            except AmbiguousBaseChart:
                LOGGER.error('Base chart is ambiguous: %s.', base_chart)

    @property
    @cached_method()
    @coerce(frozenset)
    def files(self):
        """Yields the presentation's used file IDs."""
        yield from self.configuration.files

        for chart in self.charts:
            try:
                files = chart.files
            except AttributeError:
                continue

            yield from files

    def to_dom(self):
        """Returns an XML dom presentation."""
        xml = dom.presentation()
        xml.customer = self.terminal.customer.id
        xml.tid = self.terminal.tid
        xml.configuration = self.configuration.to_dom()
        xml.playlist = [chart.to_dom(brief=True) for chart in self.charts]
        xml.menu = [menu.to_dom() for menu in self.menus]
        xml.chart = [chart.to_dom() for chart in self.charts]
        return xml

    def to_json(self):
        """Returns a JSON presentation."""
        return {
            'customer': self.terminal.customer.id,
            'tid': self.terminal.tid,
            'configuration': self.configuration.to_json(),
            'playlist': [
                chart.to_json(mode=ChartMode.BRIEF) for chart in self.charts],
            'menus': [menu.to_json() for menu in self.menus],
            'charts': [chart.to_json() for chart in self.charts]}
```
May support for `frozenset` be added?

Fixed: cfa2fa5f4f8ef170c802065fece3d828b7b6da24

I merged an equivalent patch which also includes tests. I appreciate the ticket and patch.