Commits

Jason R. Coombs committed df6fb88 Merge

Merging registering-parts branch as it's now passing all tests in openpack and paradocx

Comments (0)

Files changed (3)

openpack/basepack.py

 import logging
 from string import Template
 from UserDict import DictMixin
+from collections import defaultdict
 
 from lxml.etree import Element, ElementTree, fromstring, tostring 
 
-from util import validator, parse_tag, handle, get_ext
+from util import validator, parse_tag, get_ext
 
 log = logging.getLogger(__name__)
 
 	"A mixin class for packages and parts; both support relationships."
 	def relate(self, part, id=None):
 		"""Relate this package component to the supplied part."""
+		assert part.name.startswith(self.base)
 		name = part.name[len(self.base):].lstrip('/')
 		rel = Relationship(self, name, part.rel_type, id=id)
 		self.relationships.add(rel)
 		self[rels.name] = rels
 		self.content_types = ContentTypes()
 		self.content_types.add(ContentType.Default(rels.content_type, 'rels'))
-		self.core_properties = None
 	
 	def __setitem__(self, name, part):
 		self._validate_part(name, part)
 
 	def _load_content_types(self, source):
 		"""Load up the content_types object with value from source XML."""
-		elem = fromstring(source)
-		self.content_types.update(ContentTypes.from_element(elem))
+		self.content_types.update(ContentTypes.load(source))
 
-	def _load_part(self, name, data):
-		"""This is the default loader for unhandled parts.
-
-		Parts can have custom loading logic by defining their own package
-		level method decorated with @handle(relationship_type).  See
-		_load_core_properties in this class for an example.
+	def _load_part(self, rel_type, name, data):
 		"""
-		ct = self.content_types.find_for(name)
-		if ct is None:
+		Load a part into this package based on its relationship type
+		"""
+		if self.content_types.find_for(name) is None:
 			log.warning('no content type found for part %(name)s' % vars())
 			return
-		part = Part(self, name, data=data)
+		cls = Part.classes_by_rel_type[rel_type]
+		part = cls(self, name)
+		part.load(data)
 		self[name] = part
-
-	@handle('http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties')
-	def _load_core_props(self, name, data):
-		self.core_properties = cp = CoreProperties(self, name)
-		cp.element = fromstring(data)
-		self[cp.name] = cp
+		return part
 
 	def __repr__(self):
 		return "Package-%s" % id(self)
 			or part.content_type == content_type
 			)
 
+	@property
+	def core_properties(self):
+		next = lambda item: item.next()
+		return next(self.get_parts_by_class(CoreProperties))
+
 class DefaultNamed(object):
 	"""
 	Mix-in for Parts that have a default name. Subclasses should include
 		name = name or self.default_name
 		super(DefaultNamed, self).__init__(package, name, *args, **kwargs)
 
+class RelationshipTypeHandler(type):
+	"""
+	A metaclass designed to register new Part classes that handle
+	particular relationship types. Whenever a new subclass of Part is
+	created, its rel_type attribute will be mapped to that class.
+	
+	Subsequently, Part.classes_by_rel_type will be a mapping of
+	relationship-type to the appropriate class for that rel-type.
+	"""
+	def __new__(mcs, name, bases, attrs):
+		"""
+		This is called when a new class is created of this type
+		"""
+		# Allow the new class to be created
+		cls = type.__new__(mcs, name, bases, attrs)
+		# if the class (or its parent) doesn't already have a mapping
+		#  of relationship type to class, create one (with this new
+		#  class being the default).
+		if not hasattr(cls, 'classes_by_rel_type'):
+			cls.classes_by_rel_type = defaultdict(lambda: cls)
+		rt = attrs.get('rel_type', None)
+		if rt:
+			cls.classes_by_rel_type[rt] = cls
+		return cls
+
 class Part(Relational):
-	"""Parts are the building blocks of OOXML files.
+	"""
+	Parts are the building blocks of OOXML files.
 
 	All Part subclasses need to define their content-type in a
 	content_type attribute.  Most will also need a relationship-type 
 	(defined in the rel_type attribute).  See the documentation for the
 	part that you are implementing for the proper values for those attributes.
 	"""
+	__metaclass__ = RelationshipTypeHandler
 	content_type = None
 	rel_type = None
 
 			return data.encode('utf-8')
 		return data
 
+	def load(self, data):
+		self.data = data
+
 class Relationship(object):
 	"""Represents an OPC relationship between a Package/Part and another Part.
 
 	def dump(self, encoding='utf-8'):
 		return tostring(self.to_element(), encoding=encoding)
 
+	@classmethod
+	def load(cls, source):
+		elem = fromstring(source)
+		return cls.from_element(elem)
+
 	def to_element(self):
 		elem = Element(self.xmlns + 'Types', nsmap={None:self.xmlns.strip('{}')})
 		elem.extend(ct.to_element() for ct in self)
 	revision = 1
 	created = None
 	modified = None
+	dt_format = '%Y-%m-%dT%H:%M:%SZ'
 
-	def __init__(self, package, name, encoding=None):
+	def __init__(self, package, name):
 		Part.__init__(self, package, name)
-		self.encoding = encoding or 'utf-8'
 
-	def dump(self):
+	def load(self, data):
+		xml = fromstring(data)
+		DC = lambda tag: '{%(dc)s}' % ooxml_namespaces + tag
+		CP = lambda tag: '{%(cp)s}' % ooxml_namespaces + tag
+		DCTERMS = lambda tag: '{%(dcterms)s}' % ooxml_namespaces + tag
+		identity = lambda x: x
+		def set_attr_if_tag(tag, attr=None, transform=identity):
+			if attr is None:
+				ns, attr = parse_tag(tag)
+			elem = xml.find(tag)
+			if elem and elem.text:
+				value = transform(elem.text)
+				setattr(self, attr, value)
+		map(set_attr_if_tag, (
+			DC('title'),
+			DC('subject'),
+			DC('creator'),
+			CP('keywords'),
+			DC('description'),
+			))
+		set_attr_if_tag(CP('revision'), transform=int)
+		set_attr_if_tag(CP('lastModifiedBy'), 'last_modified_by')
+		def parse_datetime(str):
+			try:
+				result = datetime.datetime.strptime(str, self.dt_format)
+			except ValueError :
+				result = str
+			return result
+		set_attr_if_tag(DCTERMS('created'), transform=parse_datetime)
+		set_attr_if_tag(DCTERMS('modified'), transform=parse_datetime)
+
+	def dump(self, encoding='utf-8'):
+		return tostring(self.to_element(), encoding=encoding)
+
+	def to_element(self):
 		# some datetime handling
 		now = datetime.datetime.now()
 		if self.created is None:
 			self.created = now
 		if self.modified is None:
 			self.modified = now
-		created_str = self.created.strftime('%Y-%m-%dT%H:%M:%SZ')
+		created_str = self.created.strftime(self.dt_format)
 		created = E.dcterms.created(created_str)
 		created.set('{%(xsi)s}type'%ooxml_namespaces, 'dcterms:W3CDTF')
-		modified_str = self.modified.strftime('%Y-%m-%dT%H:%M:%SZ')
+		modified_str = self.modified.strftime(self.dt_format)
 		modified = E.dcterms.modified(modified_str)
 		modified.set('{%(xsi)s}type'%ooxml_namespaces, 'dcterms:W3CDTF')
-		
 		# create the element
 		element = E.cp.coreProperties(
 			E.dc.title(self.title),
 			created,
 			modified,
 		)
-		return tostring(element, encoding=self.encoding)
+		return element
 
+	element = property(to_element)
 def parse_tag(t):
 	return _nstag.match(t).groups()
 
-_handlers = {}
-
-def handle(url):
-	def _handle(f):
-		_handlers[url] = f
-		return f
-	return _handle
-
-def get_handler(url, default):
-	return _handlers.get(url, default)
-
 def get_ext(name):
 	"""
 	Return the extension only for a name (like a filename)

openpack/zippack.py

 except ImportError: from StringIO import StringIO
 
 from basepack import Package, Part, Relationship, Relationships
-from util import get_handler
 
 def to_zip_name(name):
 	"""
 					continue
 				target_path = to_zip_name(pname)
 				data = "".join(self._get_matching_segments(zf, target_path))
-				# get a handler for the relationship type or use a default
-				add_part = get_handler(rel.type, ZipPackage._load_part)
-				add_part(self, pname, data)
-				ropen(self[pname])
+				new_part = self._load_part(rel.type, pname, data)
+				if new_part: ropen(new_part)
 		ropen(self)
 		zf.close()