Opened 51 minutes ago
#37339 new Bug
`GenericIPAddressField` cannot be assigned `ipaddress.IPv4Address/IPv6Address` values
| Reported by: | Thibaut Decombe | Owned by: | |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 6.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Thibaut Decombe | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | yes | UI/UX: | no |
Description
Assigning a value from the stdlib ipaddress module to a GenericIPAddressField fails on save:
>>> obj.ip = ipaddress.ip_address('80.15.186.250') # Is of type IPv4Address('80.15.186.250') >>> obj.save() File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 902, in save self.save_base( File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 1008, in save_base updated = self._save_table( ^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 1138, in _save_table updated = self._do_update( ^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 1203, in _do_update return filtered._update(values) > 0 ^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/query.py", line 1288, in _update return query.get_compiler(self.db).execute_sql(ROW_COUNT) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 2060, in execute_sql row_count = super().execute_sql(result_type) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1610, in execute_sql sql, params = self.as_sql() ^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/backends/mysql/compiler.py", line 49, in as_sql update_query, update_params = super().as_sql() ^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 2023, in as_sql val = field.get_db_prep_save(val, connection=self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 1012, in get_db_prep_save return self.get_db_prep_value(value, connection=connection, prepared=False) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 2284, in get_db_prep_value value = self.get_prep_value(value) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 2291, in get_prep_value if value and ":" in value: ^^^^^^^^^^^^ TypeError: argument of type 'IPv4Address' is not iterable
Since ipaddress is the stdlib's representation of an IP address, and since it is what validate_ipv4_address()/validate_ipv6_address() already use internally, it is a natural type to hold an address in before assigning it to a model.
This looks like an oversight rather than a deliberate restriction, since the field already accepts these values elsewhere, to_python() coerces non-strings, while get_prep_value() does not. Which means it's ok to create using this tpye, but not update
>>> GenericIPAddressField().to_python(ipaddress.ip_address('80.15.186.250')) '80.15.186.250' >>> Bank(ip= IPv4Address('80.15.186.250')).save() # OK >>> Bank.objects.update(ip=IPv4Address('80.15.186.250')) # OK >>> b = Bank.objects.last() >>> b.ip = IPv4Address('80.15.186.250') >>> b.save() File "/home/thibaut/Desktop/.venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 2291, in get_prep_value if value and ":" in value: ^^^^^^^^^^^^ TypeError: argument of type 'IPv4Address' is not iterable
def get_prep_value(self, value): value = super().get_prep_value(value) if value is None: return None if value and ":" in value: # TypeError for any non-str value ... return str(value) # unreachable for non-str values
The trailing str(value) suggests non-string values were meant to be handled, but the membership test runs first.
Coercing before the colon check (or delegating toto_python(), as CharField does) is enough.
I'd be happy to submit a fix