Field Descriptors - Nautobot Design Builder Documentation

Skip to content

# Field Descriptors

## `nautobot_design_builder.fields`

This module includes the proxy model field descriptors.

The way design builder handles attribute assignment is with descriptors. This allows assignment of values directly to the proxy model and the descriptors handle any necessary deferrals.

For instance, `ForeignKey` relationships require that foreign key object be present in the database before the receiving object (object with the `ForeignKey` field) can be saved. When design builder encounters this situation there are some cases (for example, setting IP addresses on interfaces) where assignment must be deferred in order to guarantee the parent object is present in the database prior to save.

Another example is the reverse side of foreign key relationships. Consider a `Device` which has many `Interface` objects. The device foreign key is defined on the `Interface` object, but we would typically model this in design builder as such:

    devices:
      # additional attributes such as device type, role, location, etc
      # are not illustrated here.
      - name: "My Device"
        interfaces:
          - name: "Ethernet1"
            # type, status, etc
          - name: "Ethernet2"
            # type, status, etc

In order to save these objects to the database, the device must be saved first, and then each interface's device foreign key is set and saved.

Since design builder generally processes things in a depth first order, the natural sequence is for the interfaces (in the above example) to be created first. Therefore, the `ManyToOneRelField` will handle creating an instance of Interface but deferring database save until after the device is saved.

See also: https://docs.python.org/3/howto/descriptor.html

### `BaseModelField`

Bases: `ModelField`

`BaseModelField` is backed by django.db.models.fields.Field.

`BaseModelField` is used as the base class for any design builder field which proxies to a Django model field. Not all design builder fields are for actual model database fields. For instance, custom relationships are something design builder handles with a field descriptor, but they are not backed by django `Field` descriptors.

Source code in `nautobot_design_builder/fields.py`

    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    class BaseModelField(ModelField):  # pylint:disable=too-few-public-methods
        """`BaseModelField` is backed by django.db.models.fields.Field.

        `BaseModelField` is used as the base class for any design builder field
        which proxies to a Django model field. Not all design builder fields are
        for actual model database fields. For instance, custom relationships are
        something design builder handles with a field descriptor, but they
        are not backed by django `Field` descriptors.
        """

        field: django_models.Field
        related_model: Type[object]

        def __init__(self, field: django_models.Field):
            """Initialize a field proxy.

            Args:
                field (django_models.Field): The field that should be proxied on the django model.
            """
            self.field = field
            self.field_name = field.name
            self.related_model = field.related_model

#### `__init__(field)`

Initialize a field proxy.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`field` | `Field` | The field that should be proxied on the django model. | required

Source code in `nautobot_design_builder/fields.py`

    116
    117
    118
    119
    120
    121
    122
    123
    124
    def __init__(self, field: django_models.Field):
        """Initialize a field proxy.

        Args:
            field (django_models.Field): The field that should be proxied on the django model.
        """
        self.field = field
        self.field_name = field.name
        self.related_model = field.related_model

### `CustomRelationshipField`

Bases: `ModelField`, `RelationshipFieldMixin`

This class models a Nautobot custom relationship.

When a design builder model class is created, custom relationships are retrieved for the underlying content-type. Each of these relationships is then added to the design builder proxy model as new properties. The property name is derived from the source or destination of the label, based on which side matches the underlying content-type. The relationship label will return the verbose_name_plural of the other side object if no label has been set.

It should be noted that if a custom relationship's label matches a built-in field, then the proxy model will use the built-in field and the custom relationship will not be accessible. Additionally, a warning will be logged that a potential naming conflict exists.

Source code in `nautobot_design_builder/fields.py`

    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    class CustomRelationshipField(ModelField, RelationshipFieldMixin):  # pylint: disable=too-few-public-methods
        """This class models a Nautobot custom relationship.

        When a design builder model class is created, custom relationships are
        retrieved for the underlying content-type. Each of these relationships is
        then added to the design builder proxy model as new properties. The property
        name is derived from the source or destination of the label, based on which
        side matches the underlying content-type. The relationship label will return
        the verbose_name_plural of the other side object if no label has been set.

        It should be noted that if a custom relationship's label matches a built-in
        field, then the proxy model will use the built-in field and the custom
        relationship will not be accessible. Additionally, a warning will be logged
        that a potential naming conflict exists.
        """

        def __init__(self, model_class, relationship: Relationship):
            """Create a new custom relationship field.

            Args:
                model_class (Model): Model class for this relationship.
                relationship (Relationship): The Nautobot custom relationship backing this field.
            """
            self.relationship = relationship
            field_name = ""
            if self.relationship.source_type == ContentType.objects.get_for_model(model_class.model_class):
                self.related_model = relationship.destination_type.model_class()
                field_name = str(self.relationship.get_label("source")).lower()
            else:
                self.related_model = relationship.source_type.model_class()
                field_name = str(self.relationship.get_label("destination")).lower()
            if hasattr(model_class.model_class, field_name):
                raise FieldNameError(model_class, relationship, field_name)
            self.__set_name__(model_class, str_to_var_name(field_name))
            self.key_name = self.relationship.key

        @debug_set
        def __set__(self, obj: "ModelInstance", values):  # noqa:D105
            """Add an association between the created object and the given value.

            Args:
                obj: (ModelInstance): The object receiving this attribute setter.

                values (Model): The related objects to add.
            """

            def setter():
                for value in values:
                    value = self._get_instance(obj, value)
                    if value.design_metadata.created:
                        value.save()

                    source = obj.design_instance
                    destination = value.design_instance
                    if self.relationship.source_type == ContentType.objects.get_for_model(destination):
                        source, destination = destination, source

                    source_type = ContentType.objects.get_for_model(source)
                    destination_type = ContentType.objects.get_for_model(destination)
                    relationship_association = obj.design_metadata.create_child(
                        RelationshipAssociation,
                        attributes={
                            "relationship_id": self.relationship.id,
                            "source_id": source.id,
                            "source_type_id": source_type.id,
                            "destination_id": destination.id,
                            "destination_type_id": destination_type.id,
                        },
                    )
                    relationship_association.save()

            obj.connect("POST_INSTANCE_SAVE", setter)

#### `__init__(model_class, relationship)`

Create a new custom relationship field.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`model_class` | `Model` | Model class for this relationship. | required
`relationship` | `Relationship` | The Nautobot custom relationship backing this field. | required

Source code in `nautobot_design_builder/fields.py`

    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    def __init__(self, model_class, relationship: Relationship):
        """Create a new custom relationship field.

        Args:
            model_class (Model): Model class for this relationship.
            relationship (Relationship): The Nautobot custom relationship backing this field.
        """
        self.relationship = relationship
        field_name = ""
        if self.relationship.source_type == ContentType.objects.get_for_model(model_class.model_class):
            self.related_model = relationship.destination_type.model_class()
            field_name = str(self.relationship.get_label("source")).lower()
        else:
            self.related_model = relationship.source_type.model_class()
            field_name = str(self.relationship.get_label("destination")).lower()
        if hasattr(model_class.model_class, field_name):
            raise FieldNameError(model_class, relationship, field_name)
        self.__set_name__(model_class, str_to_var_name(field_name))
        self.key_name = self.relationship.key

#### `__set__(obj, values)`

Add an association between the created object and the given value.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`obj` | `ModelInstance` | (ModelInstance): The object receiving this attribute setter. | required
`values` | `Model` | The related objects to add. | required

Source code in `nautobot_design_builder/fields.py`

    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    @debug_set
    def __set__(self, obj: "ModelInstance", values):  # noqa:D105
        """Add an association between the created object and the given value.

        Args:
            obj: (ModelInstance): The object receiving this attribute setter.

            values (Model): The related objects to add.
        """

        def setter():
            for value in values:
                value = self._get_instance(obj, value)
                if value.design_metadata.created:
                    value.save()

                source = obj.design_instance
                destination = value.design_instance
                if self.relationship.source_type == ContentType.objects.get_for_model(destination):
                    source, destination = destination, source

                source_type = ContentType.objects.get_for_model(source)
                destination_type = ContentType.objects.get_for_model(destination)
                relationship_association = obj.design_metadata.create_child(
                    RelationshipAssociation,
                    attributes={
                        "relationship_id": self.relationship.id,
                        "source_id": source.id,
                        "source_type_id": source_type.id,
                        "destination_id": destination.id,
                        "destination_type_id": destination_type.id,
                    },
                )
                relationship_association.save()

        obj.connect("POST_INSTANCE_SAVE", setter)

### `ForeignKeyField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

`ForeignKey` relationship.

Source code in `nautobot_design_builder/fields.py`

    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    class ForeignKeyField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """`ForeignKey` relationship."""

        @debug_set
        def __set__(self, obj: "ModelInstance", value):  # noqa: D105
            deferred = getattr(value, "deferred", False) or (isinstance(value, Mapping) and value.get("deferred", False))

            def setter():
                model_instance = self._get_instance(obj, value)
                if model_instance.design_metadata.created:
                    model_instance.save()

                with change_log(obj, self.field.attname):
                    setattr(obj.design_instance, self.field_name, model_instance.design_instance)

                if deferred:
                    obj.design_instance.save(update_fields=[self.field_name])

            if deferred:
                obj.connect("POST_INSTANCE_SAVE", setter)
            else:
                setter()

### `GenericForeignKeyField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

Generic foreign key field.

Source code in `nautobot_design_builder/fields.py`

    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    class GenericForeignKeyField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """Generic foreign key field."""

        @debug_set
        def __set__(self, obj: "ModelInstance", value):  # noqa:D105
            fk_field = self.field.fk_field
            ct_field = self.field.ct_field
            ct_id_field = obj.design_instance._meta.get_field(ct_field).attname
            with change_log(obj, fk_field), change_log(obj, ct_id_field):
                setattr(obj.design_instance, fk_field, value.design_instance.pk)
                setattr(obj.design_instance, ct_field, ContentType.objects.get_for_model(value.design_instance))

### `GenericRelField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

Field used as part of content-types generic relation.

Source code in `nautobot_design_builder/fields.py`

    375
    376
    377
    378
    379
    380
    381
    class GenericRelField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """Field used as part of content-types generic relation."""

        @debug_set
        def __set__(self, obj: "ModelInstance", value):  # noqa:D105
            with change_log(obj, self.field.attname):
                setattr(obj.design_instance, self.field.attname, self._get_instance(obj, value))

### `GenericRelationField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

Generic relationship field.

Source code in `nautobot_design_builder/fields.py`

    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    class GenericRelationField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """Generic relationship field."""

        @debug_set
        def __set__(self, obj: "ModelInstance", values):  # noqa:D105
            if not isinstance(values, list):
                values = [values]
            items = []
            for value in values:
                value = self._get_instance(obj, value)
                if value.design_metadata.created:
                    value.save()
                items.append(value.design_instance)
            with change_log(obj, self.field_name):
                getattr(obj.design_instance, self.field_name).add(*items)

### `ManyToManyField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

Many to many relationship field.

Source code in `nautobot_design_builder/fields.py`

    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    class ManyToManyField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """Many to many relationship field."""

        def __init__(self, field: django_models.Field):  # noqa:D102,D107
            super().__init__(field)
            self.auto_through = True
            self._init_through()

        def _init_through(self):
            self.through = self.field.remote_field.through
            if not self.through._meta.auto_created:
                self.auto_through = False
                if self.field.remote_field.through_fields:
                    self.link_field = self.field.remote_field.through_fields[0]
                else:
                    for field in self.through._meta.fields:
                        if field.related_model == self.field.model:
                            self.link_field = field.name

        def _get_related_model(self, value):
            """Get the appropriate related model for the value.

            if there is an explicit through class, then we have two choices:
            1) Assign explicitly using the through-class attributes
            2) Assign implicitly like a normal many-to-many

            We want to be able to handle both situations, because it may be that
            the through class has additional attributes. The way we determine if
            the design is requesting the through-class or the implicit related class
            is by examining the values to be assigned and matching their keys with
            the related model and through model.
            """
            if isinstance(value, Mapping):
                attributes = set()
                # Extract all of the top-level field names from the query in order
                # to match them against available fields in the through table. If
                # the set of attributes is a subset of the through class's attributes
                # then use the through class directly, otherwise use the related_model
                # class
                for attribute in value.keys():
                    if attribute.startswith("!get") or attribute.startswith("!create"):
                        attribute_parts = attribute.split(":")
                        attribute = attribute_parts[1]

                    if "__" in attribute:
                        attribute = attribute.split("__")[0]
                    attributes.add(attribute)
                through_fields = set(field.name for field in self.through._meta.fields)
                if self.auto_through is False and attributes.issubset(through_fields):
                    return self.through, attributes.intersection(through_fields)
            return self.related_model, set()

        @debug_set
        def __set__(self, obj: "ModelInstance", values):  # noqa:D105
            def setter():
                items = []
                for value in values:
                    related_model, through_fields = self._get_related_model(value)
                    relationship_manager = getattr(obj.design_instance, self.field_name).model.objects
                    if through_fields:
                        value[f"!create_or_update:{self.link_field}_id"] = str(obj.design_instance.id)
                        relationship_manager = self.through.objects

                    for field in through_fields:
                        value[f"!create_or_update:{field}"] = value.pop(field)
                    value = self._get_instance(obj, value, relationship_manager, related_model)
                    if related_model is not self.through:
                        items.append(value.design_instance)
                    else:
                        setattr(value.design_instance, self.link_field, obj.design_instance)
                    if value.design_metadata.created:
                        value.save()
                if items:
                    with change_log(obj, self.field_name):
                        getattr(obj.design_instance, self.field_name).add(*items)

            obj.connect("POST_INSTANCE_SAVE", setter)

### `ManyToManyRelField`

Bases: `ManyToManyField`

Reverse many to many relationship field.

Source code in `nautobot_design_builder/fields.py`

    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    class ManyToManyRelField(ManyToManyField):  # pylint:disable=too-few-public-methods
        """Reverse many to many relationship field."""

        def _init_through(self):
            self.through = self.field.through
            if not self.through._meta.auto_created:
                self.auto_through = False
                if self.field.through_fields:
                    self.link_field = self.field.through_fields[0]
                else:
                    for field in self.through._meta.fields:
                        if field.related_model == self.field.model:
                            self.link_field = field.name

### `ManyToOneRelField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

The reverse side of a `ForeignKey` relationship.

Source code in `nautobot_design_builder/fields.py`

    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    class ManyToOneRelField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """The reverse side of a `ForeignKey` relationship."""

        @debug_set
        def __set__(self, obj: "ModelInstance", values):  # noqa:D105
            if not isinstance(values, list):
                raise DesignImplementationError("Many-to-one fields must be a list", obj)

            def setter():
                for value in values:
                    value = self._get_instance(obj, value, getattr(obj, self.field_name))
                    with change_log(value, self.field.field.attname):
                        setattr(value.design_instance, self.field.field.name, obj.design_instance)
                    value.save()

            obj.connect("POST_INSTANCE_SAVE", setter)

### `ModelField`

Bases: `ABC`

This represents any type of field (attribute or relationship) on a Nautobot model.

The design builder fields are descriptors that are used to build the correct relationship hierarchy of django models based on the design input. The fields are used to sequence saves and value assignments in the correct order.

Source code in `nautobot_design_builder/fields.py`

     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    class ModelField(ABC):
        """This represents any type of field (attribute or relationship) on a Nautobot model.

        The design builder fields are descriptors that are used to build the
        correct relationship hierarchy of django models based on the design input.
        The fields are used to sequence saves and value assignments in the correct
        order.
        """

        field_name: str

        def __set_name__(self, owner, name):  # noqa: D105
            self.field_name = name

        def __get__(self, obj, objtype=None) -> Any:
            """Retrieve the field value.

            In the event `obj` is None (as in getting the attribute from the class) then
            get the descriptor itself.

            Args:
                obj (ModelInstance): The model to retrieve the field value
                objtype (type, optional): The owning class of the descriptor. Defaults to None.

            Returns:
                Any: Either the descriptor instance or the field value.
            """
            if obj is None or obj.design_instance is None:
                return self
            return getattr(obj.design_instance, self.field_name)

        @abstractmethod
        def __set__(self, obj: "ModelInstance", value):
            """Method used to set the value of the field.

            Args:
                obj: (ModelInstance): The model to update.
                value (Any): Value that should be set on the model field.
            """

#### `__get__(obj, objtype=None)`

Retrieve the field value.

In the event `obj` is None (as in getting the attribute from the class) then get the descriptor itself.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`obj` | `ModelInstance` | The model to retrieve the field value | required
`objtype` | `type` | The owning class of the descriptor. Defaults to None. | `None`

Returns:

Name | Type | Description
--- | --- | ---
`Any` | `Any` | Either the descriptor instance or the field value.

Source code in `nautobot_design_builder/fields.py`

    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    def __get__(self, obj, objtype=None) -> Any:
        """Retrieve the field value.

        In the event `obj` is None (as in getting the attribute from the class) then
        get the descriptor itself.

        Args:
            obj (ModelInstance): The model to retrieve the field value
            objtype (type, optional): The owning class of the descriptor. Defaults to None.

        Returns:
            Any: Either the descriptor instance or the field value.
        """
        if obj is None or obj.design_instance is None:
            return self
        return getattr(obj.design_instance, self.field_name)

#### `__set__(obj, value)` `abstractmethod`

Method used to set the value of the field.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`obj` | `ModelInstance` | (ModelInstance): The model to update. | required
`value` | `Any` | Value that should be set on the model field. | required

Source code in `nautobot_design_builder/fields.py`

     93
     94
     95
     96
     97
     98
     99
    100
    @abstractmethod
    def __set__(self, obj: "ModelInstance", value):
        """Method used to set the value of the field.

        Args:
            obj: (ModelInstance): The model to update.
            value (Any): Value that should be set on the model field.
        """

### `RelationshipFieldMixin`

Field mixin for relationships to other models.

`RelationshipField` instances represent fields that have some sort of relationship to other objects. These include `ForeignKey` and `ManyToMany`. Relationship fields also include the reverse side of fields or even custom relationships.

Source code in `nautobot_design_builder/fields.py`

    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    class RelationshipFieldMixin:  # pylint:disable=too-few-public-methods
        """Field mixin for relationships to other models.

        `RelationshipField` instances represent fields that have some sort of relationship
        to other objects. These include `ForeignKey` and `ManyToMany`.
        Relationship fields also include the reverse side of fields or even custom relationships.
        """

        def _get_instance(
            self, obj: "ModelInstance", value: Any, relationship_manager: django_models.Manager = None, related_model=None
        ):
            """Helper function to create a new child model from a value.

            If the passed-in value is a dictionary, this method assumes that the dictionary
            represents a new design builder object which will belong to a parent. In this
            case a new child is created from the parent object.

            If the value is not a dictionary, it is simply returned.

            Args:
                obj (ModelInstance): The parent object that the value will be ultimately assigned.

                value (Any): The value being assigned to the parent object.

                relationship_manager (Manager, optional): This argument can be used to restrict the
                  child object lookups to a subset. For instance, the `interfaces` manager on a `Device`
                  instance will restrict queries interfaces where their foreign key is set to the device.
                  Defaults to None.

                related_model: The model class to use for creating new children. Defaults to the
                  field's related model.

            Returns:
                ModelInstance: Either a newly created `ModelInstance` or the original value.
            """
            if related_model is None:
                related_model = self.related_model
            if isinstance(value, Mapping):
                value = obj.design_metadata.create_child(related_model, value, relationship_manager)
            return value

### `SimpleField`

Bases: `BaseModelField`

A field that accepts a scalar value.

`SimpleField` will immediately set scalar values on the underlying field. This includes assignment to fields such as `CharField` or `IntegerField`. When this descriptor is called, the assigned value is immediately passed to the underlying model object.

Source code in `nautobot_design_builder/fields.py`

    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    class SimpleField(BaseModelField):  # pylint:disable=too-few-public-methods
        """A field that accepts a scalar value.

        `SimpleField` will immediately set scalar values on the underlying field. This
        includes assignment to fields such as `CharField` or `IntegerField`. When
        this descriptor is called, the assigned value is immediately passed to the
        underlying model object.
        """

        @debug_set
        def __set__(self, obj: "ModelInstance", value):  # noqa: D105
            with change_log(obj, self.field_name):
                setattr(obj.design_instance, self.field_name, value)

### `TagField`

Bases: `BaseModelField`, `RelationshipFieldMixin`

Taggit field.

Source code in `nautobot_design_builder/fields.py`

    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    class TagField(BaseModelField, RelationshipFieldMixin):  # pylint:disable=too-few-public-methods
        """Taggit field."""

        def __init__(self, field: django_models.Field):  # noqa:D102,D107
            super().__init__(field)
            self.related_model = field.remote_field.model

        def __set__(self, obj: "ModelInstance", values):  # noqa:D105
            # I hate that this code is almost identical to the ManyToManyField
            # __set__ code, but I don't see an easy way to DRY it up at the
            # moment.
            def setter():
                items = []
                for value in values:
                    value = self._get_instance(obj, value, getattr(obj.design_instance, self.field_name))
                    if value.design_metadata.created:
                        value.save()
                    items.append(value.design_instance)
                if items:
                    with change_log(obj, self.field_name):
                        getattr(obj.design_instance, self.field_name).add(*items)

            obj.connect("POST_INSTANCE_SAVE", setter)

### `field_factory(arg1, arg2)`

Factory function to create a ModelField.

Source code in `nautobot_design_builder/fields.py`

    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    def field_factory(arg1, arg2) -> ModelField:
        """Factory function to create a ModelField."""
        if isinstance(arg2, Relationship):
            return CustomRelationshipField(arg1, arg2)

        field = None
        if not arg2.is_relation:
            field = SimpleField(arg2)
        elif isinstance(arg2, ct_fields.GenericRelation):
            field = GenericRelationField(arg2)
        elif isinstance(arg2, ct_fields.GenericRel):
            field = GenericRelField(arg2)
        elif isinstance(arg2, ct_fields.GenericForeignKey):
            field = GenericForeignKeyField(arg2)
        elif isinstance(arg2, TaggableManager):
            field = TagField(arg2)
        elif isinstance(arg2, django_models.ForeignKey):
            field = ForeignKeyField(arg2)
        elif isinstance(arg2, django_models.ManyToOneRel):
            field = ManyToOneRelField(arg2)
        elif isinstance(arg2, django_models.ManyToManyField):
            field = ManyToManyField(arg2)
        elif isinstance(arg2, django_models.ManyToManyRel):
            field = ManyToManyRelField(arg2)
        else:
            raise DesignImplementationError(f"Cannot manufacture field for {type(arg2)}, {arg2} {arg2.is_relation}")
        return field