Issue: Issue on postgres_ext.ArrayField python_value conversion
Author: whisust
State: closed
Created: 2021-02-14 11:01:32

The `ArrayField.python_value` functions seems to misuse the conversion function in the case of a `convert_value=True` field:

Given:
```python
import uuid
from uuid import UUID
from peewee import *
from typing import List



class Program(Model):
    """
    SQL:
    CREATE TABLE IF NOT EXISTS programs
    (
        id                 UUID PRIMARY KEY   DEFAULT uuid_generate_v4(),
        course_ids         UUID[]             DEFAULT ARRAY[]::UUID[]
    );
    """
    id = UUIDField(primary_key=True, default=uuid.uuid4)
    course_ids = ArrayField(field_class=UUIDField, null=False, convert_values=True, default=[])
    
    class Meta:
        primary_key = False


def update(program_id: UUID, course_ids: List[UUID]) -> Program: 
    return Program.update(course_ids=course_ids)\
        .where(Program.id == program_id)\
        .returning(Program)\
        .execute()[0]

new_program = Program.insert().returning(Program).execute()[0]   # fails during the `course_ids` conversion function if convert_value=True, cf capture n1
new_program = update(new_program.id, [uuid.uuid4(), uuid.uuid4()])  # fails DB-side if convert_values=False, cf capture n2

```

when I create my Program with the defaults, in the function

```python
def python_value(self, value):
    if self.convert_values and value is not None:
        conv = self.__field.python_value
        if isinstance(value, list):
            return self._process(conv, value, self.dimensions) 
        else:
            return conv(value)
    else:
        return value
```

we have those fields:
![Screenshot 2021-02-14 at 11 27 04](https://user-images.githubusercontent.com/22323709/107874451-1f1e8b80-6eba-11eb-9398-abf9febc7c1b.png)

which translate in the call of `return conv(value)` as:
![Screenshot 2021-02-14 at 11 51 06](https://user-images.githubusercontent.com/22323709/107874616-0662a580-6ebb-11eb-9219-fb38641e59a5.png)

That triggers a 
```
ValueError: badly formed hexadecimal UUID string
```
which is expected from a `UUID('{}')` call


Reading the 3 returns present in the function, either we return:
  - from l.198 with an equivalent of `[conv(v) for v in value]`, which gives us a `List[UUID]`, nice
  - from l.200 with an equivalent of `UUIDField(value)`, which should give us an `UUID`, not nice
  - from l.202 with  the default `value` which is the default, which returns a `str`, ???
  
So, in the same function, we either return a List[T], T or `str`. There is probably some history that I'm missing here, but I can't understand, in terms of types, the `else: return conv(v)`.

My first workaround was to disable the `convert_values`, but then it's also skipped for the `update` call and it translates to an SQL query with:
```
sql = 'UPDATE "programs" SET "course_ids" = %s WHERE "id" = %s
params = [[UUID('571df177-0e30-49cc-8a47-276be6a74697'), UUID('11068552-797e-4f09-afdf-c83a4d8ac184')], $program_id]
```
Which raises
```python
    def execute_sql(self, sql, params=None, commit=SENTINEL):
        logger.debug((sql, params))
        if commit is SENTINEL:
            if self.in_transaction():
                commit = False
            elif self.commit_select:
                commit = True
            else:
                commit = not sql[:6].lower().startswith('select')
    
        with __exception_wrapper__:
            cursor = self.cursor(commit)
            try:
>               cursor.execute(sql, params or ())
E               peewee.ProgrammingError: can't adapt type 'UUID'

../../venvs/backend/lib/python3.8/site-packages/peewee.py:3129: ProgrammingError

```

I'll try with the conversion disabled and a bit of custom code in the update function to convert in strings, explicitly casted in UUID[] array after

I've tried switching the `UUIDField` in a `CharField` for the `course_ids` in the example, and the result of the converter is the following:
![Screenshot 2021-02-14 at 12 19 27](https://user-images.githubusercontent.com/22323709/107875270-43c93200-6ebf-11eb-8166-584be078e693.png)

Types are ok because, indeed `{something1,something2}` is a valid string, but then
```
> type(new_program.course_ids) == str
True
```

when it's suppose to be a List[str]

This feels like a psycopg2 bug rather than a peewee issue. Why is the value returned by the cursor a string (`'{}'`) and not an empty list? With other data-types psycopg2 tends to handle this correctly - returning us an empty list. But for the `[]UUID` data-type it returns empty arrays as the literal string `'{}'`.

Here is an example using an array of varchar, which works fine:

```python
class T(Model):
    id = UUIDField(primary_key=True, default=uuid.uuid4, constraints=[                                            
        SQL('default gen_random_uuid()')])
    vals = ArrayField(CharField, null=False, convert_values=True,                                                 
                      default=[], constraints=[SQL('default array[]::character varying[]')])                                  

t = T.create()
print(t.id, t.vals)

t = T.insert().execute()
print(t)

for row in T.select():
    print(row.id, row.vals)

t = T.insert().returning(T).execute()[0]
print(t)
```

All of the above work fine, and the output from one run is:

```
9237cac9-6cf3-479a-84c4-62bdd2ef83f2 []
79aa4346-19fa-4db5-8a36-0663ba405fe3
9237cac9-6cf3-479a-84c4-62bdd2ef83f2 []
79aa4346-19fa-4db5-8a36-0663ba405fe3 []
6984a525-e5b6-41cc-a9b9-ec83afcf5c58
```

As you noted, changing this to a UUID causes a problem with the empty array:

```python
    # Change "vals" to array of UUID.
class T...
    vals = ArrayField(UUIDField, null=False, convert_values=True,
                      default=[],
                      constraints=[SQL('default array[]::uuid[]')])

# These both work:
t = T.create()
t = T.insert().execute()

# These both fail with the "ValueError: badly formed hexadecimal UUID string":
for row in T.select():
    print(row.id, row.vals)

t = T.insert().returning(T).execute()[0]
```

I would raise this with the psycopg2 developers. The driver should be returning us that empty list as a standard python list and definitely not as a string. A workaround you can probably use in the meantime is to subclass `ArrayField`:

```python

class SafeArrayField(ArrayField):
    def python_value(self, value):
        if isinstance(value, str) and value == '{}':
            return []
        return super(SafeArrayField, self).python_value(value)
```

When using the above implementation, then all the above code works correctly.