Skip to content

context

xsdata.formats.dataclass.context

XmlContext

The models context class.

The context is responsible to provide binding metadata for models and their fields.

Parameters:

Name Type Description Default
element_name_generator Callable

Default element name generator

return_input
attribute_name_generator Callable

Default attribute name generator

return_input
class_type str

Default class type dataclasses

'dataclasses'
models_package Optional[str]

Restrict auto locate to a specific package

None

Attributes:

Name Type Description
cache Dict[Type, XmlMeta]

Internal cache for binding metadata instances

xsi_cache Dict[str, List[Type]]

Internal cache for xsi types to class locations

sys_modules

The number of loaded sys modules

Source code in xsdata/formats/dataclass/context.py
 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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
303
304
305
306
307
308
309
class XmlContext:
    """The models context class.

    The context is responsible to provide binding metadata
    for models and their fields.

    Args:
        element_name_generator: Default element name generator
        attribute_name_generator: Default attribute name generator
        class_type: Default class type `dataclasses`
        models_package: Restrict auto locate to a specific package

    Attributes:
        cache: Internal cache for binding metadata instances
        xsi_cache: Internal cache for xsi types to class locations
        sys_modules: The number of loaded sys modules
    """

    __slots__ = (
        "element_name_generator",
        "attribute_name_generator",
        "class_type",
        "models_package",
        "cache",
        "xsi_cache",
        "sys_modules",
    )

    def __init__(
        self,
        element_name_generator: Callable = return_input,
        attribute_name_generator: Callable = return_input,
        class_type: str = "dataclasses",
        models_package: Optional[str] = None,
    ):
        self.element_name_generator = element_name_generator
        self.attribute_name_generator = attribute_name_generator
        self.class_type = class_types.get_type(class_type)

        self.cache: Dict[Type, XmlMeta] = {}
        self.xsi_cache: Dict[str, List[Type]] = defaultdict(list)
        self.models_package = models_package
        self.sys_modules = 0

    def reset(self):
        """Reset all internal caches."""
        self.cache.clear()
        self.xsi_cache.clear()
        self.sys_modules = 0

    def get_builder(
        self,
        globalns: Optional[Dict[str, Callable]] = None,
    ) -> XmlMetaBuilder:
        """Return a new xml meta builder instance."""
        return XmlMetaBuilder(
            class_type=self.class_type,
            element_name_generator=self.element_name_generator,
            attribute_name_generator=self.attribute_name_generator,
            globalns=globalns,
        )

    def fetch(
        self,
        clazz: Type,
        parent_ns: Optional[str] = None,
        xsi_type: Optional[str] = None,
    ) -> XmlMeta:
        """Build the model metadata for the given class.

        Args:
            clazz: The requested dataclass type
            parent_ns: The inherited parent namespace
            xsi_type: if present it means that the given clazz is
                derived and the lookup procedure needs to check and match a
                dataclass model to the qualified name instead.

        Returns:
            A xml meta instance
        """
        meta = self.build(clazz, parent_ns)
        subclass = None
        if xsi_type and meta.target_qname != xsi_type:
            subclass = self.find_subclass(clazz, xsi_type)

        return self.build(subclass, parent_ns) if subclass else meta

    def build_xsi_cache(self):
        """Index all imported data classes by their xsi:type qualified name."""
        if len(sys.modules) == self.sys_modules:
            return

        self.xsi_cache.clear()
        builder = self.get_builder()
        for clazz in self.get_subclasses(object):
            if self.is_binding_model(clazz):
                meta = builder.build_class_meta(clazz)

                if meta.target_qname:
                    self.xsi_cache[meta.target_qname].append(clazz)

        self.sys_modules = len(sys.modules)

    def is_binding_model(self, clazz: Type[T]) -> bool:
        """Return whether the clazz is a binding model.

        If the models package is not empty also validate
        the class is located within that package.

        Args:
            clazz: The class type to inspect

        Returns:
            The bool result.
        """
        if not self.class_type.is_model(clazz):
            return False

        return not self.models_package or (
            hasattr(clazz, "__module__")
            and isinstance(clazz.__module__, str)
            and clazz.__module__.startswith(self.models_package)
        )

    def find_types(self, qname: str) -> List[Type[T]]:
        """Find all classes that match the given xsi:type qname.

        - Ignores native schema types, xs:string, xs:float, xs:int, ...
        - Rebuild cache if new modules were imported since last run

        Args:
            qname: A namespace qualified name

        Returns:
            A list of the matched classes.
        """
        if not DataType.from_qname(qname):
            self.build_xsi_cache()
            if qname in self.xsi_cache:
                return self.xsi_cache[qname]

        return []

    def find_type(self, qname: str) -> Optional[Type[T]]:
        """Return the last imported class that matches the given xsi:type qname.

        Args:
            qname: A namespace qualified name

        Returns:
            A class type or None if no matches.
        """
        types: List[Type] = self.find_types(qname)
        return types[-1] if types else None

    def find_type_by_fields(self, field_names: Set[str]) -> Optional[Type[T]]:
        """Find a data class that matches best the given list of field names.

        Args:
            field_names: A set of field names

        Returns:
            The best matching class or None if no matches. The class must
            have all the fields. If more than one classes have all the given
            fields, return the one with the least extra fields.
        """

        def get_field_diff(clazz: Type) -> int:
            meta = self.cache[clazz]
            local_names = {var.local_name for var in meta.get_all_vars()}
            return len(local_names - field_names)

        self.build_xsi_cache()
        choices = [
            (clazz, get_field_diff(clazz))
            for types in self.xsi_cache.values()
            for clazz in types
            if self.local_names_match(field_names, clazz)
        ]

        choices.sort(key=lambda x: (x[1], x[0].__name__))
        return choices[0][0] if len(choices) > 0 else None

    def find_subclass(self, clazz: Type, qname: str) -> Optional[Type]:
        """Find a subclass for the given clazz and xsi:type qname.

        Compare all classes that match the given xsi:type qname and return the
        first one that is either a subclass or shares the same parent class as
        the original class.

        Args:
            clazz: The input clazz type
            qname: The xsi:type to lookup from cache

        Returns:
            The matching class type or None if no matches.
        """
        types: List[Type] = self.find_types(qname)
        for tp in types:
            # Why would a xml node with have a xsi:type that points
            # to parent class is beyond me, but it happens, let's protect
            # against that scenario <node xsi:type="nodeAbstract" />
            if issubclass(clazz, tp):
                continue

            for tp_mro in tp.__mro__:
                if tp_mro is not object and tp_mro in clazz.__mro__:
                    return tp

        return None

    def build(
        self,
        clazz: Type,
        parent_ns: Optional[str] = None,
        globalns: Optional[Dict[str, Callable]] = None,
    ) -> XmlMeta:
        """Fetch or build the binding metadata for the given class.

        Args:
            clazz: A class type
            parent_ns: The inherited parent namespace
            globalns: Override the global python namespace

        Returns:
            The class binding metadata instance.
        """
        if clazz not in self.cache:
            builder = self.get_builder(globalns)
            self.cache[clazz] = builder.build(clazz, parent_ns)
        return self.cache[clazz]

    def build_recursive(self, clazz: Type, parent_ns: Optional[str] = None):
        """Build the binding metadata for the given class and all of its dependencies.

        This method is used in benchmarks!

        Args:
            clazz: The class type
            parent_ns: The inherited parent namespace
        """
        if clazz not in self.cache:
            meta = self.build(clazz, parent_ns)
            for var in meta.get_all_vars():
                types = var.element_types if var.elements else var.types
                for tp in types:
                    if self.class_type.is_model(tp):
                        self.build_recursive(tp, meta.namespace)

    def local_names_match(self, names: Set[str], clazz: Type) -> bool:
        """Check if the given field names match the given class type.

        Silently ignore, typing errors. These classes are from third
        party libraries most of them time.

        Args:
            names: A set of field names
            clazz: The class type to inspect

        Returns:
            Whether the class contains all the field names.
        """
        try:
            meta = self.build(clazz)
            local_names = {var.local_name for var in meta.get_all_vars()}
            return not names.difference(local_names)
        except (XmlContextError, NameError, TypeError):
            # The dataclass includes unsupported typing annotations
            # Let's remove it from xsi_cache
            builder = self.get_builder()
            target_qname = builder.build_class_meta(clazz).target_qname
            if target_qname and target_qname in self.xsi_cache:
                self.xsi_cache[target_qname].remove(clazz)

            return False

    @classmethod
    def is_derived(cls, obj: Any, clazz: Type) -> bool:
        """Return whether the obj is a subclass or a parent of the given class type."""
        if obj is None:
            return False

        if isinstance(obj, clazz):
            return True

        return any(x is not object and isinstance(obj, x) for x in clazz.__bases__)

    @classmethod
    def get_subclasses(cls, clazz: Type) -> Iterator[Type]:
        """Return an iterator of the given class subclasses."""
        try:
            for subclass in clazz.__subclasses__():
                yield from cls.get_subclasses(subclass)
                yield subclass
        except TypeError:
            pass

reset()

Reset all internal caches.

Source code in xsdata/formats/dataclass/context.py
58
59
60
61
62
def reset(self):
    """Reset all internal caches."""
    self.cache.clear()
    self.xsi_cache.clear()
    self.sys_modules = 0

get_builder(globalns=None)

Return a new xml meta builder instance.

Source code in xsdata/formats/dataclass/context.py
64
65
66
67
68
69
70
71
72
73
74
def get_builder(
    self,
    globalns: Optional[Dict[str, Callable]] = None,
) -> XmlMetaBuilder:
    """Return a new xml meta builder instance."""
    return XmlMetaBuilder(
        class_type=self.class_type,
        element_name_generator=self.element_name_generator,
        attribute_name_generator=self.attribute_name_generator,
        globalns=globalns,
    )

fetch(clazz, parent_ns=None, xsi_type=None)

Build the model metadata for the given class.

Parameters:

Name Type Description Default
clazz Type

The requested dataclass type

required
parent_ns Optional[str]

The inherited parent namespace

None
xsi_type Optional[str]

if present it means that the given clazz is derived and the lookup procedure needs to check and match a dataclass model to the qualified name instead.

None

Returns:

Type Description
XmlMeta

A xml meta instance

Source code in xsdata/formats/dataclass/context.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def fetch(
    self,
    clazz: Type,
    parent_ns: Optional[str] = None,
    xsi_type: Optional[str] = None,
) -> XmlMeta:
    """Build the model metadata for the given class.

    Args:
        clazz: The requested dataclass type
        parent_ns: The inherited parent namespace
        xsi_type: if present it means that the given clazz is
            derived and the lookup procedure needs to check and match a
            dataclass model to the qualified name instead.

    Returns:
        A xml meta instance
    """
    meta = self.build(clazz, parent_ns)
    subclass = None
    if xsi_type and meta.target_qname != xsi_type:
        subclass = self.find_subclass(clazz, xsi_type)

    return self.build(subclass, parent_ns) if subclass else meta

build_xsi_cache()

Index all imported data classes by their xsi:type qualified name.

Source code in xsdata/formats/dataclass/context.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def build_xsi_cache(self):
    """Index all imported data classes by their xsi:type qualified name."""
    if len(sys.modules) == self.sys_modules:
        return

    self.xsi_cache.clear()
    builder = self.get_builder()
    for clazz in self.get_subclasses(object):
        if self.is_binding_model(clazz):
            meta = builder.build_class_meta(clazz)

            if meta.target_qname:
                self.xsi_cache[meta.target_qname].append(clazz)

    self.sys_modules = len(sys.modules)

is_binding_model(clazz)

Return whether the clazz is a binding model.

If the models package is not empty also validate the class is located within that package.

Parameters:

Name Type Description Default
clazz Type[T]

The class type to inspect

required

Returns:

Type Description
bool

The bool result.

Source code in xsdata/formats/dataclass/context.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def is_binding_model(self, clazz: Type[T]) -> bool:
    """Return whether the clazz is a binding model.

    If the models package is not empty also validate
    the class is located within that package.

    Args:
        clazz: The class type to inspect

    Returns:
        The bool result.
    """
    if not self.class_type.is_model(clazz):
        return False

    return not self.models_package or (
        hasattr(clazz, "__module__")
        and isinstance(clazz.__module__, str)
        and clazz.__module__.startswith(self.models_package)
    )

find_types(qname)

Find all classes that match the given xsi:type qname.

  • Ignores native schema types, xs:string, xs:float, xs:int, ...
  • Rebuild cache if new modules were imported since last run

Parameters:

Name Type Description Default
qname str

A namespace qualified name

required

Returns:

Type Description
List[Type[T]]

A list of the matched classes.

Source code in xsdata/formats/dataclass/context.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def find_types(self, qname: str) -> List[Type[T]]:
    """Find all classes that match the given xsi:type qname.

    - Ignores native schema types, xs:string, xs:float, xs:int, ...
    - Rebuild cache if new modules were imported since last run

    Args:
        qname: A namespace qualified name

    Returns:
        A list of the matched classes.
    """
    if not DataType.from_qname(qname):
        self.build_xsi_cache()
        if qname in self.xsi_cache:
            return self.xsi_cache[qname]

    return []

find_type(qname)

Return the last imported class that matches the given xsi:type qname.

Parameters:

Name Type Description Default
qname str

A namespace qualified name

required

Returns:

Type Description
Optional[Type[T]]

A class type or None if no matches.

Source code in xsdata/formats/dataclass/context.py
157
158
159
160
161
162
163
164
165
166
167
def find_type(self, qname: str) -> Optional[Type[T]]:
    """Return the last imported class that matches the given xsi:type qname.

    Args:
        qname: A namespace qualified name

    Returns:
        A class type or None if no matches.
    """
    types: List[Type] = self.find_types(qname)
    return types[-1] if types else None

find_type_by_fields(field_names)

Find a data class that matches best the given list of field names.

Parameters:

Name Type Description Default
field_names Set[str]

A set of field names

required

Returns:

Type Description
Optional[Type[T]]

The best matching class or None if no matches. The class must

Optional[Type[T]]

have all the fields. If more than one classes have all the given

Optional[Type[T]]

fields, return the one with the least extra fields.

Source code in xsdata/formats/dataclass/context.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def find_type_by_fields(self, field_names: Set[str]) -> Optional[Type[T]]:
    """Find a data class that matches best the given list of field names.

    Args:
        field_names: A set of field names

    Returns:
        The best matching class or None if no matches. The class must
        have all the fields. If more than one classes have all the given
        fields, return the one with the least extra fields.
    """

    def get_field_diff(clazz: Type) -> int:
        meta = self.cache[clazz]
        local_names = {var.local_name for var in meta.get_all_vars()}
        return len(local_names - field_names)

    self.build_xsi_cache()
    choices = [
        (clazz, get_field_diff(clazz))
        for types in self.xsi_cache.values()
        for clazz in types
        if self.local_names_match(field_names, clazz)
    ]

    choices.sort(key=lambda x: (x[1], x[0].__name__))
    return choices[0][0] if len(choices) > 0 else None

find_subclass(clazz, qname)

Find a subclass for the given clazz and xsi:type qname.

Compare all classes that match the given xsi:type qname and return the first one that is either a subclass or shares the same parent class as the original class.

Parameters:

Name Type Description Default
clazz Type

The input clazz type

required
qname str

The xsi:type to lookup from cache

required

Returns:

Type Description
Optional[Type]

The matching class type or None if no matches.

Source code in xsdata/formats/dataclass/context.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def find_subclass(self, clazz: Type, qname: str) -> Optional[Type]:
    """Find a subclass for the given clazz and xsi:type qname.

    Compare all classes that match the given xsi:type qname and return the
    first one that is either a subclass or shares the same parent class as
    the original class.

    Args:
        clazz: The input clazz type
        qname: The xsi:type to lookup from cache

    Returns:
        The matching class type or None if no matches.
    """
    types: List[Type] = self.find_types(qname)
    for tp in types:
        # Why would a xml node with have a xsi:type that points
        # to parent class is beyond me, but it happens, let's protect
        # against that scenario <node xsi:type="nodeAbstract" />
        if issubclass(clazz, tp):
            continue

        for tp_mro in tp.__mro__:
            if tp_mro is not object and tp_mro in clazz.__mro__:
                return tp

    return None

build(clazz, parent_ns=None, globalns=None)

Fetch or build the binding metadata for the given class.

Parameters:

Name Type Description Default
clazz Type

A class type

required
parent_ns Optional[str]

The inherited parent namespace

None
globalns Optional[Dict[str, Callable]]

Override the global python namespace

None

Returns:

Type Description
XmlMeta

The class binding metadata instance.

Source code in xsdata/formats/dataclass/context.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def build(
    self,
    clazz: Type,
    parent_ns: Optional[str] = None,
    globalns: Optional[Dict[str, Callable]] = None,
) -> XmlMeta:
    """Fetch or build the binding metadata for the given class.

    Args:
        clazz: A class type
        parent_ns: The inherited parent namespace
        globalns: Override the global python namespace

    Returns:
        The class binding metadata instance.
    """
    if clazz not in self.cache:
        builder = self.get_builder(globalns)
        self.cache[clazz] = builder.build(clazz, parent_ns)
    return self.cache[clazz]

build_recursive(clazz, parent_ns=None)

Build the binding metadata for the given class and all of its dependencies.

This method is used in benchmarks!

Parameters:

Name Type Description Default
clazz Type

The class type

required
parent_ns Optional[str]

The inherited parent namespace

None
Source code in xsdata/formats/dataclass/context.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def build_recursive(self, clazz: Type, parent_ns: Optional[str] = None):
    """Build the binding metadata for the given class and all of its dependencies.

    This method is used in benchmarks!

    Args:
        clazz: The class type
        parent_ns: The inherited parent namespace
    """
    if clazz not in self.cache:
        meta = self.build(clazz, parent_ns)
        for var in meta.get_all_vars():
            types = var.element_types if var.elements else var.types
            for tp in types:
                if self.class_type.is_model(tp):
                    self.build_recursive(tp, meta.namespace)

local_names_match(names, clazz)

Check if the given field names match the given class type.

Silently ignore, typing errors. These classes are from third party libraries most of them time.

Parameters:

Name Type Description Default
names Set[str]

A set of field names

required
clazz Type

The class type to inspect

required

Returns:

Type Description
bool

Whether the class contains all the field names.

Source code in xsdata/formats/dataclass/context.py
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
def local_names_match(self, names: Set[str], clazz: Type) -> bool:
    """Check if the given field names match the given class type.

    Silently ignore, typing errors. These classes are from third
    party libraries most of them time.

    Args:
        names: A set of field names
        clazz: The class type to inspect

    Returns:
        Whether the class contains all the field names.
    """
    try:
        meta = self.build(clazz)
        local_names = {var.local_name for var in meta.get_all_vars()}
        return not names.difference(local_names)
    except (XmlContextError, NameError, TypeError):
        # The dataclass includes unsupported typing annotations
        # Let's remove it from xsi_cache
        builder = self.get_builder()
        target_qname = builder.build_class_meta(clazz).target_qname
        if target_qname and target_qname in self.xsi_cache:
            self.xsi_cache[target_qname].remove(clazz)

        return False

is_derived(obj, clazz) classmethod

Return whether the obj is a subclass or a parent of the given class type.

Source code in xsdata/formats/dataclass/context.py
290
291
292
293
294
295
296
297
298
299
@classmethod
def is_derived(cls, obj: Any, clazz: Type) -> bool:
    """Return whether the obj is a subclass or a parent of the given class type."""
    if obj is None:
        return False

    if isinstance(obj, clazz):
        return True

    return any(x is not object and isinstance(obj, x) for x in clazz.__bases__)

get_subclasses(clazz) classmethod

Return an iterator of the given class subclasses.

Source code in xsdata/formats/dataclass/context.py
301
302
303
304
305
306
307
308
309
@classmethod
def get_subclasses(cls, clazz: Type) -> Iterator[Type]:
    """Return an iterator of the given class subclasses."""
    try:
        for subclass in clazz.__subclasses__():
            yield from cls.get_subclasses(subclass)
            yield subclass
    except TypeError:
        pass