Packages update
This commit is contained in:
+187
-161
@@ -1,16 +1,16 @@
|
||||
# orm/util.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import sql, util
|
||||
|
||||
from sqlalchemy import sql, util, event, exc as sa_exc
|
||||
from sqlalchemy.sql import expression, util as sql_util, operators
|
||||
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\
|
||||
PropComparator, MapperProperty,\
|
||||
AttributeExtension
|
||||
PropComparator, MapperProperty
|
||||
from sqlalchemy.orm import attributes, exc
|
||||
import operator
|
||||
|
||||
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
|
||||
|
||||
@@ -20,7 +20,7 @@ all_cascades = frozenset(("delete", "delete-orphan", "all", "merge",
|
||||
|
||||
_INSTRUMENTOR = ('mapper', 'instrumentor')
|
||||
|
||||
class CascadeOptions(object):
|
||||
class CascadeOptions(dict):
|
||||
"""Keeps track of the options sent to relationship().cascade"""
|
||||
|
||||
def __init__(self, arg=""):
|
||||
@@ -28,69 +28,63 @@ class CascadeOptions(object):
|
||||
values = set()
|
||||
else:
|
||||
values = set(c.strip() for c in arg.split(','))
|
||||
|
||||
for name in ['save-update', 'delete', 'refresh-expire',
|
||||
'merge', 'expunge']:
|
||||
boolean = name in values or 'all' in values
|
||||
setattr(self, name.replace('-', '_'), boolean)
|
||||
if boolean:
|
||||
self[name] = True
|
||||
self.delete_orphan = "delete-orphan" in values
|
||||
self.delete = "delete" in values or "all" in values
|
||||
self.save_update = "save-update" in values or "all" in values
|
||||
self.merge = "merge" in values or "all" in values
|
||||
self.expunge = "expunge" in values or "all" in values
|
||||
self.refresh_expire = "refresh-expire" in values or "all" in values
|
||||
if self.delete_orphan:
|
||||
self['delete-orphan'] = True
|
||||
|
||||
if self.delete_orphan and not self.delete:
|
||||
util.warn("The 'delete-orphan' cascade option requires "
|
||||
"'delete'. This will raise an error in 0.6.")
|
||||
"'delete'.")
|
||||
|
||||
for x in values:
|
||||
if x not in all_cascades:
|
||||
raise sa_exc.ArgumentError("Invalid cascade option '%s'" % x)
|
||||
|
||||
def __contains__(self, item):
|
||||
return getattr(self, item.replace("-", "_"), False)
|
||||
|
||||
def __repr__(self):
|
||||
return "CascadeOptions(%s)" % repr(",".join(
|
||||
[x for x in ['delete', 'save_update', 'merge', 'expunge',
|
||||
'delete_orphan', 'refresh-expire']
|
||||
if getattr(self, x, False) is True]))
|
||||
|
||||
def _validator_events(desc, key, validator):
|
||||
"""Runs a validation method on an attribute value to be set or appended."""
|
||||
|
||||
class Validator(AttributeExtension):
|
||||
"""Runs a validation method on an attribute value to be set or appended.
|
||||
def append(state, value, initiator):
|
||||
return validator(state.obj(), key, value)
|
||||
|
||||
The Validator class is used by the :func:`~sqlalchemy.orm.validates`
|
||||
decorator, and direct access is usually not needed.
|
||||
def set_(state, value, oldvalue, initiator):
|
||||
return validator(state.obj(), key, value)
|
||||
|
||||
"""
|
||||
event.listen(desc, 'append', append, raw=True, retval=True)
|
||||
event.listen(desc, 'set', set_, raw=True, retval=True)
|
||||
|
||||
def __init__(self, key, validator):
|
||||
"""Construct a new Validator.
|
||||
|
||||
key - name of the attribute to be validated;
|
||||
will be passed as the second argument to
|
||||
the validation method (the first is the object instance itself).
|
||||
|
||||
validator - an function or instance method which accepts
|
||||
three arguments; an instance (usually just 'self' for a method),
|
||||
the key name of the attribute, and the value. The function should
|
||||
return the same value given, unless it wishes to modify it.
|
||||
|
||||
"""
|
||||
self.key = key
|
||||
self.validator = validator
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
return self.validator(state.obj(), self.key, value)
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
return self.validator(state.obj(), self.key, value)
|
||||
|
||||
def polymorphic_union(table_map, typecolname, aliasname='p_union'):
|
||||
def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=True):
|
||||
"""Create a ``UNION`` statement used by a polymorphic mapper.
|
||||
|
||||
See :ref:`concrete_inheritance` for an example of how
|
||||
this is used.
|
||||
|
||||
:param table_map: mapping of polymorphic identities to
|
||||
:class:`.Table` objects.
|
||||
:param typecolname: string name of a "discriminator" column, which will be
|
||||
derived from the query, producing the polymorphic identity for each row. If
|
||||
``None``, no polymorphic discriminator is generated.
|
||||
:param aliasname: name of the :func:`~sqlalchemy.sql.expression.alias()`
|
||||
construct generated.
|
||||
:param cast_nulls: if True, non-existent columns, which are represented as labeled
|
||||
NULLs, will be passed into CAST. This is a legacy behavior that is problematic
|
||||
on some backends such as Oracle - in which case it can be set to False.
|
||||
|
||||
"""
|
||||
|
||||
colnames = set()
|
||||
colnames = util.OrderedSet()
|
||||
colnamemaps = {}
|
||||
types = {}
|
||||
for key in table_map.keys():
|
||||
@@ -113,7 +107,10 @@ def polymorphic_union(table_map, typecolname, aliasname='p_union'):
|
||||
try:
|
||||
return colnamemaps[table][name]
|
||||
except KeyError:
|
||||
return sql.cast(sql.null(), types[name]).label(name)
|
||||
if cast_nulls:
|
||||
return sql.cast(sql.null(), types[name]).label(name)
|
||||
else:
|
||||
return sql.type_coerce(sql.null(), types[name]).label(name)
|
||||
|
||||
result = []
|
||||
for type, table in table_map.iteritems():
|
||||
@@ -184,78 +181,6 @@ def identity_key(*args, **kwargs):
|
||||
mapper = object_mapper(instance)
|
||||
return mapper.identity_key_from_instance(instance)
|
||||
|
||||
class ExtensionCarrier(dict):
|
||||
"""Fronts an ordered collection of MapperExtension objects.
|
||||
|
||||
Bundles multiple MapperExtensions into a unified callable unit,
|
||||
encapsulating ordering, looping and EXT_CONTINUE logic. The
|
||||
ExtensionCarrier implements the MapperExtension interface, e.g.::
|
||||
|
||||
carrier.after_insert(...args...)
|
||||
|
||||
The dictionary interface provides containment for implemented
|
||||
method names mapped to a callable which executes that method
|
||||
for participating extensions.
|
||||
|
||||
"""
|
||||
|
||||
interface = set(method for method in dir(MapperExtension)
|
||||
if not method.startswith('_'))
|
||||
|
||||
def __init__(self, extensions=None):
|
||||
self._extensions = []
|
||||
for ext in extensions or ():
|
||||
self.append(ext)
|
||||
|
||||
def copy(self):
|
||||
return ExtensionCarrier(self._extensions)
|
||||
|
||||
def push(self, extension):
|
||||
"""Insert a MapperExtension at the beginning of the collection."""
|
||||
self._register(extension)
|
||||
self._extensions.insert(0, extension)
|
||||
|
||||
def append(self, extension):
|
||||
"""Append a MapperExtension at the end of the collection."""
|
||||
self._register(extension)
|
||||
self._extensions.append(extension)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over MapperExtensions in the collection."""
|
||||
return iter(self._extensions)
|
||||
|
||||
def _register(self, extension):
|
||||
"""Register callable fronts for overridden interface methods."""
|
||||
|
||||
for method in self.interface.difference(self):
|
||||
impl = getattr(extension, method, None)
|
||||
if impl and impl is not getattr(MapperExtension, method):
|
||||
self[method] = self._create_do(method)
|
||||
|
||||
def _create_do(self, method):
|
||||
"""Return a closure that loops over impls of the named method."""
|
||||
|
||||
def _do(*args, **kwargs):
|
||||
for ext in self._extensions:
|
||||
ret = getattr(ext, method)(*args, **kwargs)
|
||||
if ret is not EXT_CONTINUE:
|
||||
return ret
|
||||
else:
|
||||
return EXT_CONTINUE
|
||||
_do.__name__ = method
|
||||
return _do
|
||||
|
||||
@staticmethod
|
||||
def _pass(*args, **kwargs):
|
||||
return EXT_CONTINUE
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""Delegate MapperExtension methods to bundled fronts."""
|
||||
|
||||
if key not in self.interface:
|
||||
raise AttributeError(key)
|
||||
return self.get(key, self._pass)
|
||||
|
||||
class ORMAdapter(sql_util.ColumnAdapter):
|
||||
"""Extends ColumnAdapter to accept ORM entities.
|
||||
|
||||
@@ -296,15 +221,61 @@ class AliasedClass(object):
|
||||
session.query(User, user_alias).\\
|
||||
join((user_alias, User.id > user_alias.id)).\\
|
||||
filter(User.name==user_alias.name)
|
||||
|
||||
|
||||
The resulting object is an instance of :class:`.AliasedClass`, however
|
||||
it implements a ``__getattribute__()`` scheme which will proxy attribute
|
||||
access to that of the ORM class being aliased. All classmethods
|
||||
on the mapped entity should also be available here, including
|
||||
hybrids created with the :ref:`hybrids_toplevel` extension,
|
||||
which will receive the :class:`.AliasedClass` as the "class" argument
|
||||
when classmethods are called.
|
||||
|
||||
:param cls: ORM mapped entity which will be "wrapped" around an alias.
|
||||
:param alias: a selectable, such as an :func:`.alias` or :func:`.select`
|
||||
construct, which will be rendered in place of the mapped table of the
|
||||
ORM entity. If left as ``None``, an ordinary :class:`.Alias` of the
|
||||
ORM entity's mapped table will be generated.
|
||||
:param name: A name which will be applied both to the :class:`.Alias`
|
||||
if one is generated, as well as the name present in the "named tuple"
|
||||
returned by the :class:`.Query` object when results are returned.
|
||||
:param adapt_on_names: if True, more liberal "matching" will be used when
|
||||
mapping the mapped columns of the ORM entity to those of the given selectable -
|
||||
a name-based match will be performed if the given selectable doesn't
|
||||
otherwise have a column that corresponds to one on the entity. The
|
||||
use case for this is when associating an entity with some derived
|
||||
selectable such as one that uses aggregate functions::
|
||||
|
||||
class UnitPrice(Base):
|
||||
__tablename__ = 'unit_price'
|
||||
...
|
||||
unit_id = Column(Integer)
|
||||
price = Column(Numeric)
|
||||
|
||||
aggregated_unit_price = Session.query(
|
||||
func.sum(UnitPrice.price).label('price')
|
||||
).group_by(UnitPrice.unit_id).subquery()
|
||||
|
||||
aggregated_unit_price = aliased(UnitPrice, alias=aggregated_unit_price, adapt_on_names=True)
|
||||
|
||||
Above, functions on ``aggregated_unit_price`` which
|
||||
refer to ``.price`` will return the
|
||||
``fund.sum(UnitPrice.price).label('price')`` column,
|
||||
as it is matched on the name "price". Ordinarily, the "price" function wouldn't
|
||||
have any "column correspondence" to the actual ``UnitPrice.price`` column
|
||||
as it is not a proxy of the original.
|
||||
|
||||
``adapt_on_names`` is new in 0.7.3.
|
||||
|
||||
"""
|
||||
def __init__(self, cls, alias=None, name=None):
|
||||
def __init__(self, cls, alias=None, name=None, adapt_on_names=False):
|
||||
self.__mapper = _class_to_mapper(cls)
|
||||
self.__target = self.__mapper.class_
|
||||
self.__adapt_on_names = adapt_on_names
|
||||
if alias is None:
|
||||
alias = self.__mapper._with_polymorphic_selectable.alias()
|
||||
alias = self.__mapper._with_polymorphic_selectable.alias(name=name)
|
||||
self.__adapter = sql_util.ClauseAdapter(alias,
|
||||
equivalents=self.__mapper._equivalent_columns)
|
||||
equivalents=self.__mapper._equivalent_columns,
|
||||
adapt_on_names=self.__adapt_on_names)
|
||||
self.__alias = alias
|
||||
# used to assign a name to the RowTuple object
|
||||
# returned by Query.
|
||||
@@ -315,15 +286,18 @@ class AliasedClass(object):
|
||||
return {
|
||||
'mapper':self.__mapper,
|
||||
'alias':self.__alias,
|
||||
'name':self._sa_label_name
|
||||
'name':self._sa_label_name,
|
||||
'adapt_on_names':self.__adapt_on_names,
|
||||
}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__mapper = state['mapper']
|
||||
self.__target = self.__mapper.class_
|
||||
self.__adapt_on_names = state['adapt_on_names']
|
||||
alias = state['alias']
|
||||
self.__adapter = sql_util.ClauseAdapter(alias,
|
||||
equivalents=self.__mapper._equivalent_columns)
|
||||
equivalents=self.__mapper._equivalent_columns,
|
||||
adapt_on_names=self.__adapt_on_names)
|
||||
self.__alias = alias
|
||||
name = state['name']
|
||||
self._sa_label_name = name
|
||||
@@ -336,23 +310,15 @@ class AliasedClass(object):
|
||||
'parentmapper':self.__mapper}
|
||||
)
|
||||
|
||||
def __adapt_prop(self, prop):
|
||||
existing = getattr(self.__target, prop.key)
|
||||
def __adapt_prop(self, existing, key):
|
||||
comparator = existing.comparator.adapted(self.__adapt_element)
|
||||
|
||||
queryattr = attributes.QueryableAttribute(prop.key,
|
||||
queryattr = attributes.QueryableAttribute(self, key,
|
||||
impl=existing.impl, parententity=self, comparator=comparator)
|
||||
setattr(self, prop.key, queryattr)
|
||||
setattr(self, key, queryattr)
|
||||
return queryattr
|
||||
|
||||
def __getattr__(self, key):
|
||||
if self.__mapper.has_property(key):
|
||||
return self.__adapt_prop(
|
||||
self.__mapper.get_property(
|
||||
key, _compile_mappers=False
|
||||
)
|
||||
)
|
||||
|
||||
for base in self.__target.__mro__:
|
||||
try:
|
||||
attr = object.__getattribute__(base, key)
|
||||
@@ -363,14 +329,19 @@ class AliasedClass(object):
|
||||
else:
|
||||
raise AttributeError(key)
|
||||
|
||||
if hasattr(attr, 'func_code'):
|
||||
if isinstance(attr, attributes.QueryableAttribute):
|
||||
return self.__adapt_prop(attr, key)
|
||||
elif hasattr(attr, 'func_code'):
|
||||
is_method = getattr(self.__target, key, None)
|
||||
if is_method and is_method.im_self is not None:
|
||||
return util.types.MethodType(attr.im_func, self, self)
|
||||
else:
|
||||
return None
|
||||
elif hasattr(attr, '__get__'):
|
||||
return attr.__get__(None, self)
|
||||
ret = attr.__get__(None, self)
|
||||
if isinstance(ret, PropComparator):
|
||||
return ret.adapted(self.__adapt_element)
|
||||
return ret
|
||||
else:
|
||||
return attr
|
||||
|
||||
@@ -378,6 +349,14 @@ class AliasedClass(object):
|
||||
return '<AliasedClass at 0x%x; %s>' % (
|
||||
id(self), self.__target.__name__)
|
||||
|
||||
def aliased(element, alias=None, name=None, adapt_on_names=False):
|
||||
if isinstance(element, expression.FromClause):
|
||||
if adapt_on_names:
|
||||
raise sa_exc.ArgumentError("adapt_on_names only applies to ORM elements")
|
||||
return element.alias(name)
|
||||
else:
|
||||
return AliasedClass(element, alias=alias, name=name, adapt_on_names=adapt_on_names)
|
||||
|
||||
def _orm_annotate(element, exclude=None):
|
||||
"""Deep copy the given ClauseElement, annotating each element with the
|
||||
"_orm_adapt" flag.
|
||||
@@ -453,29 +432,52 @@ class _ORMJoin(expression.Join):
|
||||
|
||||
def join(left, right, onclause=None, isouter=False, join_to_left=True):
|
||||
"""Produce an inner join between left and right clauses.
|
||||
|
||||
:func:`.orm.join` is an extension to the core join interface
|
||||
provided by :func:`.sql.expression.join()`, where the
|
||||
left and right selectables may be not only core selectable
|
||||
objects such as :class:`.Table`, but also mapped classes or
|
||||
:class:`.AliasedClass` instances. The "on" clause can
|
||||
be a SQL expression, or an attribute or string name
|
||||
referencing a configured :func:`.relationship`.
|
||||
|
||||
In addition to the interface provided by
|
||||
:func:`~sqlalchemy.sql.expression.join()`, left and right may be mapped
|
||||
classes or AliasedClass instances. The onclause may be a
|
||||
string name of a relationship(), or a class-bound descriptor
|
||||
representing a relationship.
|
||||
|
||||
join_to_left indicates to attempt aliasing the ON clause,
|
||||
``join_to_left`` indicates to attempt aliasing the ON clause,
|
||||
in whatever form it is passed, to the selectable
|
||||
passed as the left side. If False, the onclause
|
||||
is used as is.
|
||||
|
||||
:func:`.orm.join` is not commonly needed in modern usage,
|
||||
as its functionality is encapsulated within that of the
|
||||
:meth:`.Query.join` method, which features a
|
||||
significant amount of automation beyond :func:`.orm.join`
|
||||
by itself. Explicit usage of :func:`.orm.join`
|
||||
with :class:`.Query` involves usage of the
|
||||
:meth:`.Query.select_from` method, as in::
|
||||
|
||||
from sqlalchemy.orm import join
|
||||
session.query(User).\\
|
||||
select_from(join(User, Address, User.addresses)).\\
|
||||
filter(Address.email_address=='foo@bar.com')
|
||||
|
||||
In modern SQLAlchemy the above join can be written more
|
||||
succinctly as::
|
||||
|
||||
session.query(User).\\
|
||||
join(User.addresses).\\
|
||||
filter(Address.email_address=='foo@bar.com')
|
||||
|
||||
See :meth:`.Query.join` for information on modern usage
|
||||
of ORM level joins.
|
||||
|
||||
"""
|
||||
return _ORMJoin(left, right, onclause, isouter, join_to_left)
|
||||
|
||||
def outerjoin(left, right, onclause=None, join_to_left=True):
|
||||
"""Produce a left outer join between left and right clauses.
|
||||
|
||||
In addition to the interface provided by
|
||||
:func:`~sqlalchemy.sql.expression.outerjoin()`, left and right may be
|
||||
mapped classes or AliasedClass instances. The onclause may be a string
|
||||
name of a relationship(), or a class-bound descriptor representing a
|
||||
relationship.
|
||||
This is the "outer join" version of the :func:`.orm.join` function,
|
||||
featuring the same behavior except that an OUTER JOIN is generated.
|
||||
See that function's documentation for other usage details.
|
||||
|
||||
"""
|
||||
return _ORMJoin(left, right, onclause, True, join_to_left)
|
||||
@@ -508,7 +510,7 @@ def with_parent(instance, prop):
|
||||
"""
|
||||
if isinstance(prop, basestring):
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper.get_property(prop, resolve_synonyms=True)
|
||||
prop = getattr(mapper.class_, prop).property
|
||||
elif isinstance(prop, attributes.QueryableAttribute):
|
||||
prop = prop.property
|
||||
|
||||
@@ -544,8 +546,8 @@ def _entity_info(entity, compile=True):
|
||||
else:
|
||||
return None, entity, False
|
||||
|
||||
if compile:
|
||||
mapper = mapper.compile()
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper, mapper._with_polymorphic_selectable, False
|
||||
|
||||
def _entity_descriptor(entity, key):
|
||||
@@ -586,8 +588,7 @@ def _attr_as_key(attr):
|
||||
def _is_aliased_class(entity):
|
||||
return isinstance(entity, AliasedClass)
|
||||
|
||||
def _state_mapper(state):
|
||||
return state.manager.mapper
|
||||
_state_mapper = util.dottedgetter('manager.mapper')
|
||||
|
||||
def object_mapper(instance):
|
||||
"""Given an object, return the primary Mapper associated with the object
|
||||
@@ -605,9 +606,12 @@ def object_mapper(instance):
|
||||
raise exc.UnmappedInstanceError(instance)
|
||||
|
||||
def class_mapper(class_, compile=True):
|
||||
"""Given a class, return the primary Mapper associated with the key.
|
||||
"""Given a class, return the primary :class:`.Mapper` associated
|
||||
with the key.
|
||||
|
||||
Raises UnmappedClassError if no mapping is configured.
|
||||
Raises :class:`.UnmappedClassError` if no mapping is configured
|
||||
on the given class, or :class:`.ArgumentError` if a non-class
|
||||
object is passed.
|
||||
|
||||
"""
|
||||
|
||||
@@ -616,10 +620,12 @@ def class_mapper(class_, compile=True):
|
||||
mapper = class_manager.mapper
|
||||
|
||||
except exc.NO_STATE:
|
||||
if not isinstance(class_, type):
|
||||
raise sa_exc.ArgumentError("Class object expected, got '%r'." % class_)
|
||||
raise exc.UnmappedClassError(class_)
|
||||
|
||||
if compile:
|
||||
mapper = mapper.compile()
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper
|
||||
|
||||
def _class_to_mapper(class_or_mapper, compile=True):
|
||||
@@ -637,16 +643,18 @@ def _class_to_mapper(class_or_mapper, compile=True):
|
||||
else:
|
||||
raise exc.UnmappedClassError(class_or_mapper)
|
||||
|
||||
if compile:
|
||||
return mapper.compile()
|
||||
else:
|
||||
return mapper
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper
|
||||
|
||||
def has_identity(object):
|
||||
state = attributes.instance_state(object)
|
||||
return state.has_identity
|
||||
|
||||
def _is_mapped_class(cls):
|
||||
"""Return True if the given object is a mapped class,
|
||||
:class:`.Mapper`, or :class:`.AliasedClass`."""
|
||||
|
||||
if isinstance(cls, (AliasedClass, mapperlib.Mapper)):
|
||||
return True
|
||||
if isinstance(cls, expression.ClauseElement):
|
||||
@@ -656,6 +664,16 @@ def _is_mapped_class(cls):
|
||||
return manager and _INSTRUMENTOR in manager.info
|
||||
return False
|
||||
|
||||
def _mapper_or_none(cls):
|
||||
"""Return the :class:`.Mapper` for the given class or None if the
|
||||
class is not mapped."""
|
||||
|
||||
manager = attributes.manager_of_class(cls)
|
||||
if manager is not None and _INSTRUMENTOR in manager.info:
|
||||
return manager.info[_INSTRUMENTOR]
|
||||
else:
|
||||
return None
|
||||
|
||||
def instance_str(instance):
|
||||
"""Return a string describing an instance."""
|
||||
|
||||
@@ -669,6 +687,14 @@ def state_str(state):
|
||||
else:
|
||||
return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj()))
|
||||
|
||||
def state_class_str(state):
|
||||
"""Return a string describing an instance's class via its InstanceState."""
|
||||
|
||||
if state is None:
|
||||
return "None"
|
||||
else:
|
||||
return '<%s>' % (state.class_.__name__, )
|
||||
|
||||
def attribute_str(instance, attribute):
|
||||
return instance_str(instance) + "." + attribute
|
||||
|
||||
|
||||
Reference in New Issue
Block a user