# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: ruiandrebatista@gmail.com-20120207212159-\
#   04q73ey0rsqw5whb
# target_branch: file:///C:/nvda/main/
# testament_sha1: 1bcb367e70d4b0f824ffd058b7099274fa206611
# timestamp: 2012-02-07 21:23:59 +0000
# base_revision_id: jamie@jantrid.net-20120207045625-blk7m33ficvxv1na
# 
# Begin patch
=== added file 'source/addonHandler.py'
--- source/addonHandler.py	1970-01-01 00:00:00 +0000
+++ source/addonHandler.py	2012-02-07 21:21:59 +0000
@@ -0,0 +1,226 @@
+#addonHandler.py
+#A part of NonVisual Desktop Access (NVDA)
+#Copyright (C) 2012 NVDA Contributors <http://www.nvda-project.org/>
+#This file is covered by the GNU General Public License.
+#See the file COPYING for more details.
+
+import glob
+import itertools
+import os
+import os.path
+import pkgutil
+from StringIO import StringIO
+import zipfile
+
+from configobj import ConfigObj, ConfigObjError
+from validate import Validator
+
+import config
+import globalVars
+from logHandler import log
+
+#: Currently loaded add-ons.
+#: @type runningAddons: string
+runningAddons = []
+
+def initialize():
+	""" Initializes the addons subsystem. """
+	global runningAddons
+	try:
+		runningAddons = list(getAvailableAddons())
+	except:
+		log.exception("error initializing addons subsystem.")
+
+def _getDefaultAddonPaths():
+	""" Returns paths where addons can be found.
+	Paths are constructed from source dir location and user configuration directory.
+	@rtype list(string)
+	"""
+	addon_paths = []
+	source_addons = os.path.join(os.getcwd(), "addons")
+	if os.path.isdir(source_addons):
+		dirs.append(source_addons)
+	user_addons = os.path.join(globalVars.appArgs.configPath, "addons")
+	if os.path.isdir(user_addons):
+		addon_paths.append(user_addons)
+	return addon_paths
+
+def _getAvailableAddonsFromPath(path):
+	""" Gets available add-ons from path.
+	An addon is only considered available if the manifest file is loaded with no errors.
+	@param path: path from where to find addon directories.
+	@type path: string
+	@rtype generator of Addon instances
+	"""
+	log.debug("Listing add-ons from %s", path)
+	for p in os.listdir(path):
+		addon_path = os.path.join(path, p)
+		if os.path.isdir(addon_path) and addon_path not in ('.', '..'):
+			log.debug("Loading add-on from %s", addon_path)
+			try:
+				a = Addon(addon_path)
+				log.debug("Found add-on %s", a.manifest['name'])
+				yield a
+			except:
+				log.exception("Error loading Addon from path: %s", p)
+
+def getAvailableAddons():
+	""" Gets all available addons on the system.
+	@rtype generator of Addon instances.
+	"""
+	generators = [_getAvailableAddonsFromPath(path) for path in _getDefaultAddonPaths()]
+	return itertools.chain(*generators)
+
+
+def installAddonBundle(bundle):
+	pass
+
+
+class AddonError(Exception):
+	""" Represents an exception comming from the addon subsystem. """
+	pass
+
+
+MANIFEST_FILENAME = "manifest.ini"
+
+class Addon(object):
+	""" Represents an Addon available on the file system."""
+	def __init__(self, path):
+		""" Constructs an C[Addon} from.
+		@param path: the base directory for the addon data.
+		@type path: string
+		"""
+		self.path = path
+		manifest_path = os.path.join(path, MANIFEST_FILENAME)
+		with open(manifest_path) as f:
+			self.manifest = AddonManifest(f)
+
+	def addToPackagePath(self, package):
+		""" Adds this C{Addon} extensions to the specific package path if those exist.
+		@param package: the python module representing the package.
+		@type package: python module.
+		"""
+		extension_path = os.path.join(self.path, package.__name__)
+		if not os.path.isdir(extension_path):
+			# This addon does not have extension points for this package
+			return
+		# Python 2.x doesn't properly handle unicode import paths, so convert them before adding.
+		converted_path = extension_path.encode("mbcs")
+		package.__path__.insert(0, converted_path)
+		log.debug("Addon %s added to %s package path", self.manifest['name'], package.__name__)
+
+
+class AddonBundle(object):
+	""" Represents the contents of an NVDA addon in a for suitable for distribution and.
+	The bundle is compressed using the zip file format. Manifest information
+	is available without the need for extraction."""
+	def __init__(self, bundle_filename):
+		""" Constructs a C{AddonBundle} from a filename.
+		@param: bundle_filename the bundle's file path on the file system.
+		"""
+		self._filename = bundle_filename
+		# Read manifest:
+		with zipfile.ZipFile(bundle_filename, 'r') as z:
+			self._manifest = AddonManifest(z.open(MANIFEST_FILENAME))
+
+	def extract(addons_path, override=False):
+		""" Extracts the bundle content to the specified path.
+		A directory with the addon's name will be created under C{addons_path}.
+		@param addons_path: Path where to extract contents.
+		@type addons_path: string
+		@param override: specifies if the contents of the addon-directory are or not overriden
+		@type override: boolean
+		"""
+		name = self._manifest['name']
+		path = os.path.join(addons_path, name)
+		if not override and os.path.isdir(path):
+			raise AddonError, "Addon already installed."
+		with zipfile.ZipFile(self._filename, 'r') as z:
+			z.extractall(path)
+
+	@property
+	def manifest(self):
+		""" Gets the manifest for the represented Addon.
+		@rtype AddonManifest
+		"""
+		return self._manifest
+
+def _report_manifest_errors(manifest):
+	log.warning("Error loading manifest:\n%s", manifest.errors)
+
+def create_addon_bundle_from_manifest(manifest_path, dest_dir='.'):
+	""" Creates the bundle in zip format from a manifest and specified data.
+"""
+	basedir = os.path.dirname(os.path.abspath(manifest_path))
+	with open(manifest_path) as f:
+		manifest = AddonManifest(f)
+	if manifest.errors is not None:
+		_report_manifest_errors(manifest)
+		return False
+	bundle_name = "%s-%s.nda-adon" % (manifest['name'], manifest['version'])
+	with zipfile.PyZipFile(bundle_name, 'w') as z:
+		# write manifest
+		z.write(manifest_path, MANIFEST_FILENAME)
+		# Write python files. Compiling if necessary.
+		z.writepy(basedir)
+		# simple includes
+		includes = itertools.chain(*[glob.glob(path) for path in manifest['data']['include']])
+		for path in includes:
+			z.write(os.path.join(basedir, path), path)
+
+
+class AddonManifest(ConfigObj):
+	""" Add-on manifest file. It contains metadata about an NVDA add-on package. """
+	configspec = ConfigObj(StringIO(
+	"""
+# NVDA Ad-don Manifest configuration specification
+# Add-on name
+name = string()
+# Quick description of the add-on to show to users.
+description = string()
+# Long description with further information and instructions
+long_description = string(default=None)
+# Name of the author or entity that created the add-on
+author = string()
+# Version of the add-on. Should preferably in some standard format such as x.y.z
+version = string()
+# URL for more information about the add-on. New versions and such.
+url= string(default=None)
+# Categories where this add-on belongs to.
+categories = list(default=list())
+
+# Compatibility with NVDA releases
+# Only considering stable versions.
+[compatibility]
+# Minimum version compatible with this add-on.
+minVersion = string(default=None)
+# Maximum compatible version.
+maxVersion = string(default=None)
+
+# Extra data to bundle with the add-on.
+# Python files and packages are bundled by default.
+# Must be relative to the base directory where are this manifest.
+[data]
+# Extra files to include. Can use glob expressions.
+include = list(default=list())
+# Directories to include recursively.
+include_recursive = list(default=list())
+"""))
+
+	def __init__(self, input):
+		""" Constructs an C{AddonManfiest} instance from manifest string data
+		@param input: data to read the manifest informatinon
+		@type input: a fie-like object.
+		"""
+		
+		super(AddonManifest, self).__init__(input, configspec=self.configspec)
+		self._errors = []
+		val = Validator()
+		self._errors = None
+		result = self.validate(val, copy=True, preserve_errors=True)
+		if result != True:
+			self._errors = result
+
+	@property
+	def errors(self):
+		return self._errors

=== modified file 'source/config/__init__.py'
--- source/config/__init__.py	2012-01-05 02:17:19 +0000
+++ source/config/__init__.py	2012-02-07 21:21:59 +0000
@@ -273,7 +273,7 @@
 		configPath=globalVars.appArgs.configPath
 	if not os.path.isdir(configPath):
 		os.makedirs(configPath)
-	for subdir in ("appModules","brailleDisplayDrivers","speechDicts","synthDrivers","globalPlugins"):
+	for subdir in ("addons", "appModules","brailleDisplayDrivers","speechDicts","synthDrivers","globalPlugins"):
 		subdir=os.path.join(configPath,subdir)
 		if not os.path.isdir(subdir):
 			os.makedirs(subdir)
@@ -394,3 +394,7 @@
 	dirs = [dir.encode("mbcs") for dir in getConfigDirs(subdir)]
 	dirs.extend(module.__path__ )
 	module.__path__ = dirs
+	# FIXME: this should not be coupled to the config module....
+	import addonHandler
+	for addon in addonHandler.runningAddons:
+		addon.addToPackagePath(module)

=== modified file 'source/core.py'
--- source/core.py	2012-02-07 04:56:25 +0000
+++ source/core.py	2012-02-07 21:21:59 +0000
@@ -55,6 +55,7 @@
 import logHandler
 import globalVars
 from logHandler import log
+import addonHandler
 
 # Work around an issue with comtypes where __del__ seems to be called twice on COM pointers.
 # This causes Release() to be called more than it should, which is very nasty and will eventually cause us to access pointers which have been freed.
@@ -113,6 +114,8 @@
 	lang = config.conf["general"]["language"]
 	log.debug("setting language to %s"%lang)
 	languageHandler.setLanguage(lang)
+	# Addons
+	addonHandler.initialize()
 	#Speech
 	log.debug("initializing speech")
 	speech.initialize()
@@ -170,6 +173,8 @@
 	import NVDAHelper
 	log.debug("Initializing NVDAHelper")
 	NVDAHelper.initialize()
+	log.debug("Initializing addons system.")
+	addonHandler.initialize()
 	import speechDictHandler
 	log.debug("Speech Dictionary processing")
 	speechDictHandler.initialize()

# Begin bundle
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
UJCwV+TT
