DRY natural keys

The current code is below. Usage:

@with_natural_key(["name"])
class Widget(models.Model):
    name = models.CharField(max_length=200)
    another_field = models.IntegerField()

This adds a natural_key method on Widget and creates a Manager at objects with a get_by_natural_key method to match.

"""
Decorators for Django models
"""
from inspect import signature, Parameter

from django.db import models


def with_natural_key(fields, manager_name=None):
    """
    Decorator to add DRY natural key support to a Django model.

    Adds a Manager class with get_by_natural_key and a corresponding natural_key
    method on the model.

    TODO: add support for dependencies
    """
    assert 'self' not in fields

    def natural_key_wrapper(klass):
        def _natural_key(self):
            return tuple([self.__dict__[x] for x in fields])

        klass.natural_key = _natural_key
        klass.natural_key.__name__ = "natural_key"
        klass.natural_key.__doc__ = "Return a natural key for the model: ({})".format(
            ", ".join(fields))

        class NaturalKeyManager(models.Manager):
            """
            Implements get_by_natural_key for {}
            """
            def get_by_natural_key(self, *args):
                """
                Find an object by its natural key
                """
                if len(args) != len(fields):
                    raise RuntimeError("expected {} arguments ({}), got {}".format(
                        len(fields), ", ".join(fields), len(args)
                    ))

                return self.get(**dict(zip(fields, args)))

        NaturalKeyManager.__name__ = klass.__name__ + "NaturalKeyManager"
        NaturalKeyManager.__doc__ = NaturalKeyManager.__doc__.format(klass.__name__)

        # fix up the help() to show the expected positional parameters instead of "*args"
        sig = signature(NaturalKeyManager.get_by_natural_key)
        sig = sig.replace(parameters=[Parameter(f, Parameter.POSITIONAL_ONLY) for f in ['self'] + fields])
        setattr(NaturalKeyManager.get_by_natural_key, "__signature__", sig)

        _m = NaturalKeyManager()
        _m.contribute_to_class(klass, manager_name or "objects")

        return klass

    return natural_key_wrapper
/r/django Thread