Skip to content

Commit

Permalink
Merge 3425e10 into f51f330
Browse files Browse the repository at this point in the history
  • Loading branch information
charettes committed Oct 23, 2022
2 parents f51f330 + 3425e10 commit 49515c2
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 99 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -2,3 +2,4 @@
source = reverse_unique
branch = 1
omit = reverse_unique/test*
relative_files = 1
58 changes: 58 additions & 0 deletions .github/workflows/test.yml
@@ -0,0 +1,58 @@
name: Test

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
- name: Tox tests
run: |
tox -v
- name: Coveralls
uses: AndreMiras/coveralls-python-action@develop
with:
parallel: true
flag-name: Unit Test

coveralls_finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: AndreMiras/coveralls-python-action@develop
with:
parallel-finished: true
47 changes: 0 additions & 47 deletions .travis.yml

This file was deleted.

4 changes: 2 additions & 2 deletions README.rst
@@ -1,8 +1,8 @@
django-reverse-unique
=====================

.. image:: https://travis-ci.org/akaariai/django-reverse-unique.svg?branch=master
:target: https://travis-ci.org/akaariai/django-reverse-unique
.. image:: https://github.com/akaariai/django-reverse-unique/workflows/Test/badge.svg
:target: https://github.com/akaariai/django-reverse-unique/actions
:alt: Build Status

.. image:: https://coveralls.io/repos/akaariai/django-reverse-unique/badge.svg?branch=master
Expand Down
65 changes: 26 additions & 39 deletions reverse_unique/fields.py
@@ -1,40 +1,21 @@
import django

try:
from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor
except ImportError:
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor

from django.db.models.fields.related import ForeignObject
from django.db import models


if django.VERSION >= (1, 9):
def get_remote_field(field):
return field.remote_field

def get_remote_field_model(field):
return field.remote_field.model
else:
def get_remote_field(field):
return getattr(field, 'rel', None)

def get_remote_field_model(field):
return field.rel.to
from django.db.models.fields.related import ForeignObject
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor


class ReverseUniqueDescriptor(ForwardManyToOneDescriptor):
def __set__(self, instance, value):
if instance is None:
raise AttributeError("%s must be accessed via instance" % self.field.name)
instance.__dict__[self.field.get_cache_name()] = value
if value is not None and not get_remote_field(self.field).multiple:
if value is not None and not self.field.remote_field.multiple:
setattr(value, self.field.related.get_cache_name(), instance)

def __get__(self, instance, *args, **kwargs):
try:
return super(ReverseUniqueDescriptor, self).__get__(instance, *args, **kwargs)
except get_remote_field_model(self.field).DoesNotExist:
return super().__get__(instance, *args, **kwargs)
except self.field.remote_field.model.DoesNotExist:
instance.__dict__[self.field.get_cache_name()] = None
return None

Expand All @@ -50,20 +31,20 @@ def __init__(self, *args, **kwargs):
kwargs['null'] = True
kwargs['related_name'] = '+'
kwargs['on_delete'] = models.DO_NOTHING
super(ReverseUnique, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def resolve_related_fields(self):
if self.through is None:
possible_models = [self.model] + [m for m in self.model.__mro__ if hasattr(m, '_meta')]
possible_targets = [f for f in get_remote_field_model(self)._meta.concrete_fields
if get_remote_field(f) and get_remote_field_model(f) in possible_models]
possible_targets = [f for f in self.remote_field.model._meta.concrete_fields
if f.remote_field and f.remote_field.model in possible_models]
if len(possible_targets) != 1:
raise Exception("Found %s target fields instead of one, the fields found were %s."
% (len(possible_targets), [f.name for f in possible_targets]))
related_field = possible_targets[0]
else:
related_field = self.model._meta.get_field(self.through).field
if get_remote_field_model(related_field)._meta.concrete_model != self.model._meta.concrete_model:
if related_field.remote_field.model._meta.concrete_model != self.model._meta.concrete_model:
# We have found a foreign key pointing to parent model.
# This will only work if the fk is pointing to a value
# that can be found from the child model, too. This is
Expand All @@ -80,7 +61,7 @@ def resolve_related_fields(self):
to_fields = [f.name for f in related_field.foreign_related_fields]
self.to_fields = [f.name for f in related_field.local_related_fields]
self.from_fields = to_fields
return super(ReverseUnique, self).resolve_related_fields()
return super().resolve_related_fields()

def _find_parent_link(self, related_field):
"""
Expand All @@ -96,14 +77,14 @@ def _find_parent_link(self, related_field):
ancestor_links = []
curr_model = self.model
while True:
found_link = curr_model._meta.get_ancestor_link(get_remote_field_model(related_field))
found_link = curr_model._meta.get_ancestor_link(related_field.remote_field.model)
if not found_link:
# OK, we found to parent model. Lets check that the pointed to
# field contains the correct value.
last_link = ancestor_links[-1]
if last_link.foreign_related_fields != related_field.foreign_related_fields:
curr_opts = curr_model._meta
rel_opts = get_remote_field_model(self)._meta
rel_opts = self.remote_field.model._meta
opts = self.model._meta
raise ValueError(
"The field(s) %s of model %s.%s which %s.%s.%s is "
Expand All @@ -119,17 +100,17 @@ def _find_parent_link(self, related_field):
if ancestor_links:
assert found_link.local_related_fields == ancestor_links[-1].foreign_related_fields
ancestor_links.append(found_link)
curr_model = get_remote_field_model(found_link)
return [self.model._meta.get_ancestor_link(get_remote_field_model(related_field)).name]
curr_model = found_link.remote_field.model
return [self.model._meta.get_ancestor_link(related_field.remote_field.model).name]

def get_filters(self):
if callable(self.filters):
return self.filters()
else:
return self.filters

def get_extra_restriction(self, where_class, alias, related_alias):
remote_model = get_remote_field_model(self)
def _get_extra_restriction(self, alias, related_alias):
remote_model = self.remote_field.model
qs = remote_model.objects.filter(self.get_filters()).query
my_table = self.model._meta.db_table
rel_table = remote_model._meta.db_table
Expand All @@ -141,20 +122,26 @@ def get_extra_restriction(self, where_class, alias, related_alias):
where.relabel_aliases({my_table: related_alias, rel_table: alias})
return where

if django.VERSION[0] >= 4:
get_extra_restriction = _get_extra_restriction
else:
def get_extra_restriction(self, where_class, alias, related_alias):
return self._get_extra_restriction(alias, related_alias)

def get_extra_descriptor_filter(self, instance):
return self.get_filters()

def get_path_info(self, filtered_relation):
ret = super(ReverseUnique, self).get_path_info(filtered_relation)
def get_path_info(self, *args, **kwargs):
ret = super().get_path_info(*args, **kwargs)
assert len(ret) == 1
return [ret[0]._replace(direct=False)]

def contribute_to_class(self, cls, name):
super(ReverseUnique, self).contribute_to_class(cls, name)
super().contribute_to_class(cls, name)
setattr(cls, self.name, ReverseUniqueDescriptor(self))

def deconstruct(self):
name, path, args, kwargs = super(ReverseUnique, self).deconstruct()
name, path, args, kwargs = super().deconstruct()
kwargs['filters'] = self.filters
if self.through is not None:
kwargs['through'] = self.through
Expand Down
2 changes: 2 additions & 0 deletions reverse_unique_tests/settings.py
Expand Up @@ -16,3 +16,5 @@
'reverse_unique',
'reverse_unique_tests',
]

DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
2 changes: 0 additions & 2 deletions reverse_unique_tests/tests.py
@@ -1,5 +1,3 @@
from __future__ import unicode_literals

import datetime

from django import forms
Expand Down
28 changes: 19 additions & 9 deletions tox.ini
Expand Up @@ -2,26 +2,36 @@
args_are_paths = false
envlist =
flake8,
py39-{2.0,2.1,2.2,3.0,3.1,3.2},
py310-{3.0,3.1,3.2}
py36-3.2,
py37-3.2,
py38-{3.2,4.0,4.1,main},
py39-{3.2,4.0,4.1,main},
py310-{3.2,4.0,4.1,main},

[gh-actions]
python =
3.6: py36, flake8, isort
3.7: py37
3.8: py38
3.9: py39
3.10: py310

[testenv]
usedevelop = true
commands =
python -R -Wonce {envbindir}/coverage run {envbindir}/django-admin.py test -v2 --settings=reverse_unique_tests.settings {posargs}
{envpython} -R -Wonce {envbindir}/coverage run -a -m django test -v2 --settings=reverse_unique_tests.settings {posargs}
coverage report
deps =
coverage
2.0: Django>=2.0,<2.1
2.1: Django>=2.1,<2.2
2.2: Django>=2.2,<3.0
3.0: Django>=3.0,<3.1
3.1: Django>=3.1,<3.2
3.2: Django>=3.2,<4.0
4.0: Django>=4.0,<4.1
4.1: Django>=4.1,<4.2
main: https://github.com/django/django/archive/main.tar.gz
passenv =
GITHUB_*

[testenv:flake8]
basepython = python3.9
basepython = python3.6
commands =
flake8 reverse_unique
deps =
Expand Down

0 comments on commit 49515c2

Please sign in to comment.