Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Test

on:
push:
branches: [master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Guard both sides of the peewee 3/4 compatibility line, since
# peewee is unpinned in requirements.txt and a plain `pip install`
# can resolve to either.
peewee-version: ['peewee<4', 'peewee>=4']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: peeweext
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=10
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: peeweext
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=10
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r test-requirements.txt -U
pip install "${{ matrix.peewee-version }}"
- name: Run tests
run: pytest tests
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

## [1.2.9] - 2026-08-14

- Fix `AttributeError: module 'peewee' has no attribute 'basestring'` raised by `Model.save(only=[...])` on peewee>=4, which dropped the py2-compat `basestring` alias.
- Fix `Model.delete_instance(recursive=True)` failing with a foreign key constraint error: nullable FK dependents were never discovered (and thus never nulled out) because `search_nullable` was incorrectly tied to `delete_nullable`.
- Fix `URLValidator` raising an uncaught `ValueError` (instead of `ValidationError`) for malformed bracketed IPv6 hosts like `[::1:2::3]` on Python 3.9+.

## [1.2.8] - 2024-05-15

- Compatible with both MySQLClient and PyMySQL
Expand Down
2 changes: 1 addition & 1 deletion peeweext/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.2.8'
__version__ = '1.2.9'
9 changes: 7 additions & 2 deletions peeweext/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ def delete_instance(self, *args, **kwargs):
recursive = kwargs.get('recursive', False)
delete_nullable = kwargs.get('delete_nullable', False)
if recursive:
dependencies = self.dependencies(delete_nullable)
# search_nullable must always be True here: this only controls
# which dependents peewee *discovers*, and we still need to find
# nullable ones so the branch below can null them out. Whether a
# nullable dependent gets nulled or deleted is governed by
# `delete_nullable` in the loop, not by this call.
dependencies = self.dependencies(True)
for query, fk in reversed(list(dependencies)):
fk_model = fk.model
if fk.null and not delete_nullable:
Expand All @@ -124,7 +129,7 @@ def _validate(self, only=None):
if only:
items = []
for field in only:
if isinstance(field, pw.basestring):
if isinstance(field, str):
name = field
else:
name = field.name
Expand Down
6 changes: 5 additions & 1 deletion peeweext/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ def validate(self, value):
raise
else:
# Now verify IPv6 in the netloc part
try:
netloc = urlsplit(value).netloc
except ValueError: # for example, "Invalid IPv6 URL"
raise ValidationError(self.message)
host_match = re.search(
r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
r'^\[(.+)\](?::\d{2,5})?$', netloc)
if host_match:
potential_ip = host_match.groups()[0]
try:
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ coveralls
codeclimate-test-reporter
coverage
flask
sea
sea>=4.0.0
psycopg2-binary
mysqlclient
32 changes: 32 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,38 @@ def test_validator(table):
note.save()


class _PeeweeWithoutBasestring:
"""Proxies to the real `peewee` module but raises AttributeError for
`basestring`, mimicking peewee>=4 which dropped that py2-compat alias.
Only used to patch the `pw` name inside `peeweext.model`, so peewee's
own internals (which still use `basestring` elsewhere on peewee 3.x)
are left untouched.
"""

def __getattr__(self, name):
if name == 'basestring':
raise AttributeError(name)
return getattr(peewee, name)


def test_save_only_without_peewee_basestring(monkeypatch):
"""Regression test: peewee>=4 removed `peewee.basestring`, which
`Model._validate` used to rely on for `save(only=...)`. Confirm it
still works for both field-name strings and `Field` objects when that
attribute is gone.
"""
monkeypatch.setattr(peeweext.model, 'pw', _PeeweeWithoutBasestring())

Note.create_table()
try:
note = Note.create(message='hello')
note.published_at = pendulum.now()
note.save(only=['published_at'])
note.save(only=[Note.published_at])
finally:
Note.drop_table()


def test_instance_delete(table):
# test delete
note = Note.create(message='Hello')
Expand Down
Loading