Exemplo n.º 1
0
    def __init__(self, to=None, to_base=None, **kwargs):
        """
        Initialise the tag options and store

        Takes all tag options as keyword arguments, as long as there is no
        ``to`` tag model specified.

        Undocumented keyword argument ``_set_tag_meta`` is used internally to
        update tag model options with those specified in keyword arguments when
        a ``to`` tag model is specified. However, this is intended for internal
        use only (when migrating), so if you must use it, use it with care.
        """
        # Save tag model data
        self.tag_model = to
        self.tag_model_base = to_base

        # Extract options from kwargs
        options = {}
        for key, default in constants.OPTION_DEFAULTS.items():
            # Look in kwargs, then in tag_meta
            if key in kwargs:
                options[key] = kwargs.pop(key)

        # Detect if _set_tag_meta is set
        set_tag_meta = kwargs.pop("_set_tag_meta", False)

        # Detect whether we need to automatically generate a tag model
        if self.tag_model:
            self.auto_tag_model = False

            # Tag model might be a string - set options later
            self.tag_options = None
            self._deferred_options = [set_tag_meta, options]

        else:
            self.auto_tag_model = True
            self.tag_options = TagOptions(**options)
            self._deferred_options = None

        # If the tag model was not specified, we need to specify one.
        # However, we can't reliably auto-generate a unique and repeatable
        # model name for tags here in __init__ - we can only do that in
        # contribute_to_class once we know the name of the field in the model.
        # We'll therefore use the string '-'; Django will not do anything about
        # resolving it until contribute_to_class, at which point we'll replace
        # it with a reference to the real tag model.
        kwargs["to"] = self.tag_model if self.tag_model else "-"

        # Call super __init__
        super(BaseTagField, self).__init__(**kwargs)

        # Make a note that this has not been contributed to a class yet
        self.contributed = False

        # This attribute will let us tell South to supress undesired M2M fields
        self.south_supression = True
Exemplo n.º 2
0
    def __new__(cls, name, bases, attrs):
        # Set up as normal
        new_cls = super(TagModelBase, cls).__new__(cls, name, bases, attrs)

        # TagMeta takes priority for the model
        new_tag_options = None
        if 'TagMeta' in attrs:
            tag_meta = {}
            tag_meta = dict(
                (key, val) for key, val in attrs['TagMeta'].__dict__.items()
                if key in constants.OPTION_DEFAULTS
            )
            if 'tree' in tag_meta:
                raise ValueError('Cannot set tree option in TagMeta')

            new_tag_options = TagOptions(**tag_meta)

        # Failing that, look for a direct tag_options setting
        # It will probably have been passed by BaseTagField.contribute_to_class
        elif 'tag_options' in attrs:
            new_tag_options = attrs['tag_options']

        # Otherwise start a new one
        else:
            new_tag_options = TagOptions()

        # See if there's anything to inherit
        # This also means that tag_options will be available on abstract models
        if hasattr(new_cls, 'tag_options'):
            # Inherit by setting missing values in place
            new_tag_options.set_missing(new_cls.tag_options)

        # Assign
        new_cls.tag_options = new_tag_options

        # Check for self-referential tag fields on this model
        fields = new_cls._meta.fields + new_cls._meta.many_to_many

        for field in fields:
            # Can't test for subclass of field here - would be circular import
            if hasattr(field, 'tag_model') and field.tag_model == new_cls:
                # This method is being called after the tag field's
                # contribute_to_class and _process_deferred_options. This means
                # that the field is using tag_options from the inherited model.
                # Change it to use this one
                field.tag_options = new_tag_options

        return new_cls
Exemplo n.º 3
0
    def __init__(self, to=None, to_base=None, **kwargs):
        """
        Initialise the tag options and store

        Takes all tag options as keyword arguments, as long as there is no
        ``to`` tag model specified.

        Undocumented keyword argument ``_set_tag_meta`` is used internally to
        update tag model options with those specified in keyword arguments when
        a ``to`` tag model is specified. However, this is intended for internal
        use only (when migrating), so if you must use it, use it with care.
        """
        # Save tag model data
        self.tag_model = to
        self.tag_model_base = to_base

        # Extract options from kwargs
        options = {}
        for key, default in constants.OPTION_DEFAULTS.items():
            # Look in kwargs, then in tag_meta
            if key in kwargs:
                options[key] = kwargs.pop(key)

        # Detect if _set_tag_meta is set
        set_tag_meta = kwargs.pop('_set_tag_meta', False)

        # Detect whether we need to automatically generate a tag model
        if self.tag_model:
            self.auto_tag_model = False

            # Tag model might be a string - set options later
            self.tag_options = None
            self._deferred_options = [set_tag_meta, options]

        else:
            self.auto_tag_model = True
            self.tag_options = TagOptions(**options)
            self._deferred_options = None

        # If the tag model was not specified, we need to specify one.
        # However, we can't reliably auto-generate a unique and repeatable
        # model name for tags here in __init__ - we can only do that in
        # contribute_to_class once we know the name of the field in the model.
        # We'll therefore use the string '-'; Django will not do anything about
        # resolving it until contribute_to_class, at which point we'll replace
        # it with a reference to the real tag model.
        kwargs['to'] = self.tag_model if self.tag_model else '-'

        # Call super __init__
        super(BaseTagField, self).__init__(**kwargs)

        # Make a note that this has not been contributed to a class yet
        self.contributed = False

        # This attribute will let us tell South to supress undesired M2M fields
        self.south_supression = True
    def formfield(self, form_class, **kwargs):
        """
        Common actions for TagField and SingleTagField to set up a formfield
        """
        required = not self.blank
        if hasattr(self, 'required'):
            required = self.required

        # Update tag options, if necessary
        tag_options = self.tag_options
        if 'tag_options' in kwargs:
            new_options = kwargs.pop('tag_options')
            if not isinstance(new_options, TagOptions):
                new_options = TagOptions(**new_options)
            tag_options += new_options

        # Start off with defaults
        options = {
            # Arguments the TagField base (CharField) would expect
            "label": capfirst(self.verbose_name),
            "help_text": self.help_text,
            "required": required,

            # Also pass tag options
            "tag_options": tag_options
        }

        # Update with kwargs
        options.update(kwargs)

        # Add in list of tags for autocomplete, if appropriate
        if 'autocomplete_tags' in kwargs:
            options['autocomplete_tags'] = kwargs['autocomplete_tags']
        elif not tag_options.autocomplete_view:
            tags = self.tag_model.objects.all()
            if tag_options.autocomplete_initial:
                tags = tags.initial()
            options['autocomplete_tags'] = tags

        # Create the field instance
        return form_class(**options)
Exemplo n.º 5
0
    def __new__(cls, name, bases, attrs):
        # Set up as normal
        new_cls = super(TagModelBase, cls).__new__(cls, name, bases, attrs)

        # TagMeta takes priority for the model
        new_tag_options = None
        if 'TagMeta' in attrs:
            tag_meta = {}
            tag_meta = dict(
                (key, val) for key, val in attrs['TagMeta'].__dict__.items()
                if key in constants.OPTION_DEFAULTS
            )
            if 'tree' in tag_meta:
                raise ValueError('Cannot set tree option in TagMeta')

            new_tag_options = TagOptions(**tag_meta)

        # Failing that, look for a direct tag_options setting
        # It will probably have been passed by BaseTagField.contribute_to_class
        elif 'tag_options' in attrs:
            new_tag_options = attrs['tag_options']

        # Otherwise start a new one
        else:
            new_tag_options = TagOptions()

        # See if there's anything to inherit
        # This also means that tag_options will be available on abstract models
        if hasattr(new_cls, 'tag_options'):
            # Inherit by setting missing values in place
            new_tag_options.set_missing(new_cls.tag_options)

        # Assign
        new_cls.tag_options = new_tag_options

        # Check for self-referential tag fields on this model
        fields = new_cls._meta.fields + new_cls._meta.many_to_many

        for field in fields:
            # Can't test for subclass of field here - would be circular import
            if hasattr(field, 'tag_model') and field.tag_model == new_cls:
                # This method is being called after the tag field's
                # contribute_to_class and _process_deferred_options. This means
                # that the field is using tag_options from the inherited model.
                # Change it to use this one
                field.tag_options = new_tag_options

        return new_cls
class BaseTagField(object):
    """
    Mixin for TagField and SingleTagField
    """
    # List of fields which are forbidden from __init__
    forbidden_fields = ()

    def __init__(self, to=None, **kwargs):
        """
        Initialise the tag options and store

        Takes all tag options as keyword arguments, as long as there is no
        ``to`` tag model specified.

        Undocumented keyword argument ``_set_tag_meta`` is used internally to
        update tag model options with those specified in keyword arguments when
        a ``to`` tag model is specified. However, this is intended for internal
        use only (when migrating), so if you must use it, use it with care.
        """
        # Save tag model
        self.tag_model = to

        # Extract options from kwargs
        options = {}
        for key, default in constants.OPTION_DEFAULTS.items():
            # Look in kwargs, then in tag_meta
            if key in kwargs:
                options[key] = kwargs.pop(key)

        # Detect if _set_tag_meta is set
        set_tag_meta = kwargs.pop('_set_tag_meta', False)

        # Detect whether we need to automatically generate a tag model
        if self.tag_model:
            self.auto_tag_model = False

            # Tag model might be a string - set options later
            self.tag_options = None
            self._deferred_options = [set_tag_meta, options]

        else:
            self.auto_tag_model = True
            self.tag_options = TagOptions(**options)
            self._deferred_options = None

        # If the tag model was not specified, we need to specify one.
        # However, we can't reliably auto-generate a unique and repeatable
        # model name for tags here in __init__ - we can only do that in
        # contribute_to_class once we know the name of the field in the model.
        # We'll therefore use the string '-'; Django will not do anything about
        # resolving it until contribute_to_class, at which point we'll replace
        # it with a reference to the real tag model.
        kwargs['to'] = self.tag_model if self.tag_model else '-'

        # Call super __init__
        super(BaseTagField, self).__init__(**kwargs)

        # Make a note that this has not been contributed to a class yet
        self.contributed = False

        # This attribute will let us tell South to supress undesired M2M fields
        self.south_supression = True

    if django.VERSION < (1, 9):
        remote_field = property(lambda self: self.rel)

    def do_related_class(self, other, cls):
        """
        Process tag model now it has been resolved if it was a string
        """
        # Set up relation as normal
        super(BaseTagField, self).do_related_class(other, cls)

        # Make sure tag model is the related model, in case it was a string
        if django.VERSION < (1, 8):
            # Django 1.7 or earlier
            self.tag_model = self.related.parent_model
        else:
            # Django 1.8 or later
            self.tag_model = self.remote_field.model

        # Check class type of tag model
        if not issubclass(self.tag_model, BaseTagModel):
            raise ValueError('Tag model must be a subclass of TagModel')

        # Process the deferred options
        self._process_deferred_options(is_to_self=self.tag_model == cls)

    def _process_deferred_options(self, is_to_self=False):
        """
        Process tag options once we have the related model

        If the field is explicitly referring to itself, is_to_self will be True
        """
        # See if tag options were deferred
        if self._deferred_options is None:
            return

        # Get deferred options
        set_tag_meta, options = self._deferred_options
        self._deferred_options = None

        # Set options
        if options:
            if set_tag_meta:
                # Tag option arguments must be used to update the tag model
                # (and any other tag fields which use it) - used during
                # migration when the tag model doesn't know its own options
                self.tag_model.tag_options.update(options)
            else:
                raise ValueError(
                    'Cannot set tag options on explicit tag model %r' %
                    (self.tag_model, ))

        # Link to model options by reference in case they get updated later
        self.tag_options = self.tag_model.tag_options

    def contribute_to_class(self, cls, name):
        """
        Create the tag model if necessary, then initialise and contribute the
        field to the class
        """
        #
        # Get or create the tag model
        #

        # Create a new tag model if we need to
        if self.auto_tag_model:
            # Make sure a TagField is only contributed once if the model is
            # not explicitly set. This isn't normal for model fields, but in
            # this case the name of the model (and therefore db) would depend
            # on the load order, which could change. Rather than risk problems
            # later, ban it outright to save developers from themselves.
            # If it causes problems for anyone, they can explicitly set a tag
            # model and avoid this being a problem.
            if self.contributed:
                raise AttributeError(
                    "The tag field %r is already attached to a model" % self)

            # Generate a list of attributes for the new tag model
            model_attrs = {
                # Module should be the same as the main model
                '__module__': cls.__module__,

                # Give it access to the options
                'tag_options': self.tag_options,
            }

            # Build new tag model
            # Name is _Tagulous_MODELNAME_FIELDNAME
            model_name = "%s_%s_%s" % (
                constants.MODEL_PREFIX,
                cls._meta.object_name,
                name,
            )
            model_cls = TagModel
            if self.tag_options.tree:
                model_cls = TagTreeModel
            self.tag_model = type(str(model_name), (model_cls, ), model_attrs)

            # Give it a verbose name, for admin filters
            verbose_name_singular = (self.tag_options.verbose_name_singular
                                     or self.verbose_name or name)
            verbose_name_plural = self.tag_options.verbose_name_plural
            if not verbose_name_plural:
                verbose_name_plural = (verbose_name_singular
                                       or self.verbose_name or name)
                if not verbose_name_plural.endswith('s'):
                    verbose_name_plural += 's'

            # Get object verbose name
            object_name = cls._meta.verbose_name
            self.tag_model._meta.verbose_name = '%s %s' % (
                object_name,
                verbose_name_singular,
            )
            self.tag_model._meta.verbose_name_plural = '%s %s' % (
                object_name,
                verbose_name_plural,
            )

            # Make no attempt to enforce max length of verbose_name - no good
            # automatic solution, and the limit may change, see
            #   https://code.djangoproject.com/ticket/17763
            # If it's a problem, contrib.auth will raise a ValidationError

        #
        # Build the tag field
        #

        # Update the rel on the field
        if django.VERSION < (1, 9):
            # Django 1.8 or earlier
            self.remote_field.to = self.tag_model
        else:
            # Django 1.9 and later
            self.remote_field.model = self.tag_model

        # Contribute to class
        super(BaseTagField, self).contribute_to_class(cls, name)
        self.contributed = True

    def formfield(self, form_class, **kwargs):
        """
        Common actions for TagField and SingleTagField to set up a formfield
        """
        required = not self.blank
        if hasattr(self, 'required'):
            required = self.required

        # Update tag options, if necessary
        tag_options = self.tag_options
        if 'tag_options' in kwargs:
            new_options = kwargs.pop('tag_options')
            if not isinstance(new_options, TagOptions):
                new_options = TagOptions(**new_options)
            tag_options += new_options

        # Start off with defaults
        options = {
            # Arguments the TagField base (CharField) would expect
            "label": capfirst(self.verbose_name),
            "help_text": self.help_text,
            "required": required,

            # Also pass tag options
            "tag_options": tag_options
        }

        # Update with kwargs
        options.update(kwargs)

        # Add in list of tags for autocomplete, if appropriate
        if 'autocomplete_tags' in kwargs:
            options['autocomplete_tags'] = kwargs['autocomplete_tags']
        elif not tag_options.autocomplete_view:
            tags = self.tag_model.objects.all()
            if tag_options.autocomplete_initial:
                tags = tags.initial()
            options['autocomplete_tags'] = tags

        # Create the field instance
        return form_class(**options)

    def get_manager_name(self):
        """
        Get the field name for the Manager
        """
        return "_%s_tagulous" % self.name

    def deconstruct(self):
        """
        Deconstruct field options to a dict for __init__
        """
        name, path, args, kwargs = super(BaseTagField, self).deconstruct()

        # Find tag model options
        if isinstance(self.tag_options, TagOptions):
            # When deconstruct is called on a real field on a real model,
            # we will have a concrete tag model, so use its tag options
            items = self.tag_options.items(with_defaults=False)

            # Freeze initial as a string, not array
            if 'initial' in items:
                items['initial'] = self.tag_options.initial_string

        elif self._deferred_options is not None:
            # When deconstruct is called on a ModelState field, the options
            # will have been deferred
            set_tag_meta, options = self._deferred_options
            items = options

        else:  # pragma: no cover
            # It should never get here - raise an exception so we can debug
            raise ValueError('Unexpected state')

        # Add tag model options to kwargs
        kwargs.update(items)

        # Can't freeze lambdas. It should work if it's a function at the top
        # level of a module, but there's no easy way to differentiate. For
        # safety and consistency, strip callable arguments and mention in
        # documentation.
        if 'get_absolute_url' in kwargs:
            del kwargs['get_absolute_url']

        # Remove forbidden fields
        # This shouldn't be needed, but strip them just in case
        for forbidden in self.forbidden_fields:  # pragma: no cover
            if forbidden in kwargs:
                del kwargs[forbidden]

        # Always store _set_tag_meta=True, so migrating tag fields can set tag
        # models' TagMeta
        kwargs['_set_tag_meta'] = True

        return name, path, args, kwargs
Exemplo n.º 7
0
class BaseTagField(object):
    """
    Mixin for TagField and SingleTagField
    """
    # List of fields which are forbidden from __init__
    forbidden_fields = ()
    
    def __init__(self, to=None, **kwargs):
        """
        Initialise the tag options and store 
        
        Takes all tag options as keyword arguments, as long as there is no
        ``to`` tag model specified.
        
        Undocumented keyword argument ``_set_tag_meta`` is used internally to
        update tag model options with those specified in keyword arguments when
        a ``to`` tag model is specified. However, this is intended for internal
        use only (when migrating), so if you must use it, use it with care.
        """
        # Save tag model
        self.tag_model = to
        
        # Extract options from kwargs
        options = {}
        for key, default in constants.OPTION_DEFAULTS.items():
            # Look in kwargs, then in tag_meta
            if key in kwargs:
                options[key] = kwargs.pop(key)
        
        # Detect if _set_tag_meta is set
        set_tag_meta = kwargs.pop('_set_tag_meta', False)
        
        # Detect whether we need to automatically generate a tag model
        if self.tag_model:
            self.auto_tag_model = False
            
            # Tag model might be a string - set options later
            self.tag_options = None
            self._deferred_options = [set_tag_meta, options]
            
        else:
            self.auto_tag_model = True
            self.tag_options = TagOptions(**options)
            self._deferred_options = None
        
        # If the tag model was not specified, we need to specify one.
        # However, we can't reliably auto-generate a unique and repeatable
        # model name for tags here in __init__ - we can only do that in
        # contribute_to_class once we know the name of the field in the model.
        # We'll therefore use the string '-'; Django will not do anything about
        # resolving it until contribute_to_class, at which point we'll replace
        # it with a reference to the real tag model.
        kwargs['to'] = self.tag_model if self.tag_model else '-'
        
        # Call super __init__
        super(BaseTagField, self).__init__(**kwargs)
        
        # Make a note that this has not been contributed to a class yet
        self.contributed = False
        
        # This attribute will let us tell South to supress undesired M2M fields
        self.south_supression = True
    
    if django.VERSION < (1, 9):
        remote_field = property(lambda self: self.rel)
    
    def do_related_class(self, other, cls):
        """
        Process tag model now it has been resolved if it was a string
        """
        # Set up relation as normal 
        super(BaseTagField, self).do_related_class(other, cls)
        
        # Make sure tag model is the related model, in case it was a string
        if django.VERSION < (1, 8):
            # Django 1.7 or earlier
            self.tag_model = self.related.parent_model
        else:
            # Django 1.8 or later
            self.tag_model = self.remote_field.model
        
        # Check class type of tag model
        if not issubclass(self.tag_model, BaseTagModel):
            raise ValueError('Tag model must be a subclass of TagModel')
        
        # Process the deferred options
        self._process_deferred_options(is_to_self=self.tag_model == cls)
        
    def _process_deferred_options(self, is_to_self=False):
        """
        Process tag options once we have the related model
        
        If the field is explicitly referring to itself, is_to_self will be True
        """
        # See if tag options were deferred
        if self._deferred_options is None:
            return
        
        # Get deferred options
        set_tag_meta, options = self._deferred_options
        self._deferred_options = None
        
        # Set options
        if options:
            if set_tag_meta:
                # Tag option arguments must be used to update the tag model
                # (and any other tag fields which use it) - used during
                # migration when the tag model doesn't know its own options
                self.tag_model.tag_options.update(options)
            else:
                raise ValueError(
                    'Cannot set tag options on explicit tag model %r' % (
                        self.tag_model,
                    )
                )
        
        # Link to model options by reference in case they get updated later
        self.tag_options = self.tag_model.tag_options
        
    def contribute_to_class(self, cls, name):
        """
        Create the tag model if necessary, then initialise and contribute the
        field to the class
        """
        #
        # Get or create the tag model
        #
        
        # Create a new tag model if we need to
        if self.auto_tag_model:
            # Make sure a TagField is only contributed once if the model is
            # not explicitly set. This isn't normal for model fields, but in
            # this case the name of the model (and therefore db) would depend
            # on the load order, which could change. Rather than risk problems
            # later, ban it outright to save developers from themselves.
            # If it causes problems for anyone, they can explicitly set a tag
            # model and avoid this being a problem.
            if self.contributed:
                raise AttributeError(
                    "The tag field %r is already attached to a model" % self
                )
            
            # Generate a list of attributes for the new tag model
            model_attrs = {
                # Module should be the same as the main model
                '__module__': cls.__module__,
                
                # Give it access to the options
                'tag_options': self.tag_options,
            }
            
            # Build new tag model
            # Name is _Tagulous_MODELNAME_FIELDNAME
            model_name = "%s_%s_%s" % (
                constants.MODEL_PREFIX, cls._meta.object_name, name,
            )
            model_cls = TagModel
            if self.tag_options.tree:
                model_cls = TagTreeModel
            self.tag_model = type(str(model_name), (model_cls,), model_attrs)
            
            # Give it a verbose name, for admin filters
            verbose_name_singular = (
                self.tag_options.verbose_name_singular or self.verbose_name or name
            )
            verbose_name_plural = self.tag_options.verbose_name_plural
            if not verbose_name_plural:
                verbose_name_plural = (
                    verbose_name_singular or self.verbose_name or name
                )
                if not verbose_name_plural.endswith('s'):
                    verbose_name_plural += 's'
            
            # Get object verbose name
            object_name = cls._meta.verbose_name
            self.tag_model._meta.verbose_name = '%s %s' % (
                object_name, verbose_name_singular,
            )
            self.tag_model._meta.verbose_name_plural = '%s %s' % (
                object_name, verbose_name_plural,
            )
            
            # Make no attempt to enforce max length of verbose_name - no good
            # automatic solution, and the limit may change, see
            #   https://code.djangoproject.com/ticket/17763
            # If it's a problem, contrib.auth will raise a ValidationError
            
        
        #
        # Build the tag field
        #
        
        # Update the rel on the field
        if django.VERSION < (1, 9):
            # Django 1.8 or earlier
            self.remote_field.to = self.tag_model
        else:
            # Django 1.9 and later
            self.remote_field.model = self.tag_model
        
        # Contribute to class
        super(BaseTagField, self).contribute_to_class(cls, name)
        self.contributed = True
    
    def formfield(self, form_class, **kwargs):
        """
        Common actions for TagField and SingleTagField to set up a formfield
        """
        required = not self.blank
        if hasattr(self, 'required'):
            required = self.required
        
        # Update tag options, if necessary
        tag_options = self.tag_options
        if 'tag_options' in kwargs:
            new_options = kwargs.pop('tag_options')
            if not isinstance(new_options, TagOptions):
                new_options = TagOptions(**new_options)
            tag_options += new_options
        
        # Start off with defaults
        options = {
            # Arguments the TagField base (CharField) would expect
            "label": capfirst(self.verbose_name),
            "help_text": self.help_text,
            "required": required,
            
            # Also pass tag options
            "tag_options": tag_options
        }
        
        # Update with kwargs
        options.update(kwargs)
        
        # Add in list of tags for autocomplete, if appropriate
        if 'autocomplete_tags' in kwargs:
            options['autocomplete_tags'] = kwargs['autocomplete_tags']
        elif not tag_options.autocomplete_view:
            tags = self.tag_model.objects.all()
            if tag_options.autocomplete_initial:
                tags = tags.initial()
            options['autocomplete_tags'] = tags
        
        # Create the field instance
        return form_class(**options)

    def get_manager_name(self):
        """
        Get the field name for the Manager
        """
        return "_%s_tagulous" % self.name
    
    def deconstruct(self):
        """
        Deconstruct field options to a dict for __init__
        """
        name, path, args, kwargs = super(BaseTagField, self).deconstruct()
        
        # Find tag model options
        if isinstance(self.tag_options, TagOptions):
            # When deconstruct is called on a real field on a real model,
            # we will have a concrete tag model, so use its tag options
            items = self.tag_options.items(with_defaults=False)
            
            # Freeze initial as a string, not array
            if 'initial' in items:
                items['initial'] = self.tag_options.initial_string
            
        elif self._deferred_options is not None:
            # When deconstruct is called on a ModelState field, the options
            # will have been deferred
            set_tag_meta, options = self._deferred_options
            items = options
            
        else: # pragma: no cover
            # It should never get here - raise an exception so we can debug
            raise ValueError('Unexpected state')
        
        # Add tag model options to kwargs
        kwargs.update(items)
        
        # Can't freeze lambdas. It should work if it's a function at the top
        # level of a module, but there's no easy way to differentiate. For
        # safety and consistency, strip callable arguments and mention in
        # documentation.
        if 'get_absolute_url' in kwargs:
            del kwargs['get_absolute_url']
        
        # Remove forbidden fields
        # This shouldn't be needed, but strip them just in case
        for forbidden in self.forbidden_fields: # pragma: no cover
            if forbidden in kwargs:
                del kwargs[forbidden]
            
        # Always store _set_tag_meta=True, so migrating tag fields can set tag
        # models' TagMeta
        kwargs['_set_tag_meta'] = True
        
        return name, path, args, kwargs