File: declarative.py

package info (click to toggle)
python-django-import-export 4.3.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,300 kB
  • sloc: python: 11,650; makefile: 180; sh: 63; javascript: 50
file content (171 lines) | stat: -rw-r--r-- 6,702 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
import logging
import warnings
from collections import OrderedDict

from django.apps import apps
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel

from import_export.options import ResourceOptions

from .fields import Field
from .instance_loaders import ModelInstanceLoader
from .utils import get_related_model

logger = logging.getLogger(__name__)


class DeclarativeMetaclass(type):
    def __new__(cls, name, bases, attrs):
        def _load_meta_options(base_, meta_):
            options = getattr(base_, "Meta", None)

            for option in [
                option
                for option in dir(options)
                if not option.startswith("_") and hasattr(options, option)
            ]:
                option_value = getattr(options, option)
                if option == "model" and isinstance(option_value, str):
                    option_value = apps.get_model(option_value)

                setattr(meta_, option, option_value)

        declared_fields = []
        meta = ResourceOptions()

        # If this class is subclassing another Resource, add that Resource's
        # fields. Note that we loop over the bases in *reverse*. This is
        # necessary in order to preserve the correct order of fields.
        for base in bases[::-1]:
            if hasattr(base, "fields"):
                declared_fields = list(base.fields.items()) + declared_fields
                # Collect the Meta options
                # #1363 If there are any parent classes, set those options first
                for parent in base.__bases__:
                    _load_meta_options(parent, meta)
                _load_meta_options(base, meta)

        # Add direct fields
        for field_name, obj in attrs.copy().items():
            if isinstance(obj, Field):
                field = attrs.pop(field_name)
                if not field.column_name:
                    field.column_name = field_name
                declared_fields.append((field_name, field))

        attrs["fields"] = OrderedDict(declared_fields)
        new_class = super().__new__(cls, name, bases, attrs)
        # add direct fields
        _load_meta_options(new_class, meta)
        new_class._meta = meta

        return new_class


class ModelDeclarativeMetaclass(DeclarativeMetaclass):
    def __new__(cls, name, bases, attrs):
        new_class = super().__new__(cls, name, bases, attrs)

        opts = new_class._meta

        if not opts.instance_loader_class:
            opts.instance_loader_class = ModelInstanceLoader

        if opts.model:
            model_opts = opts.model._meta

            # #1693 check the fields explicitly declared as attributes of the Resource
            # class.
            # if 'fields' property is defined, declared fields can only be included
            # if they appear in the 'fields' iterable.
            declared_fields = {}
            for field_name, field in new_class.fields.items():
                column_name = field.column_name
                if (
                    opts.fields is not None
                    and field_name not in opts.fields
                    and column_name not in opts.fields
                ):
                    warnings.warn(
                        f"ignoring field '{field_name}' because not declared "
                        "in 'fields' whitelist",
                        stacklevel=2,
                    )
                    continue
                declared_fields[field_name] = field

            field_list = []
            for f in sorted(model_opts.fields + model_opts.many_to_many):
                if opts.fields is not None and f.name not in opts.fields:
                    continue
                if opts.exclude and f.name in opts.exclude:
                    continue

                if f.name in set(declared_fields.keys()):
                    # If model field is declared in `ModelResource`,
                    # remove it from `declared_fields`
                    # to keep exact order of model fields
                    field = declared_fields.pop(f.name)
                else:
                    field = new_class.field_from_django_field(f.name, f, readonly=False)

                field_list.append(
                    (
                        f.name,
                        field,
                    )
                )

            # Order as model fields first then declared fields by default
            new_class.fields = OrderedDict([*field_list, *declared_fields.items()])

            # add fields that follow relationships
            if opts.fields is not None:
                field_list = []
                for field_name in opts.fields:
                    if field_name in declared_fields:
                        continue
                    if field_name.find("__") == -1:
                        continue

                    model = opts.model
                    attrs = field_name.split("__")
                    for i, attr in enumerate(attrs):
                        verbose_path = ".".join(
                            [opts.model.__name__] + attrs[0 : i + 1]
                        )

                        try:
                            f = model._meta.get_field(attr)
                        except FieldDoesNotExist as e:
                            logger.debug(e, exc_info=e)
                            raise FieldDoesNotExist(
                                "%s: %s has no field named '%s'"
                                % (verbose_path, model.__name__, attr)
                            )

                        if i < len(attrs) - 1:
                            # We're not at the last attribute yet, so check
                            # that we're looking at a relation, and move on to
                            # the next model.
                            if isinstance(f, ForeignObjectRel):
                                model = get_related_model(f)
                            else:
                                if get_related_model(f) is None:
                                    raise KeyError(
                                        "%s is not a relation" % verbose_path
                                    )
                                model = get_related_model(f)

                    if isinstance(f, ForeignObjectRel):
                        f = f.field

                    field = new_class.field_from_django_field(
                        field_name, f, readonly=True
                    )
                    field_list.append((field_name, field))

                new_class.fields.update(OrderedDict(field_list))

        return new_class