6bfdb33cc9
Previously, `str()` was used to convert things into strings. This will not work for unicode messages in Python 2, so everything was converted to using `six.text_type` instead.
163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
# Copyright 2013 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Provides the base report model
|
|
|
|
This module defines a class representing the basic report
|
|
data model from which all data models should inherit (or
|
|
at least implement similar functionality). Data models
|
|
store unserialized data generated by generators during
|
|
the report serialization process.
|
|
"""
|
|
|
|
import collections as col
|
|
import copy
|
|
|
|
import six
|
|
|
|
|
|
class ReportModel(col.MutableMapping):
|
|
"""A Report Data Model
|
|
|
|
A report data model contains data generated by some
|
|
generator method or class. Data may be read or written
|
|
using dictionary-style access, and may be read (but not
|
|
written) using object-member-style access. Additionally,
|
|
a data model may have an associated view. This view is
|
|
used to serialize the model when str() is called on the
|
|
model. An appropriate object for a view is callable with
|
|
a single parameter: the model to be serialized.
|
|
|
|
If present, the object passed in as data will be transformed
|
|
into a standard python dict. For mappings, this is fairly
|
|
straightforward. For sequences, the indices become keys
|
|
and the items become values.
|
|
|
|
:param data: a sequence or mapping of data to associate with the model
|
|
:param attached_view: a view object to attach to this model
|
|
"""
|
|
|
|
def __init__(self, data=None, attached_view=None):
|
|
self.attached_view = attached_view
|
|
|
|
if data is not None:
|
|
if isinstance(data, col.Mapping):
|
|
self.data = dict(data)
|
|
elif isinstance(data, col.Sequence):
|
|
# convert a list [a, b, c] to a dict {0: a, 1: b, 2: c}
|
|
self.data = dict(enumerate(data))
|
|
else:
|
|
raise TypeError('Data for the model must be a sequence '
|
|
'or mapping.')
|
|
else:
|
|
self.data = {}
|
|
|
|
def __str__(self):
|
|
self_cpy = copy.deepcopy(self)
|
|
for key in self_cpy:
|
|
if getattr(self_cpy[key], 'attached_view', None) is not None:
|
|
self_cpy[key] = six.text_type(self_cpy[key])
|
|
|
|
if self.attached_view is not None:
|
|
return self.attached_view(self_cpy)
|
|
else:
|
|
raise Exception("Cannot stringify model: no attached view")
|
|
|
|
def __repr__(self):
|
|
if self.attached_view is not None:
|
|
return ("<Model {cl.__module__}.{cl.__name__} {dt}"
|
|
" with view {vw.__module__}."
|
|
"{vw.__name__}>").format(cl=type(self),
|
|
dt=self.data,
|
|
vw=type(self.attached_view))
|
|
else:
|
|
return ("<Model {cl.__module__}.{cl.__name__} {dt}"
|
|
" with no view>").format(cl=type(self),
|
|
dt=self.data)
|
|
|
|
def __getitem__(self, attrname):
|
|
return self.data[attrname]
|
|
|
|
def __setitem__(self, attrname, attrval):
|
|
self.data[attrname] = attrval
|
|
|
|
def __delitem__(self, attrname):
|
|
del self.data[attrname]
|
|
|
|
def __contains__(self, key):
|
|
return self.data.__contains__(key)
|
|
|
|
def __getattr__(self, attrname):
|
|
# Needed for deepcopy in Python3. That will avoid an infinite loop
|
|
# in __getattr__ .
|
|
if 'data' not in self.__dict__:
|
|
self.data = {}
|
|
|
|
try:
|
|
return self.data[attrname]
|
|
except KeyError:
|
|
# we don't have that key in data, and the
|
|
# model class doesn't have that attribute
|
|
raise AttributeError(
|
|
"'{cl}' object has no attribute '{an}'".format(
|
|
cl=type(self).__name__, an=attrname
|
|
)
|
|
)
|
|
|
|
def __len__(self):
|
|
return len(self.data)
|
|
|
|
def __iter__(self):
|
|
return self.data.__iter__()
|
|
|
|
def set_current_view_type(self, tp, visited=None):
|
|
"""Set the current view type
|
|
|
|
This method attempts to set the current view
|
|
type for this model and all submodels by calling
|
|
itself recursively on all values, traversing
|
|
intervening sequences and mappings when possible,
|
|
and ignoring all other objects.
|
|
|
|
:param tp: the type of the view ('text', 'json', 'xml', etc)
|
|
:param visited: a set of object ids for which the corresponding objects
|
|
have already had their view type set
|
|
"""
|
|
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
def traverse_obj(obj):
|
|
oid = id(obj)
|
|
|
|
# don't die on recursive structures,
|
|
# and don't treat strings like sequences
|
|
if oid in visited or isinstance(obj, six.string_types):
|
|
return
|
|
|
|
visited.add(oid)
|
|
|
|
if hasattr(obj, 'set_current_view_type'):
|
|
obj.set_current_view_type(tp, visited=visited)
|
|
|
|
if isinstance(obj, col.Sequence):
|
|
for item in obj:
|
|
traverse_obj(item)
|
|
|
|
elif isinstance(obj, col.Mapping):
|
|
for val in six.itervalues(obj):
|
|
traverse_obj(val)
|
|
|
|
traverse_obj(self)
|