# This program is free software; you can redistribute it and/or modify # it under the terms of the (LGPL) GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library Lesser General Public License for more details at # ( http://www.gnu.org/licenses/lgpl.html ). # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # written by: Jeff Ortel ( jortel@redhat.com ) """ Properties classes. """ class AutoLinker(object): """ Base class, provides interface for I{automatic} link management between a L{Properties} object and the L{Properties} contained within I{values}. """ def updated(self, properties, prev, next): """ Notification that a values was updated and the linkage between the I{properties} contained with I{prev} need to be relinked to the L{Properties} contained within the I{next} value. """ pass class Link(object): """ Property link object. @ivar endpoints: A tuple of the (2) endpoints of the link. @type endpoints: tuple(2) """ def __init__(self, a, b): """ @param a: Property (A) to link. @type a: L{Property} @param b: Property (B) to link. @type b: L{Property} """ pA = Endpoint(self, a) pB = Endpoint(self, b) self.endpoints = (pA, pB) self.validate(a, b) a.links.append(pB) b.links.append(pA) def validate(self, pA, pB): """ Validate that the two properties may be linked. @param pA: Endpoint (A) to link. @type pA: L{Endpoint} @param pB: Endpoint (B) to link. @type pB: L{Endpoint} @return: self @rtype: L{Link} """ if pA in pB.links or \ pB in pA.links: raise Exception('Already linked') dA = pA.domains() dB = pB.domains() for d in dA: if d in dB: raise Exception('Duplicate domain "%s" found' % d) for d in dB: if d in dA: raise Exception('Duplicate domain "%s" found' % d) kA = list(pA.keys()) kB = list(pB.keys()) for k in kA: if k in kB: raise Exception('Duplicate key %s found' % k) for k in kB: if k in kA: raise Exception('Duplicate key %s found' % k) return self def teardown(self): """ Teardown the link. Removes endpoints from properties I{links} collection. @return: self @rtype: L{Link} """ pA, pB = self.endpoints if pA in pB.links: pB.links.remove(pA) if pB in pA.links: pA.links.remove(pB) return self class Endpoint(object): """ Link endpoint (wrapper). @ivar link: The associated link. @type link: L{Link} @ivar target: The properties object. @type target: L{Property} """ def __init__(self, link, target): self.link = link self.target = target def teardown(self): return self.link.teardown() def __eq__(self, rhs): return ( self.target == rhs ) def __hash__(self): return hash(self.target) def __getattr__(self, name): return getattr(self.target, name) class Definition: """ Property definition. @ivar name: The property name. @type name: str @ivar classes: The (class) list of permitted values @type classes: tuple @ivar default: The default value. @ivar type: any """ def __init__(self, name, classes, default, linker=AutoLinker()): """ @param name: The property name. @type name: str @param classes: The (class) list of permitted values @type classes: tuple @param default: The default value. @type default: any """ if not isinstance(classes, (list, tuple)): classes = (classes,) self.name = name self.classes = classes self.default = default self.linker = linker def nvl(self, value=None): """ Convert the I{value} into the default when I{None}. @param value: The proposed value. @type value: any @return: The I{default} when I{value} is I{None}, else I{value}. @rtype: any """ if value is None: return self.default else: return value def validate(self, value): """ Validate the I{value} is of the correct class. @param value: The value to validate. @type value: any @raise AttributeError: When I{value} is invalid. """ if value is None: return if len(self.classes) and \ not isinstance(value, self.classes): msg = '"%s" must be: %s' % (self.name, self.classes) raise AttributeError(msg) def __repr__(self): return '%s: %s' % (self.name, str(self)) def __str__(self): s = [] if len(self.classes): s.append('classes=%s' % str(self.classes)) else: s.append('classes=*') s.append("default=%s" % str(self.default)) return ', '.join(s) class Properties: """ Represents basic application properties. Provides basic type validation, default values and link/synchronization behavior. @ivar domain: The domain name. @type domain: str @ivar definitions: A table of property definitions. @type definitions: {name: L{Definition}} @ivar links: A list of linked property objects used to create a network of properties. @type links: [L{Property},..] @ivar defined: A dict of property values. @type defined: dict """ def __init__(self, domain, definitions, kwargs): """ @param domain: The property domain name. @type domain: str @param definitions: A table of property definitions. @type definitions: {name: L{Definition}} @param kwargs: A list of property name/values to set. @type kwargs: dict """ self.definitions = {} for d in definitions: self.definitions[d.name] = d self.domain = domain self.links = [] self.defined = {} self.modified = set() self.prime() self.update(kwargs) def definition(self, name): """ Get the definition for the property I{name}. @param name: The property I{name} to find the definition for. @type name: str @return: The property definition @rtype: L{Definition} @raise AttributeError: On not found. """ d = self.definitions.get(name) if d is None: raise AttributeError(name) return d def update(self, other): """ Update the property values as specified by keyword/value. @param other: An object to update from. @type other: (dict|L{Properties}) @return: self @rtype: L{Properties} """ if isinstance(other, Properties): other = other.defined for n,v in list(other.items()): self.set(n, v) return self def notset(self, name): """ Get whether a property has never been set by I{name}. @param name: A property name. @type name: str @return: True if never been set. @rtype: bool """ self.provider(name).__notset(name) def set(self, name, value): """ Set the I{value} of a property by I{name}. The value is validated against the definition and set to the default when I{value} is None. @param name: The property name. @type name: str @param value: The new property value. @type value: any @return: self @rtype: L{Properties} """ self.provider(name).__set(name, value) return self def unset(self, name): """ Unset a property by I{name}. @param name: A property name. @type name: str @return: self @rtype: L{Properties} """ self.provider(name).__set(name, None) return self def get(self, name, *df): """ Get the value of a property by I{name}. @param name: The property name. @type name: str @param df: An optional value to be returned when the value is not set @type df: [1]. @return: The stored value, or I{df[0]} if not set. @rtype: any """ return self.provider(name).__get(name, *df) def link(self, other): """ Link (associate) this object with anI{other} properties object to create a network of properties. Links are bidirectional. @param other: The object to link. @type other: L{Properties} @return: self @rtype: L{Properties} """ Link(self, other) return self def unlink(self, *others): """ Unlink (disassociate) the specified properties object. @param others: The list object to unlink. Unspecified means unlink all. @type others: [L{Properties},..] @return: self @rtype: L{Properties} """ if not len(others): others = self.links[:] for p in self.links[:]: if p in others: p.teardown() return self def provider(self, name, history=None): """ Find the provider of the property by I{name}. @param name: The property name. @type name: str @param history: A history of nodes checked to prevent circular hunting. @type history: [L{Properties},..] @return: The provider when found. Otherwise, None (when nested) and I{self} when not nested. @rtype: L{Properties} """ if history is None: history = [] history.append(self) if name in self.definitions: return self for x in self.links: if x in history: continue provider = x.provider(name, history) if provider is not None: return provider history.remove(self) if len(history): return None return self def keys(self, history=None): """ Get the set of I{all} property names. @param history: A history of nodes checked to prevent circular hunting. @type history: [L{Properties},..] @return: A set of property names. @rtype: list """ if history is None: history = [] history.append(self) keys = set() keys.update(list(self.definitions.keys())) for x in self.links: if x in history: continue keys.update(x.keys(history)) history.remove(self) return keys def domains(self, history=None): """ Get the set of I{all} domain names. @param history: A history of nodes checked to prevent circular hunting. @type history: [L{Properties},..] @return: A set of domain names. @rtype: list """ if history is None: history = [] history.append(self) domains = set() domains.add(self.domain) for x in self.links: if x in history: continue domains.update(x.domains(history)) history.remove(self) return domains def prime(self): """ Prime the stored values based on default values found in property definitions. @return: self @rtype: L{Properties} """ for d in list(self.definitions.values()): self.defined[d.name] = d.default return self def __notset(self, name): return not (name in self.modified) def __set(self, name, value): d = self.definition(name) d.validate(value) value = d.nvl(value) prev = self.defined[name] self.defined[name] = value self.modified.add(name) d.linker.updated(self, prev, value) def __get(self, name, *df): d = self.definition(name) value = self.defined.get(name) if value == d.default and len(df): value = df[0] return value def str(self, history): s = [] s.append('Definitions:') for d in list(self.definitions.values()): s.append('\t%s' % repr(d)) s.append('Content:') for d in list(self.defined.items()): s.append('\t%s' % str(d)) if self not in history: history.append(self) s.append('Linked:') for x in self.links: s.append(x.str(history)) history.remove(self) return '\n'.join(s) def __repr__(self): return str(self) def __str__(self): return self.str([]) class Skin(object): """ The meta-programming I{skin} around the L{Properties} object. @ivar __pts__: The wrapped object. @type __pts__: L{Properties}. """ def __init__(self, domain, definitions, kwargs): self.__pts__ = Properties(domain, definitions, kwargs) def __setattr__(self, name, value): builtin = name.startswith('__') and name.endswith('__') if builtin: self.__dict__[name] = value return self.__pts__.set(name, value) def __getattr__(self, name): return self.__pts__.get(name) def __repr__(self): return str(self) def __str__(self): return str(self.__pts__) class Unskin(object): def __new__(self, *args, **kwargs): return args[0].__pts__ class Inspector: """ Wrapper inspector. """ def __init__(self, options): self.properties = options.__pts__ def get(self, name, *df): """ Get the value of a property by I{name}. @param name: The property name. @type name: str @param df: An optional value to be returned when the value is not set @type df: [1]. @return: The stored value, or I{df[0]} if not set. @rtype: any """ return self.properties.get(name, *df) def update(self, **kwargs): """ Update the property values as specified by keyword/value. @param kwargs: A list of property name/values to set. @type kwargs: dict @return: self @rtype: L{Properties} """ return self.properties.update(**kwargs) def link(self, other): """ Link (associate) this object with anI{other} properties object to create a network of properties. Links are bidirectional. @param other: The object to link. @type other: L{Properties} @return: self @rtype: L{Properties} """ p = other.__pts__ return self.properties.link(p) def unlink(self, other): """ Unlink (disassociate) the specified properties object. @param other: The object to unlink. @type other: L{Properties} @return: self @rtype: L{Properties} """ p = other.__pts__ return self.properties.unlink(p)