import sys
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import field
from dataclasses import replace
from enum import IntEnum
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Type
from xsdata.formats.converter import converter
from xsdata.formats.dataclass.models.constants import XmlType
from xsdata.models.enums import DataType
from xsdata.models.enums import QNames
from xsdata.models.enums import Tag
from xsdata.models.mixins import ElementBase
from xsdata.models.wsdl import BindingMessage
from xsdata.models.wsdl import BindingOperation
from xsdata.models.wsdl import Message
from xsdata.models.xsd import ComplexType
from xsdata.models.xsd import Element
from xsdata.utils import text
xml_type_map = {
Tag.ELEMENT: XmlType.ELEMENT,
Tag.ANY: XmlType.WILDCARD,
Tag.ANY_ATTRIBUTE: XmlType.ATTRIBUTES,
Tag.ATTRIBUTE: XmlType.ATTRIBUTE,
None: XmlType.TEXT,
}
[docs]@dataclass
class Restrictions:
"""
Model representation of a dataclass field validation and type metadata.
:param required:
:param prohibited:
:param min_occurs:
:param max_occurs:
:param min_exclusive:
:param min_inclusive:
:param min_length:
:param max_exclusive:
:param max_inclusive:
:param max_length:
:param total_digits:
:param fraction_digits:
:param length:
:param white_space:
:param pattern:
:param explicit_timezone:
:param nillable:
:param sequential:
:param tokens:
"""
required: Optional[bool] = field(default=None)
prohibited: Optional[bool] = field(default=None)
min_occurs: Optional[int] = field(default=None)
max_occurs: Optional[int] = field(default=None)
min_exclusive: Optional[str] = field(default=None)
min_inclusive: Optional[str] = field(default=None)
min_length: Optional[int] = field(default=None)
max_exclusive: Optional[str] = field(default=None)
max_inclusive: Optional[str] = field(default=None)
max_length: Optional[int] = field(default=None)
total_digits: Optional[int] = field(default=None)
fraction_digits: Optional[int] = field(default=None)
length: Optional[int] = field(default=None)
white_space: Optional[str] = field(default=None)
pattern: Optional[str] = field(default=None)
explicit_timezone: Optional[str] = field(default=None)
nillable: Optional[bool] = field(default=None)
sequential: Optional[bool] = field(default=None)
tokens: Optional[bool] = field(default=None)
@property
def is_list(self) -> bool:
"""Return true if max occurs property is larger than one."""
return self.max_occurs is not None and self.max_occurs > 1
@property
def is_optional(self) -> bool:
"""Return true if min occurs property equals zero."""
return self.min_occurs == 0
[docs] def merge(self, source: "Restrictions"):
"""Update properties from another instance."""
keys = (
"required",
"prohibited",
"min_exclusive",
"min_inclusive",
"min_length",
"max_exclusive",
"max_inclusive",
"max_length",
"total_digits",
"fraction_digits",
"length",
"white_space",
"pattern",
"explicit_timezone",
"nillable",
)
for key in keys:
value = getattr(source, key)
if value is not None:
setattr(self, key, value)
min_occurs = source.min_occurs
max_occurs = source.max_occurs
is_list = max_occurs is not None and max_occurs > 1
# Update the sequential flag if new value is true and restrictions indicate
# the field was and still is a list.
if source.sequential and (is_list or not self.is_list):
self.sequential = source.sequential
if not self.tokens and source.tokens:
self.tokens = True
# Update min occurs if current value is None and the new value is more than one.
if self.min_occurs is None or (min_occurs is not None and min_occurs != 1):
self.min_occurs = min_occurs
# Update max occurs if current value is None and the new value is more than one.
if self.max_occurs is None or (max_occurs is not None and max_occurs != 1):
self.max_occurs = max_occurs
[docs] def asdict(self, types: Optional[List[Type]] = None) -> Dict:
"""
Return the initialized only properties as a dictionary.
Optionally convert min/max inclusive/exclusive values by
providing the parent attribute types.
"""
sorted_types = converter.sort_types(types) if types else []
def clean(key: str, value: Any) -> Any:
if types and key.endswith("clusive"):
return converter.from_string(value, sorted_types)
return value
return {k: clean(k, v) for k, v in asdict(self).items() if v is not None}
[docs] def clone(self) -> "Restrictions":
"""Return a deep cloned instance."""
return replace(self)
[docs] @classmethod
def from_element(cls, element: ElementBase) -> "Restrictions":
"""Static constructor from an xsd model."""
return cls(**element.get_restrictions())
[docs]@dataclass(unsafe_hash=True)
class AttrType:
"""
Model representation for the typing information for fields and extensions.
:param qname:
:param index:
:param alias:
:param native:
:param forward:
:param circular:
"""
qname: str
index: int = field(default_factory=int)
alias: Optional[str] = field(default=None)
native: bool = field(default=False)
forward: bool = field(default=False)
circular: bool = field(default=False)
@property
def name(self) -> str:
"""Shortcut for qname local name."""
return text.split_qname(self.qname)[1]
@property
def is_dependency(self) -> bool:
"""Return true if attribute is not a forward/circular references and
it's not a native python time."""
return not (self.forward or self.native or self.circular)
@property
def native_name(self) -> Optional[str]:
"""Return the python build-in type name: `'str'`, `'int'` if it's
native type."""
data_type = DataType.get_enum(self.name) if self.native else None
return data_type.local_name if data_type else None
@property
def native_code(self) -> Optional[str]:
"""Return the xml data type if it's native type."""
data_type = DataType.get_enum(self.name) if self.native else None
return data_type.code if data_type else None
@property
def native_type(self) -> Any:
"""Return the python build-in type if it's a native type."""
data_type = DataType.get_enum(self.name) if self.native else None
return data_type.local if data_type else None
[docs] def clone(self) -> "AttrType":
"""Return a deep cloned instance."""
return replace(self)
[docs]@dataclass
class Attr:
"""
Model representation for a dataclass field.
:param tag:
:param name:
:param local_name:
:param index:
:param default:
:param fixed:
:param mixed:
:param types:
:param namespace:
:param help:
:param restrictions:
"""
tag: str
name: str
local_name: str
index: int = field(compare=False, default_factory=int)
default: Any = field(default=None, compare=False)
fixed: bool = field(default=False, compare=False)
mixed: bool = field(default=False, compare=False)
types: List[AttrType] = field(default_factory=list)
namespace: Optional[str] = field(default=None)
help: Optional[str] = field(default=None, compare=False)
restrictions: Restrictions = field(default_factory=Restrictions, compare=False)
@property
def is_attribute(self) -> bool:
"""Return whether this attribute is derived from an xs:attribute or
xs:anyAttribute."""
return self.tag in (Tag.ATTRIBUTE, Tag.ANY_ATTRIBUTE)
@property
def is_enumeration(self) -> bool:
"""Return whether this attribute is derived from an xs:enumeration."""
return self.tag == Tag.ENUMERATION
@property
def is_factory(self) -> bool:
"""Return whether this attribute is a list of items or a mapping."""
return self.is_list or self.is_dict or self.is_tokens
@property
def is_group(self) -> bool:
"""Return whether this attribute is derived from an xs:group or
xs:attributeGroup."""
return self.tag in (Tag.ATTRIBUTE_GROUP, Tag.GROUP)
@property
def is_dict(self) -> bool:
"""Return whether this attribute is a mapping of values."""
return self.tag == Tag.ANY_ATTRIBUTE
@property
def is_nameless(self) -> bool:
"""Return whether this attribute has a local name that will be used
during parsing/serialization."""
return self.xml_type in (XmlType.WILDCARD, XmlType.ATTRIBUTES, None)
@property
def is_list(self) -> bool:
"""Return whether this attribute is a list of values."""
return self.restrictions.is_list
@property
def is_tokens(self) -> bool:
"""Return whether this attribute is a list of values."""
return self.restrictions.tokens is True
@property
def is_optional(self) -> bool:
"""Return whether this attribute is not required."""
return self.restrictions.is_optional
@property
def is_suffix(self) -> bool:
"""Return whether this attribute is not derived from an xs element with
mode suffix."""
return self.index == sys.maxsize
@property
def is_xsi_type(self) -> bool:
"""Return whether this attribute qualified name is equal to
xsi:type."""
return QNames.XSI_TYPE == text.qname(self.namespace, self.name)
@property
def is_wildcard(self) -> bool:
"""Return whether this attribute is derived from xs:anyAttribute or
xs:any."""
return self.tag in (Tag.ANY_ATTRIBUTE, Tag.ANY)
@property
def xml_type(self) -> Optional[str]:
"""Return the xml node type this attribute is mapped to."""
return xml_type_map.get(self.tag)
[docs] def clone(self) -> "Attr":
"""Return a deep cloned instance."""
return replace(
self,
types=[x.clone() for x in self.types],
restrictions=self.restrictions.clone(),
)
[docs]@dataclass
class Extension:
"""
Model representation of a dataclass base class.
:param type:
:param restrictions:
"""
type: AttrType
restrictions: Restrictions
[docs] def clone(self) -> "Extension":
"""Return a deep cloned instance."""
return replace(
self, type=self.type.clone(), restrictions=self.restrictions.clone()
)
[docs]class Status(IntEnum):
RAW = 0
PROCESSING = 1
PROCESSED = 2
[docs]@dataclass
class Class:
"""
Model representation of a dataclass with fields, base/inner classes and
additional metadata settings.
:param qname:
:param type:
:param module:
:param mixed:
:param abstract:
:param nillable:
:param strict_type:
:param status:
:param container:
:param package:
:param namespace:
:param help:
:param meta_name:
:param substitutions:
:param extensions:
:param attrs:
:param inner:
:param ns_map:
"""
qname: str
type: Type
module: str
mixed: bool = field(default=False)
abstract: bool = field(default=False)
nillable: bool = field(default=False)
strict_type: bool = field(default=False)
status: Status = field(default=Status.RAW)
container: Optional[str] = field(default=None)
package: Optional[str] = field(default=None)
namespace: Optional[str] = field(default=None)
help: Optional[str] = field(default=None)
meta_name: Optional[str] = field(default=None)
substitutions: List[str] = field(default_factory=list)
extensions: List[Extension] = field(default_factory=list)
attrs: List[Attr] = field(default_factory=list)
inner: List["Class"] = field(default_factory=list)
ns_map: Dict = field(default_factory=dict)
@property
def name(self) -> str:
"""Shortcut for qname local name."""
return text.split_qname(self.qname)[1]
@property
def target_namespace(self) -> Optional[str]:
return text.split_qname(self.qname)[0]
@property
def has_suffix_attr(self) -> bool:
"""Return whether or not it includes a suffix attribute."""
return any(attr.is_suffix for attr in self.attrs)
@property
def is_complex(self) -> bool:
"""Return whether or not this instance is derived from an xs:element or
xs:complexType."""
return self.type in (Element, ComplexType)
@property
def is_element(self) -> bool:
"""Return whether or not this instance is derived from an non abstract
xs:element."""
return self.type is Element
@property
def is_enumeration(self) -> bool:
"""Return whether all attributes are derived from xs:enumeration."""
return len(self.attrs) > 0 and all(attr.is_enumeration for attr in self.attrs)
@property
def is_nillable(self) -> bool:
"""Return whether this class represents a nillable xml element."""
return self.nillable or next(
(True for ext in self.extensions if ext.restrictions.nillable), False
)
@property
def is_service(self) -> bool:
"""Return whether or not this instance is derived from
wsdl:operation."""
return self.type is BindingOperation
@property
def should_generate(self) -> bool:
"""Return whether or not this instance should be generated."""
if self.strict_type:
return False
if self.type in (
Element,
ComplexType,
BindingOperation,
BindingMessage,
Message,
):
return True
return self.is_enumeration
@property
def target_module(self) -> str:
"""Return the target module this class is assigned to."""
return f"{self.package}.{self.module}"
[docs] def clone(self) -> "Class":
"""Return a deep cloned instance."""
inners = [inner.clone() for inner in self.inner]
extensions = [extension.clone() for extension in self.extensions]
attrs = [attr.clone() for attr in self.attrs]
return replace(self, inner=inners, extensions=extensions, attrs=attrs)
[docs] def dependencies(self) -> Iterator[str]:
"""
Return a set of dependencies for the given class.
Collect:
* base classes
* attribute types
* recursively go through the inner classes
* Ignore inner class references
* Ignore native types.
"""
seen = set()
for attr in self.attrs:
for attr_type in attr.types:
if attr_type.is_dependency and attr_type.name not in seen:
yield attr_type.qname
seen.add(attr_type.name)
for ext in self.extensions:
if ext.type.is_dependency and ext.type.name not in seen:
yield ext.type.qname
seen.add(ext.type.name)
for inner in self.inner:
yield from inner.dependencies()
[docs]@dataclass
class Import:
"""
Model representation of a python import statement.
:param name:
:param source:
:param alias:
"""
name: str
source: str
alias: Optional[str] = field(default=None)