Skip to content

Commit

Permalink
(hotfix): resolve incorrect and inconsistent module naming
Browse files Browse the repository at this point in the history
- see #2
- in Python-land, packages are _not_ imported by their package name,
  but their _module_ name
  - see also https://stackoverflow.com/a/54599368/3431180
  - and the module name of this package was `serializable`, _not_
    `django-serializable-model`
- also in Python-land, one cannot use the standard `import` syntax
  for hyphenated modules, it will actually cause a SyntaxError
  - but for some reason it's not illegal to do so, and is allowed both
    in `setuptools` naming and PyPI naming (it is discouraged by PEP8)

- rename serializable.py to django_serializable_model.py
  - leave serializable.py and just make it a wrapper / alias of
    django_serialiazable_model so as not to cause a breaking change

(pkg): publish `django_serializable_model` module as well
- don't remove `serializable` though so as to not cause a breaking
  change; should remove it in v1.0.0

(docs): use `django_serializable_model` in Usage docs
  • Loading branch information
agilgur5 committed Aug 8, 2019
1 parent e82be57 commit b7e9e70
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 134 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Simplest use case, just implements the `.serialize()` function on a model:

```python
from django.db import models
from django-serializable-model import SerializableModel
from django_serializable_model import SerializableModel


class User(SerializableModel):
Expand All @@ -71,7 +71,7 @@ With an override of the default `.serialize()` function to only include whitelis

```python
from django.db import models
from django-serializable-model import SerializableModel
from django_serializable_model import SerializableModel


class User(SerializableModel):
Expand Down Expand Up @@ -104,7 +104,7 @@ With a simple, one-to-one relation:

```python
from django.db import models
from django-serializable-model import SerializableModel
from django_serializable_model import SerializableModel


class User(SerializableModel):
Expand Down Expand Up @@ -143,7 +143,7 @@ With a foreign key relation:

```python
from django.db import models
from django-serializable-model import SerializableModel
from django_serializable_model import SerializableModel


class User(SerializableModel):
Expand Down
131 changes: 131 additions & 0 deletions django_serializable_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import django
from django.db import models
from django.core.exceptions import ObjectDoesNotExist


class _SerializableQuerySet(models.query.QuerySet):
"""Implements the serialize method on a QuerySet"""
def serialize(self, *args):
serialized = []
for elem in self:
serialized.append(elem.serialize(*args))
return serialized


class SerializableManager(models.Manager):
"""Implements table-level serialization via SerializableQuerySet"""
# backward compatibility for Django < 1.10
if django.VERSION < (1, 10):
# when queried from a related Model, use this Manager
use_for_related_fields = True

def get_queryset(self):
return _SerializableQuerySet(self.model)

# backward compatibility for Django < 1.6
if django.VERSION < (1, 6):
get_query_set = get_queryset

def get_queryset_compat(self):
get_queryset = (self.get_query_set
if hasattr(self, 'get_query_set')
else self.get_queryset)
return get_queryset()

# implement serialize on the Manager itself (on .objects, before .all())
def serialize(self, *args):
return self.get_queryset_compat().serialize(*args)


class SerializableModel(models.Model):
"""
Abstract Model that implements recursive serializability of models to
dictionaries, both at the row and table level, with some overriding allowed
"""
objects = SerializableManager()

class Meta:
abstract = True
# when queried from a related Model, use this Manager
base_manager_name = 'SerializableManager'

def serialize(self, *args, **kwargs):
"""
Serializes the Model object with model_to_dict_custom and kwargs, and
proceeds to recursively serialize related objects as requested in args
"""
serialized = model_to_dict_custom(self, **kwargs)
args = list(args) # convert tuple to list

# iterate and recurse through all arguments
index = 0
length = len(args)
while index < length:
# split the current element
field_with_joins = args[index]
field, join = _split_joins(field_with_joins)
all_joins = [join] if join else [] # empty string to empty array

# delete it from the list
del args[index]
length -= 1

# get all joins for this field from the arguments
arg_joins = [_split_joins(arg, only_join=True)
for arg in args if arg.startswith(field)]
all_joins += arg_joins # combine all joins on this field

# recurse if related object actually exists
try:
serialized[field] = getattr(self, field).serialize(*all_joins)
except (AttributeError, ObjectDoesNotExist):
pass

# shrink length and remove all args that were recursed over
length -= len(arg_joins)
args = [arg for arg in args if not arg.startswith(field)]

return serialized


def model_to_dict_custom(instance, fields=None, exclude=None, editable=True):
"""
Custom model_to_dict function that differs by including all uneditable
fields and excluding all M2M fields by default
Also sets all ForeignKey fields to name + _id, similar to .values()
"""
# avoid circular import
from django.db.models.fields.related import ForeignKey
opts = instance._meta
data = {}
for f in opts.fields:
# skip uneditable fields if editable kwarg is False
if not editable and not f.ediable:
continue
# whitelisted fields only if fields kwarg is passed
if fields and f.name not in fields:
continue
# blacklist fields from exclude kwarg
if exclude and f.name in exclude:
continue
else:
if isinstance(f, ForeignKey):
data[f.name + '_id'] = f.value_from_object(instance)
else:
data[f.name] = f.value_from_object(instance)
return data


def _split_joins(join_string, only_join=False):
"""
Split a string into the field and it's joins, separated by __ as per
Django convention
"""
split = join_string.split('__')
field = split.pop(0) # the first field
join = '__'.join(split) # the rest of the fields

# return single join or tuple based on kwarg
if only_join:
return join
return field, join
137 changes: 8 additions & 129 deletions serializable.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,10 @@
import django
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
"""
This is just a pure wrapper / alias module around django_serializable_model to
still be able to import it with the original, unintended name of serializable.
See https://github.com/agilgur5/django-serializable-model/issues/2
In the first major/breaking release, v1.0.0, this file should be deleted and
the module removed from `setup.py`.
"""

class _SerializableQuerySet(models.query.QuerySet):
"""Implements the serialize method on a QuerySet"""
def serialize(self, *args):
serialized = []
for elem in self:
serialized.append(elem.serialize(*args))
return serialized


class SerializableManager(models.Manager):
"""Implements table-level serialization via SerializableQuerySet"""
# backward compatibility for Django < 1.10
if django.VERSION < (1, 10):
# when queried from a related Model, use this Manager
use_for_related_fields = True

def get_queryset(self):
return _SerializableQuerySet(self.model)

# backward compatibility for Django < 1.6
if django.VERSION < (1, 6):
get_query_set = get_queryset

def get_queryset_compat(self):
get_queryset = (self.get_query_set
if hasattr(self, 'get_query_set')
else self.get_queryset)
return get_queryset()

# implement serialize on the Manager itself (on .objects, before .all())
def serialize(self, *args):
return self.get_queryset_compat().serialize(*args)


class SerializableModel(models.Model):
"""
Abstract Model that implements recursive serializability of models to
dictionaries, both at the row and table level, with some overriding allowed
"""
objects = SerializableManager()

class Meta:
abstract = True
# when queried from a related Model, use this Manager
base_manager_name = 'SerializableManager'

def serialize(self, *args, **kwargs):
"""
Serializes the Model object with model_to_dict_custom and kwargs, and
proceeds to recursively serialize related objects as requested in args
"""
serialized = model_to_dict_custom(self, **kwargs)
args = list(args) # convert tuple to list

# iterate and recurse through all arguments
index = 0
length = len(args)
while index < length:
# split the current element
field_with_joins = args[index]
field, join = _split_joins(field_with_joins)
all_joins = [join] if join else [] # empty string to empty array

# delete it from the list
del args[index]
length -= 1

# get all joins for this field from the arguments
arg_joins = [_split_joins(arg, only_join=True)
for arg in args if arg.startswith(field)]
all_joins += arg_joins # combine all joins on this field

# recurse if related object actually exists
try:
serialized[field] = getattr(self, field).serialize(*all_joins)
except (AttributeError, ObjectDoesNotExist):
pass

# shrink length and remove all args that were recursed over
length -= len(arg_joins)
args = [arg for arg in args if not arg.startswith(field)]

return serialized


def model_to_dict_custom(instance, fields=None, exclude=None, editable=True):
"""
Custom model_to_dict function that differs by including all uneditable
fields and excluding all M2M fields by default
Also sets all ForeignKey fields to name + _id, similar to .values()
"""
# avoid circular import
from django.db.models.fields.related import ForeignKey
opts = instance._meta
data = {}
for f in opts.fields:
# skip uneditable fields if editable kwarg is False
if not editable and not f.ediable:
continue
# whitelisted fields only if fields kwarg is passed
if fields and f.name not in fields:
continue
# blacklist fields from exclude kwarg
if exclude and f.name in exclude:
continue
else:
if isinstance(f, ForeignKey):
data[f.name + '_id'] = f.value_from_object(instance)
else:
data[f.name] = f.value_from_object(instance)
return data


def _split_joins(join_string, only_join=False):
"""
Split a string into the field and it's joins, separated by __ as per
Django convention
"""
split = join_string.split('__')
field = split.pop(0) # the first field
join = '__'.join(split) # the rest of the fields

# return single join or tuple based on kwarg
if only_join:
return join
return field, join
from django_serializable_model import * # noqa F403, F401
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@
],
keywords=('django serializer serializers serializer-django serialize ' +
'json dict queryset model modelmanager full wadofstuff'),
py_modules=['serializable'],
py_modules=[
'django_serializable_model',
# this is the original, unintended name, and should be removed in the
# first breaking/major release, v1.0.0. See `serializable.py` comment.
'serializable'
],
python_requires='>=2.7, <4',
project_urls={ # Optional
'Source': 'https://github.com/agilgur5/django-serializable-model/',
Expand Down

0 comments on commit b7e9e70

Please sign in to comment.