summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSVN-Git Migration <python-modules-team@lists.alioth.debian.org>2015-10-08 12:22:04 -0700
committerSVN-Git Migration <python-modules-team@lists.alioth.debian.org>2015-10-08 12:22:04 -0700
commit7d1cd972ba81f86e9161d7708278678a4b8d5f68 (patch)
tree30e34783d56f7d5f34d4db5b44c69665c11bb5c9 /src
parent095b6a3014c89083dcb6bdee66a0699a2791a6a8 (diff)
Imported Upstream version 1.9.11
Diffstat (limited to 'src')
-rw-r--r--src/launchpadlib.egg-info/PKG-INFO24
-rw-r--r--src/launchpadlib.egg-info/SOURCES.txt214
-rw-r--r--src/launchpadlib.egg-info/requires.txt1
-rw-r--r--src/launchpadlib/NEWS.txt22
-rw-r--r--src/launchpadlib/__init__.py4
-rw-r--r--src/launchpadlib/credentials.py28
-rw-r--r--src/launchpadlib/launchpad.py7
-rw-r--r--src/launchpadlib/testing/launchpad.py537
-rw-r--r--src/launchpadlib/testing/resources.py55
-rw-r--r--src/launchpadlib/testing/tests/__init__.py0
-rw-r--r--src/launchpadlib/testing/tests/test_launchpad.py393
-rw-r--r--src/launchpadlib/tests/test_credential_store.py18
-rw-r--r--src/launchpadlib/tests/test_launchpad.py8
13 files changed, 1296 insertions, 15 deletions
diff --git a/src/launchpadlib.egg-info/PKG-INFO b/src/launchpadlib.egg-info/PKG-INFO
index 5e33416..a3aed35 100644
--- a/src/launchpadlib.egg-info/PKG-INFO
+++ b/src/launchpadlib.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.0
Name: launchpadlib
-Version: 1.9.9
+Version: 1.9.11
Summary: Script Launchpad through its web services interfaces. Officially supported.
Home-page: https://help.launchpad.net/API/launchpadlib
Author: LAZR Developers
@@ -31,7 +31,29 @@ Description: ..
NEWS for launchpadlib
=====================
+ 1.9.11 (2011-11-21)
+ ===================
+ - 1.9.10 was a bad release due to incomplete NEWS entries.
+
+ - Add fake Launchpad web service for unit test.
+
+ - Improve HACKING documentation.
+
+ - Imporove launchpadlib directory discovery on Windows.
+
+ - Added script to delete spurious bugtasks or split a bugtask from a bug.
+
+ - Properly handle Unicode passwords if returned by the keyring.
+
+ - Base 64 encode serialized credentials before putting in keyring/wallet.
+
+
+ 1.9.10 (2011-11-21)
+ ===================
+ - Base 64 encode serialized credentials before putting in keyring/wallet.
+
1.9.9 (2011-07-27)
+ ==================
- Fix a failing test for lazr.restfulclient 0.12.0.
diff --git a/src/launchpadlib.egg-info/SOURCES.txt b/src/launchpadlib.egg-info/SOURCES.txt
index 7869872..4d60a3a 100644
--- a/src/launchpadlib.egg-info/SOURCES.txt
+++ b/src/launchpadlib.egg-info/SOURCES.txt
@@ -3,6 +3,216 @@ HACKING.txt
README.txt
ez_setup.py
setup.py
+eggs/distribute-0.6.24-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/distribute-0.6.24-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/distribute-0.6.24-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/distribute-0.6.24-py2.6.egg/EGG-INFO/top_level.txt
+eggs/docutils-0.8.1-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/docutils-0.8.1-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/docutils-0.8.1-py2.6.egg/EGG-INFO/top_level.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/README.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamsa.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamsb.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamsc.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamsn.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamso.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isoamsr.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isobox.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isocyr1.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isocyr2.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isodia.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isogrk1.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isogrk2.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isogrk3.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isogrk4-wide.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isogrk4.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isolat1.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isolat2.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomfrk-wide.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomfrk.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomopf-wide.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomopf.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomscr-wide.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isomscr.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isonum.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isopub.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/isotech.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/mmlalias.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/mmlextra-wide.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/mmlextra.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/s5defs.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/xhtml1-lat1.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/xhtml1-special.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/parsers/rst/include/xhtml1-symbol.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/writers/html4css1/template.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/writers/pep_html/template.txt
+eggs/docutils-0.8.1-py2.6.egg/docutils/writers/s5_html/themes/README.txt
+eggs/elementtree-1.2.7_20070827_preview-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/elementtree-1.2.7_20070827_preview-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/elementtree-1.2.7_20070827_preview-py2.6.egg/EGG-INFO/top_level.txt
+eggs/httplib2-0.7.2-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/httplib2-0.7.2-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/httplib2-0.7.2-py2.6.egg/EGG-INFO/top_level.txt
+eggs/httplib2-0.7.2-py2.6.egg/httplib2/cacerts.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/EGG-INFO/requires.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/EGG-INFO/top_level.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/lazr/authentication/NEWS.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/lazr/authentication/README.txt
+eggs/lazr.authentication-0.1.2-py2.6.egg/lazr/authentication/version.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/EGG-INFO/requires.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/EGG-INFO/top_level.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/NEWS.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/README.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/version.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/authorizer.standalone.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/caching.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/collections.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/entries.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/hosted-files.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/operations.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/retry.standalone.txt
+eggs/lazr.restfulclient-0.12.0-py2.6.egg/lazr/restfulclient/docs/toplevel.txt
+eggs/simplejson-2.2.1-py2.6-linux-i686.egg/EGG-INFO/SOURCES.txt
+eggs/simplejson-2.2.1-py2.6-linux-i686.egg/EGG-INFO/dependency_links.txt
+eggs/simplejson-2.2.1-py2.6-linux-i686.egg/EGG-INFO/native_libs.txt
+eggs/simplejson-2.2.1-py2.6-linux-i686.egg/EGG-INFO/top_level.txt
+eggs/testresources-0.2.4-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/testresources-0.2.4-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/testresources-0.2.4-py2.6.egg/EGG-INFO/top_level.txt
+eggs/wsgi_intercept-0.5.1-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/wsgi_intercept-0.5.1-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/wsgi_intercept-0.5.1-py2.6.egg/EGG-INFO/top_level.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/requires.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/EGG-INFO/top_level.txt
+eggs/z3c.recipe.scripts-1.0.1-py2.6.egg/z3c/recipe/scripts/README.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/requires.txt
+eggs/z3c.recipe.tag-0.4.0-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/README.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/requires.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/allowhosts.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/bootstrap.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/buildout.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/debugging.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/dependencylinks.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/distribute.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/download.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/downloadcache.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/easy_install.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/extends-cache.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/repeatable.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/runsetup.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/setup.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/testing.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/testing_bugfix.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/unzip.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/update.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/upgrading_distribute.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/virtualenv.txt
+eggs/zc.buildout-1.5.2-py2.6.egg/zc/buildout/windows.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/requires.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/zc/recipe/egg/README.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/zc/recipe/egg/api.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/zc/recipe/egg/custom.txt
+eggs/zc.recipe.egg-1.3.2-py2.6.egg/zc/recipe/egg/selecting-python.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/requires.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/zc/recipe/testrunner/README.txt
+eggs/zc.recipe.testrunner-1.4.0-py2.6.egg/zc/recipe/testrunner/bugfixes.txt
+eggs/zope.exceptions-3.6.1-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/zope.exceptions-3.6.1-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/zope.exceptions-3.6.1-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/zope.exceptions-3.6.1-py2.6.egg/EGG-INFO/requires.txt
+eggs/zope.exceptions-3.6.1-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/SOURCES.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/dependency_links.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/namespace_packages.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/native_libs.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/requires.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/EGG-INFO/top_level.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/README.ru.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/README.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/adapter.ru.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/adapter.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/human.ru.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/human.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/index.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/verify.txt
+eggs/zope.interface-3.8.0-py2.6-linux-i686.egg/zope/interface/tests/foodforthought.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/SOURCES.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/dependency_links.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/entry_points.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/namespace_packages.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/requires.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/EGG-INFO/top_level.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-arguments.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-colors.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-coverage-win32.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-coverage.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-debugging.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-discovery.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-edge-cases.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-errors.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-gc.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-knit.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-layers-api.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-layers-buff.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-layers-ntd.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-layers.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-leaks-err.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-leaks.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-profiling-cprofiler.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-profiling.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-progress.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-repeat.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-shuffle.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-simple.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-subunit-err.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-subunit-leaks.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-subunit.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-tb-format.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-test-selection.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-unexpected-success.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-verbose.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-wo-source.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/README.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sampletests.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sampletestsl.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample2/e.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample3/post_mortem5.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample3/post_mortem6.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample3/post_mortem_failure.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample3/set_trace5.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/sample3/set_trace6.txt
+eggs/zope.testrunner-4.0.4-py2.6.egg/zope/testrunner/testrunner-ex/usecompiled/README.txt
src/launchpadlib/NEWS.txt
src/launchpadlib/README.txt
src/launchpadlib/__init__.py
@@ -26,6 +236,10 @@ src/launchpadlib/docs/toplevel.txt
src/launchpadlib/docs/files/mugshot.png
src/launchpadlib/testing/__init__.py
src/launchpadlib/testing/helpers.py
+src/launchpadlib/testing/launchpad.py
+src/launchpadlib/testing/resources.py
+src/launchpadlib/testing/tests/__init__.py
+src/launchpadlib/testing/tests/test_launchpad.py
src/launchpadlib/tests/__init__.py
src/launchpadlib/tests/test_credential_store.py
src/launchpadlib/tests/test_http.py
diff --git a/src/launchpadlib.egg-info/requires.txt b/src/launchpadlib.egg-info/requires.txt
index 1ae4fb5..312b7e8 100644
--- a/src/launchpadlib.egg-info/requires.txt
+++ b/src/launchpadlib.egg-info/requires.txt
@@ -5,4 +5,5 @@ lazr.uri
oauth
setuptools
simplejson
+testresources
wadllib \ No newline at end of file
diff --git a/src/launchpadlib/NEWS.txt b/src/launchpadlib/NEWS.txt
index 39ff26d..89d307b 100644
--- a/src/launchpadlib/NEWS.txt
+++ b/src/launchpadlib/NEWS.txt
@@ -2,7 +2,29 @@
NEWS for launchpadlib
=====================
+1.9.11 (2011-11-21)
+===================
+- 1.9.10 was a bad release due to incomplete NEWS entries.
+
+- Add fake Launchpad web service for unit test.
+
+- Improve HACKING documentation.
+
+- Imporove launchpadlib directory discovery on Windows.
+
+- Added script to delete spurious bugtasks or split a bugtask from a bug.
+
+- Properly handle Unicode passwords if returned by the keyring.
+
+- Base 64 encode serialized credentials before putting in keyring/wallet.
+
+
+1.9.10 (2011-11-21)
+===================
+- Base 64 encode serialized credentials before putting in keyring/wallet.
+
1.9.9 (2011-07-27)
+==================
- Fix a failing test for lazr.restfulclient 0.12.0.
diff --git a/src/launchpadlib/__init__.py b/src/launchpadlib/__init__.py
index 0833395..9c9f97d 100644
--- a/src/launchpadlib/__init__.py
+++ b/src/launchpadlib/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2008 Canonical Ltd.
+# Copyright 2008-2011 Canonical Ltd.
# This file is part of launchpadlib.
#
@@ -14,4 +14,4 @@
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
-__version__ = '1.9.9'
+__version__ = '1.9.11'
diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py
index dded027..3e3ac0b 100644
--- a/src/launchpadlib/credentials.py
+++ b/src/launchpadlib/credentials.py
@@ -36,6 +36,11 @@ import time
from urllib import urlencode
from urlparse import urljoin
import webbrowser
+import ConfigParser
+from base64 import (
+ b64decode,
+ b64encode,
+ )
import simplejson
@@ -69,6 +74,8 @@ class Credentials(OAuthAuthorizer):
URI_TOKEN_FORMAT = "uri"
DICT_TOKEN_FORMAT = "dict"
+ ITEM_SEPARATOR = '<BR>'
+ NEWLINE = '\n'
def serialize(self):
"""Turn this object into a string.
@@ -77,7 +84,12 @@ class Credentials(OAuthAuthorizer):
"""
sio = StringIO()
self.save(sio)
- return sio.getvalue()
+ serialized = sio.getvalue()
+ # Some users have reported problems with corrupted keyrings, both in
+ # Gnome and KDE, when newlines are included in the password. Avoid
+ # this problem by base 64 encoding the serialized value.
+ serialized = b64encode(serialized)
+ return serialized
@classmethod
def from_string(cls, value):
@@ -86,6 +98,8 @@ class Credentials(OAuthAuthorizer):
This should probably be moved into OAuthAuthorizer.
"""
credentials = cls()
+ if 'consumer_key' not in value:
+ value = b64decode(value)
credentials.load(StringIO(value))
return credentials
@@ -121,7 +135,7 @@ class Credentials(OAuthAuthorizer):
oauth_signature_method='PLAINTEXT',
oauth_signature='&')
url = web_root + request_token_page
- headers = {'Referer' : web_root}
+ headers = {'Referer': web_root}
if token_format == self.DICT_TOKEN_FORMAT:
headers['Accept'] = 'application/json'
response, content = httplib2.Http().request(
@@ -310,8 +324,9 @@ class KeyringCredentialStore(CredentialStore):
def do_save(self, credentials, unique_key):
"""Store newly-authorized credentials in the keyring."""
self._ensure_keyring_imported()
+ serialized = credentials.serialize()
keyring.set_password(
- 'launchpadlib', unique_key, credentials.serialize())
+ 'launchpadlib', unique_key, serialized)
def do_load(self, unique_key):
"""Retrieve credentials from the keyring."""
@@ -319,7 +334,12 @@ class KeyringCredentialStore(CredentialStore):
credential_string = keyring.get_password(
'launchpadlib', unique_key)
if credential_string is not None:
- return Credentials.from_string(credential_string)
+ credential_string = credential_string.encode('utf8')
+ try:
+ credentials = Credentials.from_string(credential_string)
+ return credentials
+ except ConfigParser.NoOptionError:
+ return None
return None
diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py
index e8e4a80..e42d4fc 100644
--- a/src/launchpadlib/launchpad.py
+++ b/src/launchpadlib/launchpad.py
@@ -590,9 +590,11 @@ class Launchpad(ServiceRoot):
(service_root_uri, launchpadlib_dir, cache_dir, service_root_dir)
"""
if launchpadlib_dir is None:
- home_dir = os.environ['HOME']
- launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
+ launchpadlib_dir = os.path.join('~', '.launchpadlib')
launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
+ if launchpadlib_dir[:1] == '~':
+ raise ValueError("Must set $HOME or pass 'launchpadlib_dir' to "
+ "indicate location to store cached data")
if not os.path.exists(launchpadlib_dir):
os.makedirs(launchpadlib_dir, 0700)
os.chmod(launchpadlib_dir, 0700)
@@ -606,4 +608,3 @@ class Launchpad(ServiceRoot):
if not os.path.exists(cache_path):
os.makedirs(cache_path, 0700)
return (service_root, launchpadlib_dir, cache_path, service_root_dir)
-
diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py
new file mode 100644
index 0000000..0da882b
--- /dev/null
+++ b/src/launchpadlib/testing/launchpad.py
@@ -0,0 +1,537 @@
+# Copyright 2008 Canonical Ltd.
+
+# This file is part of launchpadlib.
+#
+# launchpadlib is free software: you can redistribute it and/or modify
+# it under the terms of the 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.
+#
+# launchpadlib 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with launchpadlib. If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Testing API allows fake data to be used in unit tests.
+
+Testing launchpadlib code is tricky, because it depends so heavily on a
+remote, unique webservice: Launchpad. This module helps you write tests for
+your launchpadlib application that can be run locally and quickly.
+
+Say you were writing some code that needed to call out to Launchpad and get
+the branches owned by the logged-in person, and then do something to them. For
+example, something like this::
+
+ def collect_unique_names(lp):
+ names = []
+ for branch in lp.me.getBranches():
+ names.append(branch.unique_name)
+ return names
+
+To test it, you would first prepare a L{FakeLaunchpad} object, and give it
+some sample data of your own devising::
+
+ lp = FakeLaunchpad()
+ my_branches = [dict(unique_name='~foo/bar/baz')]
+ lp.me = dict(getBranches: lambda status: my_branches)
+
+Then, in the test, call your own code and assert that it behaves correctly
+given the data.
+
+ names = collect_unique_names(lp)
+ self.assertEqual(['~foo/bar/baz'], names)
+
+And that's it.
+
+The L{FakeLaunchpad} code uses a WADL file to type-check any objects created
+or returned. This means you can be sure that you won't accidentally store
+sample data with misspelled attribute names.
+
+The WADL file that we use by default is for version 1.0 of the Launchpad API.
+If you want to work against a more recent version of the API, download the
+WADL yourself (see <https://help.launchpad.net/API/Hacking>) and construct
+your C{FakeLaunchpad} like this::
+
+ from wadllib.application import Application
+ lp = FakeLaunchpad(
+ Application('https://api.launchpad.net/devel/',
+ '/path/to/wadl.xml'))
+
+Where 'https://api.launchpad.net/devel/' is the URL for the WADL file, found
+also in the WADL file itelf.
+"""
+
+from datetime import datetime
+
+
+JSON_MEDIA_TYPE = "application/json"
+
+
+class IntegrityError(Exception):
+ """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
+
+
+class FakeLaunchpad(object):
+ """A fake Launchpad API class for unit tests that depend on L{Launchpad}.
+
+ @param application: A C{wadllib.application.Application} instance for a
+ Launchpad WADL definition file.
+ """
+
+ def __init__(self, credentials=None, service_root=None, cache=None,
+ timeout=None, proxy_info=None, application=None):
+ if application is None:
+ from launchpadlib.testing.resources import get_application
+ application = get_application()
+ root_resource = FakeRoot(application)
+ self.__dict__.update({"credentials": credentials,
+ "_application": application,
+ "_service_root": root_resource})
+
+ def __setattr__(self, name, values):
+ """Set sample data.
+
+ @param name: The name of the attribute.
+ @param values: A dict representing an object matching a resource
+ defined in Launchpad's WADL definition.
+ """
+ service_root = self._service_root
+ setattr(service_root, name, values)
+
+ def __getattr__(self, name):
+ """Get sample data.
+
+ @param name: The name of the attribute.
+ """
+ return getattr(self._service_root, name)
+
+ @classmethod
+ def login(cls, consumer_name, token_string, access_secret,
+ service_root=None, cache=None, timeout=None, proxy_info=None):
+ """Convenience for setting up access credentials."""
+ from launchpadlib.testing.resources import get_application
+ return cls(object(), application=get_application())
+
+ @classmethod
+ def get_token_and_login(cls, consumer_name, service_root=None,
+ cache=None, timeout=None, proxy_info=None):
+ """Get credentials from Launchpad and log into the service root."""
+ from launchpadlib.testing.resources import get_application
+ return cls(object(), application=get_application())
+
+ @classmethod
+ def login_with(cls, consumer_name, service_root=None,
+ launchpadlib_dir=None, timeout=None, proxy_info=None):
+ """Log in to Launchpad with possibly cached credentials."""
+ from launchpadlib.testing.resources import get_application
+ return cls(object(), application=get_application())
+
+
+def find_by_attribute(element, name, value):
+ """Find children of 'element' where attribute 'name' is equal to 'value'.
+ """
+ return [child for child in element if child.get(name) == value]
+
+
+def strip_suffix(string, suffix):
+ if string.endswith(suffix):
+ return string[:-len(suffix)]
+ return string
+
+
+class FakeResource(object):
+ """
+ Represents valid sample data on L{FakeLaunchpad} instances.
+
+ @ivar _children: A dictionary of child resources, each of type
+ C{FakeResource}.
+ @ivar _values: A dictionary of values associated with this resource. e.g.
+ "display_name" or "date_created". The values of this dictionary will
+ never be C{FakeResource}s.
+
+ Note that if C{_children} has a key, then C{_values} will not, and vice
+ versa. That is, they are distinct dicts.
+ """
+
+ special_methods = ["lp_save"]
+
+ def __init__(self, application, resource_type, values=None):
+ """Construct a FakeResource.
+
+ @param application: A C{waddlib.application.Application} instance.
+ @param resource_type: A C{wadllib.application.ResourceType} instance
+ for this resource.
+ @param values: Optionally, a dict representing attribute key/value
+ pairs for this resource.
+ """
+ if values is None:
+ values = {}
+ self.__dict__.update({"_application": application,
+ "_resource_type": resource_type,
+ "_children": {},
+ "_values": values})
+
+ def __setattr__(self, name, value):
+ """Set sample data.
+
+ C{value} can be a dict representing an object matching a resource
+ defined in the WADL definition. Alternatively, C{value} could be a
+ resource itself. Either way, it is checked for type correctness
+ against the WADL definition.
+ """
+ if isinstance(value, dict):
+ self._children[name] = self._create_child_resource(name, value)
+ else:
+ values = {}
+ values.update(self._values)
+ values[name] = value
+ # Confirm that the new 'values' dict is a partial type match for
+ # this resource.
+ self._check_resource_type(self._resource_type, values)
+ self.__dict__["_values"] = values
+
+ def __getattr__(self, name, _marker=object()):
+ """Get sample data.
+
+ @param name: The name of the attribute.
+ """
+ result = self._children.get(name, _marker)
+ if result is _marker:
+ result = self._values.get(name, _marker)
+ if callable(result):
+ return self._wrap_method(name, result)
+ if name in self.special_methods:
+ return lambda: True
+ if result is _marker:
+ raise AttributeError("%r has no attribute '%s'" % (self, name))
+ return result
+
+ def _wrap_method(self, name, method):
+ """Wrapper around methods validates results when it's run.
+
+ @param name: The name of the method.
+ @param method: The callable to run when the method is called.
+ """
+ def wrapper(*args, **kwargs):
+ return self._run_method(name, method, *args, **kwargs)
+ return wrapper
+
+ def _create_child_resource(self, name, values):
+ """
+ Ensure that C{values} is a valid object for the C{name} attribute and
+ return a resource object to represent it as API data.
+
+ @param name: The name of the attribute to check the C{values} object
+ against.
+ @param values: A dict with key/value pairs representing attributes and
+ methods of an object matching the C{name} resource's definition.
+ @return: A L{FakeEntry} for an ordinary resource or a
+ L{FakeCollection} for a resource that represents a collection.
+ @raises IntegrityError: Raised if C{name} isn't a valid attribute for
+ this resource or if C{values} isn't a valid object for the C{name}
+ attribute.
+ """
+ root_resource = self._application.get_resource_by_path("")
+ is_link = False
+ param = root_resource.get_parameter(name + "_collection_link",
+ JSON_MEDIA_TYPE)
+ if param is None:
+ is_link = True
+ param = root_resource.get_parameter(name + "_link", JSON_MEDIA_TYPE)
+ if param is None:
+ raise IntegrityError("%s isn't a valid property." % (name,))
+ resource_type = self._get_resource_type(param)
+ if is_link:
+ self._check_resource_type(resource_type, values)
+ return FakeEntry(self._application, resource_type, values)
+ else:
+ name, child_resource_type = (
+ self._check_collection_type(resource_type, values))
+ return FakeCollection(self._application, resource_type, values,
+ name, child_resource_type)
+
+ def _get_resource_type(self, param):
+ """Get the resource type for C{param}.
+
+ @param param: An object representing a C{_link} or C{_collection_link}
+ parameter.
+ @return: The resource type for the parameter, or None if one isn't
+ available.
+ """
+ [link] = list(param.tag)
+ name = link.get("resource_type")
+ return self._application.get_resource_type(name)
+
+ def _check_resource_type(self, resource_type, partial_object):
+ """
+ Ensure that attributes and methods defined for C{partial_object} match
+ attributes and methods defined for C{resource_type}.
+
+ @param resource_type: The resource type to check the attributes and
+ methods against.
+ @param partial_object: A dict with key/value pairs representing
+ attributes and methods.
+ """
+ for name, value in partial_object.iteritems():
+ if callable(value):
+ # Performs an integrity check.
+ self._get_method(resource_type, name)
+ else:
+ self._check_attribute(resource_type, name, value)
+
+ def _check_collection_type(self, resource_type, partial_object):
+ """
+ Ensure that attributes and methods defined for C{partial_object} match
+ attributes and methods defined for C{resource_type}. Collection
+ entries are treated specially.
+
+ @param resource_type: The resource type to check the attributes and
+ methods against.
+ @param partial_object: A dict with key/value pairs representing
+ attributes and methods.
+ @return: (name, resource_type), where 'name' is the name of the child
+ resource type and 'resource_type' is the corresponding resource
+ type.
+ """
+ name = None
+ child_resource_type = None
+ for name, value in partial_object.iteritems():
+ if name == "entries":
+ name, child_resource_type = (
+ self._check_entries(resource_type, value))
+ elif callable(value):
+ # Performs an integrity check.
+ self._get_method(resource_type, name)
+ else:
+ self._check_attribute(resource_type, name, value)
+ return name, child_resource_type
+
+ def _find_representation_id(self, resource_type, name):
+ """Find the WADL XML id for the representation of C{resource_type}.
+
+ Looks in the WADL for the first representiation associated with the
+ method for a resource type.
+
+ :return: An XML id (a string).
+ """
+ get_method = self._get_method(resource_type, name)
+ for response in get_method:
+ for representation in response:
+ representation_url = representation.get("href")
+ if representation_url is not None:
+ return self._application.lookup_xml_id(representation_url)
+
+ def _check_attribute(self, resource_type, name, value):
+ """
+ Ensure that C{value} is a valid C{name} attribute on C{resource_type}.
+
+ Does this by finding the representation for the default, canonical GET
+ method (as opposed to the many "named" GET methods that exist.)
+
+ @param resource_type: The resource type to check the attribute
+ against.
+ @param name: The name of the attribute.
+ @param value: The value to check.
+ """
+ xml_id = self._find_representation_id(resource_type, 'get')
+ self._check_attribute_representation(xml_id, name, value)
+
+ def _check_attribute_representation(self, xml_id, name, value):
+ """
+ Ensure that C{value} is a valid value for C{name} with the
+ representation definition matching C{xml_id}.
+
+ @param xml_id: The XML ID for the representation to check the
+ attribute against.
+ @param name: The name of the attribute.
+ @param value: The value to check.
+ @raises IntegrityError: Raised if C{name} is not a valid attribute
+ name or if C{value}'s type is not valid for the attribute.
+ """
+ representation = self._application.representation_definitions[xml_id]
+ parameters = dict((child.get("name"), child)
+ for child in representation.tag)
+ if name not in parameters:
+ raise IntegrityError("%s not found" % name)
+ parameter = parameters[name]
+ data_type = parameter.get("type")
+ if data_type is None:
+ if not isinstance(value, basestring):
+ raise IntegrityError(
+ "%s is not a str or unicode for %s" % (value, name))
+ elif data_type == "xsd:dateTime":
+ if not isinstance(value, datetime):
+ raise IntegrityError(
+ "%s is not a datetime for %s" % (value, name))
+
+ def _get_method(self, resource_type, name):
+ """Get the C{name} method on C{resource_type}.
+
+ @param resource_type: The method's resource type.
+ @param name: The name of the method.
+ @raises IntegrityError: Raised if a method called C{name} is not
+ available on C{resource_type}.
+ @return: The XML element for the method from the WADL.
+ """
+ if name in self.special_methods:
+ return
+ resource_name = resource_type.tag.get("id")
+ xml_id = "%s-%s" % (resource_name, name)
+ try:
+ [get_method] = find_by_attribute(resource_type.tag, 'id', xml_id)
+ except ValueError:
+ raise IntegrityError(
+ "%s is not a method of %s" % (name, resource_name))
+ return get_method
+
+ def _run_method(self, name, method, *args, **kwargs):
+ """Run a method and convert its result into a L{FakeResource}.
+
+ If the result represents an object it is validated against the WADL
+ definition before being returned.
+
+ @param name: The name of the method.
+ @param method: A callable.
+ @param args: Arguments to pass to the callable.
+ @param kwargs: Keyword arguments to pass to the callable.
+ @return: A L{FakeResource} representing the result if it's an object.
+ @raises IntegrityError: Raised if the return value from the method
+ isn't valid.
+ """
+ result = method(*args, **kwargs)
+ if name in self.special_methods:
+ return result
+ else:
+ return self._create_resource(self._resource_type, name, result)
+
+ def _create_resource(self, resource_type, name, result):
+ """Create a new L{FakeResource} for C{resource_type} method call result.
+
+ @param resource_type: The resource type of the method.
+ @param name: The name of the method on C{resource_type}.
+ @param result: The result of calling the method.
+ @raises IntegrityError: Raised if C{result} is an invalid return value
+ for the method.
+ @return: A L{FakeResource} for C{result}.
+ """
+ resource_name = resource_type.tag.get("id")
+ if resource_name == name:
+ name = "get"
+ xml_id = self._find_representation_id(resource_type, name)
+ xml_id = strip_suffix(xml_id, '-full')
+ if xml_id not in self._application.resource_types:
+ xml_id += '-resource'
+ result_resource_type = self._application.resource_types[xml_id]
+ self._check_resource_type(result_resource_type, result)
+ # XXX: Should this wrap in collection?
+ return FakeResource(self._application, result_resource_type, result)
+
+ def _get_child_resource_type(self, resource_type):
+ """Get the name and resource type for the entries in a collection.
+
+ @param resource_type: The resource type for a collection.
+ @return: (name, resource_type), where 'name' is the name of the child
+ resource type and 'resource_type' is the corresponding resource
+ type.
+ """
+ xml_id = self._find_representation_id(resource_type, 'get')
+ representation_definition = (
+ self._application.representation_definitions[xml_id])
+
+ [entry_links] = find_by_attribute(
+ representation_definition.tag, 'name', 'entry_links')
+ [resource_type] = list(entry_links)
+ resource_type_url = resource_type.get("resource_type")
+ resource_type_name = resource_type_url.split("#")[1]
+ return (
+ resource_type_name,
+ self._application.get_resource_type(resource_type_url))
+
+ def _check_entries(self, resource_type, entries):
+ """Ensure that C{entries} are valid for a C{resource_type} collection.
+
+ @param resource_type: The resource type of the collection the entries
+ are in.
+ @param entries: A list of dicts representing objects in the
+ collection.
+ @return: (name, resource_type), where 'name' is the name of the child
+ resource type and 'resource_type' is the corresponding resource
+ type.
+ """
+ name, child_resource_type = self._get_child_resource_type(resource_type)
+ for entry in entries:
+ self._check_resource_type(child_resource_type, entry)
+ return name, child_resource_type
+
+ def __repr__(self):
+ """
+ The resource type, identifier if available, and memory address are
+ used to generate a representation of this fake resource.
+ """
+ name = self._resource_type.tag.get("id")
+ key = "object"
+ key = self._values.get("id", key)
+ key = self._values.get("name", key)
+ return "<%s %s %s at %s>" % (
+ self.__class__.__name__, name, key, hex(id(self)))
+
+
+class FakeRoot(FakeResource):
+ """Fake root object for an application."""
+
+ def __init__(self, application):
+ """Create a L{FakeResource} for the service root of C{application}.
+
+ @param application: A C{wadllib.application.Application} instance.
+ """
+ resource_type = application.get_resource_type(
+ application.markup_url + "#service-root")
+ super(FakeRoot, self).__init__(application, resource_type)
+
+
+class FakeEntry(FakeResource):
+ """A fake resource for an entry."""
+
+
+class FakeCollection(FakeResource):
+ """A fake resource for a collection."""
+
+ def __init__(self, application, resource_type, values=None,
+ name=None, child_resource_type=None):
+ super(FakeCollection, self).__init__(application, resource_type, values)
+ self.__dict__.update({"_name": name,
+ "_child_resource_type": child_resource_type})
+
+ def __iter__(self):
+ """Iterate items if this resource has an C{entries} attribute."""
+ entries = self._values.get("entries", ())
+ for entry in entries:
+ yield self._create_resource(self._child_resource_type, self._name,
+ entry)
+
+ def __getitem__(self, key):
+ """Look up a slice, or a subordinate resource by index.
+
+ @param key: An individual object key or a C{slice}.
+ @raises IndexError: Raised if an invalid key is provided.
+ @return: A L{FakeResource} instance for the entry matching C{key}.
+ """
+ entries = list(self)
+ if isinstance(key, slice):
+ start = key.start or 0
+ stop = key.stop
+ if start < 0:
+ raise ValueError("Collection slices must have a nonnegative "
+ "start point.")
+ if stop < 0:
+ raise ValueError("Collection slices must have a definite, "
+ "nonnegative end point.")
+ return entries.__getitem__(key)
+ elif isinstance(key, int):
+ return entries.__getitem__(key)
+ else:
+ raise IndexError("Do not support index lookups yet.")
diff --git a/src/launchpadlib/testing/resources.py b/src/launchpadlib/testing/resources.py
new file mode 100644
index 0000000..b0b396e
--- /dev/null
+++ b/src/launchpadlib/testing/resources.py
@@ -0,0 +1,55 @@
+# Copyright 2008, 2011 Canonical Ltd.
+
+# This file is part of launchpadlib.
+#
+# launchpadlib is free software: you can redistribute it and/or modify
+# it under the terms of the 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.
+#
+# launchpadlib 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with launchpadlib. If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Resources for use in unit tests with the C{testresources} module."""
+
+from pkg_resources import resource_string
+
+from testresources import TestResource
+
+from wadllib.application import Application
+
+from launchpadlib.testing.launchpad import FakeLaunchpad
+
+
+launchpad_testing_application = None
+
+
+def get_application():
+ """Get or create a WADL application for testing Launchpad.
+
+ Note that this uses the Launchpad v1.0 WADL bundled with launchpadlib for
+ testing purposes. For your own application, you might want to construct
+ an L{Application} object directly, giving it your own WADL.
+ """
+ global launchpad_testing_application
+ if launchpad_testing_application is None:
+ markup_url = "https://api.launchpad.net/1.0/"
+ markup = resource_string("launchpadlib.testing",
+ "launchpad-wadl.xml")
+ launchpad_testing_application = Application(markup_url, markup)
+ return launchpad_testing_application
+
+
+class FakeLaunchpadResource(TestResource):
+
+ def make(self, dependency_resources):
+ return FakeLaunchpad(
+ application=Application(
+ "https://api.example.com/testing/",
+ resource_string("launchpadlib.testing", "testing-wadl.xml")))
diff --git a/src/launchpadlib/testing/tests/__init__.py b/src/launchpadlib/testing/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/launchpadlib/testing/tests/__init__.py
diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py
new file mode 100644
index 0000000..1b6591c
--- /dev/null
+++ b/src/launchpadlib/testing/tests/test_launchpad.py
@@ -0,0 +1,393 @@
+# Copyright 2008 Canonical Ltd.
+
+# This file is part of launchpadlib.
+#
+# launchpadlib is free software: you can redistribute it and/or modify
+# it under the terms of the 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.
+#
+# launchpadlib 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with launchpadlib. If not, see
+# <http://www.gnu.org/licenses/>.
+
+from datetime import datetime
+
+from testresources import ResourcedTestCase
+
+from launchpadlib.testing.launchpad import (
+ FakeLaunchpad,
+ FakeResource,
+ FakeRoot,
+ IntegrityError,
+ )
+from launchpadlib.testing.resources import (
+ FakeLaunchpadResource, get_application)
+
+
+class FakeRootTest(ResourcedTestCase):
+
+ def test_create_root_resource(self):
+ root_resource = FakeRoot(get_application())
+ self.assertTrue(isinstance(root_resource, FakeResource))
+
+
+class FakeResourceTest(ResourcedTestCase):
+
+ resources = [("launchpad", FakeLaunchpadResource())]
+
+ def test_repr(self):
+ """A custom C{__repr__} is provided for L{FakeResource}s."""
+ branches = dict(total_size="test-branch")
+ self.launchpad.me = dict(getBranches=lambda statuses: branches)
+ branches = self.launchpad.me.getBranches([])
+ obj_id = hex(id(branches))
+ self.assertEqual(
+ "<FakeResource branch-page-resource object at %s>" % obj_id,
+ repr(branches))
+
+ def test_repr_with_name(self):
+ """
+ If the fake has a C{name} property it's included in the repr string to
+ make it easier to figure out what it is.
+ """
+ self.launchpad.me = dict(name="foo")
+ person = self.launchpad.me
+ self.assertEqual("<FakeEntry person foo at %s>" % hex(id(person)),
+ repr(person))
+
+ def test_repr_with_id(self):
+ """
+ If the fake has an C{id} property it's included in the repr string to
+ make it easier to figure out what it is.
+ """
+ bug = dict(id="1", title="Bug #1")
+ self.launchpad.bugs = dict(entries=[bug])
+ [bug] = list(self.launchpad.bugs)
+ self.assertEqual("<FakeResource bug 1 at %s>" % hex(id(bug)), repr(bug))
+
+
+class FakeLaunchpadTest(ResourcedTestCase):
+
+ resources = [("launchpad", FakeLaunchpadResource())]
+
+ def test_wb_instantiate_without_application(self):
+ """
+ The builtin WADL definition is used if the C{application} is not
+ provided during instantiation.
+ """
+ credentials = object()
+ launchpad = FakeLaunchpad(credentials)
+ self.assertEqual(credentials, launchpad.credentials)
+ self.assertEqual(get_application(), launchpad._application)
+
+ def test_instantiate_with_everything(self):
+ """
+ L{FakeLaunchpad} takes the same parameters as L{Launchpad} during
+ instantiation, with the addition of an C{application} parameter. The
+ optional parameters are discarded when the object is instantiated.
+ """
+ credentials = object()
+ launchpad = FakeLaunchpad(credentials, service_root=None, cache=None,
+ timeout=None, proxy_info=None,
+ application=get_application())
+ self.assertEqual(credentials, launchpad.credentials)
+
+ def test_instantiate_with_credentials(self):
+ """A L{FakeLaunchpad} can be instantiated with credentials."""
+ credentials = object()
+ launchpad = FakeLaunchpad(credentials, application=get_application())
+ self.assertEqual(credentials, launchpad.credentials)
+
+ def test_instantiate_without_credentials(self):
+ """
+ A L{FakeLaunchpad} instantiated without credentials has its
+ C{credentials} attribute set to C{None}.
+ """
+ self.assertEqual(None, self.launchpad.credentials)
+
+ def test_set_undefined_property(self):
+ """
+ An L{IntegrityError} is raised if an attribute is set on a
+ L{FakeLaunchpad} instance that isn't present in the WADL definition.
+ """
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "foo", "bar")
+
+ def test_get_undefined_resource(self):
+ """
+ An L{AttributeError} is raised if an attribute is accessed on a
+ L{FakeLaunchpad} instance that doesn't exist.
+ """
+ self.launchpad.me = dict(display_name="Foo")
+ self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
+
+ def test_string_property(self):
+ """
+ Sample data can be created by setting L{FakeLaunchpad} attributes with
+ dicts that represent objects. Plain string values can be represented
+ as C{str} values.
+ """
+ self.launchpad.me = dict(name="foo")
+ self.assertEqual("foo", self.launchpad.me.name)
+
+ def test_unicode_property(self):
+ """
+ Sample data can be created by setting L{FakeLaunchpad} attributes with
+ dicts that represent objects. Plain string values can be represented
+ as C{unicode} strings.
+ """
+ self.launchpad.me = dict(name=u"foo")
+ self.assertEqual(u"foo", self.launchpad.me.name)
+
+ def test_datetime_property(self):
+ """
+ Attributes that represent dates are set with C{datetime} instances.
+ """
+ now = datetime.utcnow()
+ self.launchpad.me = dict(date_created=now)
+ self.assertEqual(now, self.launchpad.me.date_created)
+
+ def test_invalid_datetime_property(self):
+ """
+ Only C{datetime} values can be set on L{FakeLaunchpad} instances for
+ attributes that represent dates.
+ """
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
+ dict(date_created="now"))
+
+ def test_multiple_string_properties(self):
+ """
+ Sample data can be created by setting L{FakeLaunchpad} attributes with
+ dicts that represent objects.
+ """
+ self.launchpad.me = dict(name="foo", display_name="Foo")
+ self.assertEqual("foo", self.launchpad.me.name)
+ self.assertEqual("Foo", self.launchpad.me.display_name)
+
+ def test_invalid_property_name(self):
+ """
+ Sample data set on a L{FakeLaunchpad} instance is validated against
+ the WADL definition. If a key is defined on a resource that doesn't
+ match a related parameter, an L{IntegrityError} is raised.
+ """
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
+ dict(foo="bar"))
+
+ def test_invalid_property_value(self):
+ """
+ The types of sample data values set on L{FakeLaunchpad} instances are
+ validated against types defined in the WADL definition.
+ """
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
+ dict(name=102))
+
+ def test_callable(self):
+ """
+ A callable set on a L{FakeLaunchpad} instance is validated against the
+ WADL definition, to make sure a matching method exists.
+ """
+ branches = dict(total_size="test-branch")
+ self.launchpad.me = dict(getBranches=lambda statuses: branches)
+ self.assertNotEqual(None, self.launchpad.me.getBranches([]))
+
+ def test_invalid_callable_name(self):
+ """
+ An L{IntegrityError} is raised if a method is defined on a resource
+ that doesn't match a method defined in the WADL definition.
+ """
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
+ dict(foo=lambda: None))
+
+ def test_callable_object_return_type(self):
+ """
+ The result of a fake method is a L{FakeResource}, automatically
+ created from the object used to define the return object.
+ """
+ branches = dict(total_size="8")
+ self.launchpad.me = dict(getBranches=lambda statuses: branches)
+ branches = self.launchpad.me.getBranches([])
+ self.assertTrue(isinstance(branches, FakeResource))
+ self.assertEqual("8", branches.total_size)
+
+ def test_invalid_callable_object_return_type(self):
+ """
+ An L{IntegrityError} is raised if a method returns an invalid result.
+ """
+ branches = dict(total_size=8)
+ self.launchpad.me = dict(getBranches=lambda statuses: branches)
+ self.assertRaises(IntegrityError, self.launchpad.me.getBranches, [])
+
+ def test_collection_property(self):
+ """
+ Sample collections can be set on L{FakeLaunchpad} instances. They are
+ validated the same way other sample data is validated.
+ """
+ branch = dict(name="foo")
+ self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
+ branch = self.launchpad.branches.getByUniqueName("foo")
+ self.assertEqual("foo", branch.name)
+
+ def test_iterate_collection(self):
+ """
+ Data for a sample collection set on a L{FakeLaunchpad} instance can be
+ iterated over if an C{entries} key is defined.
+ """
+ bug = dict(id="1", title="Bug #1")
+ self.launchpad.bugs = dict(entries=[bug])
+ bugs = [bug for bug in self.launchpad.bugs]
+ self.assertEqual(1, len(bugs))
+ bug = bugs[0]
+ self.assertEqual("1", bug.id)
+ self.assertEqual("Bug #1", bug.title)
+
+ def test_collection_with_invalid_entries(self):
+ """
+ Sample data for each entry in a collection is validated when it's set
+ on a L{FakeLaunchpad} instance.
+ """
+ bug = dict(foo="bar")
+ self.assertRaises(IntegrityError, setattr, self.launchpad, "bugs",
+ dict(entries=[bug]))
+
+ def test_slice_collection(self):
+ """
+ Data for a sample collection set on a L{FakeLaunchpad} instance can be
+ sliced if an C{entries} key is defined.
+ """
+ bug1 = dict(id="1", title="Bug #1")
+ bug2 = dict(id="2", title="Bug #2")
+ bug3 = dict(id="3", title="Bug #3")
+ self.launchpad.bugs = dict(entries=[bug1, bug2, bug3])
+ bugs = self.launchpad.bugs[1:3]
+ self.assertEqual(2, len(bugs))
+ self.assertEqual("2", bugs[0].id)
+ self.assertEqual("3", bugs[1].id)
+
+ def test_slice_collection_with_negative_start(self):
+ """
+ A C{ValueError} is raised if a negative start value is used when
+ slicing a sample collection set on a L{FakeLaunchpad} instance.
+ """
+ bug1 = dict(id="1", title="Bug #1")
+ bug2 = dict(id="2", title="Bug #2")
+ self.launchpad.bugs = dict(entries=[bug1, bug2])
+ self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:])
+ self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:2])
+
+ def test_slice_collection_with_negative_stop(self):
+ """
+ A C{ValueError} is raised if a negative stop value is used when
+ slicing a sample collection set on a L{FakeLaunchpad} instance.
+ """
+ bug1 = dict(id="1", title="Bug #1")
+ bug2 = dict(id="2", title="Bug #2")
+ self.launchpad.bugs = dict(entries=[bug1, bug2])
+ self.assertRaises(ValueError, lambda: self.launchpad.bugs[:-1])
+ self.assertRaises(ValueError, lambda: self.launchpad.bugs[0:-1])
+
+ def test_subscript_operator_out_of_range(self):
+ """
+ An C{IndexError} is raised if an invalid index is used when retrieving
+ data from a sample collection.
+ """
+ bug1 = dict(id="1", title="Bug #1")
+ self.launchpad.bugs = dict(entries=[bug1])
+ self.assertRaises(IndexError, lambda: self.launchpad.bugs[2])
+
+ def test_replace_property(self):
+ """Values already set on fake resource objects can be replaced."""
+ self.launchpad.me = dict(name="foo")
+ person = self.launchpad.me
+ self.assertEqual("foo", person.name)
+ person.name = "bar"
+ self.assertEqual("bar", person.name)
+ self.assertEqual("bar", self.launchpad.me.name)
+
+ def test_replace_method(self):
+ """Methods already set on fake resource objects can be replaced."""
+ branch1 = dict(name="foo", bzr_identity="lp:~user/project/branch1")
+ branch2 = dict(name="foo", bzr_identity="lp:~user/project/branch2")
+ self.launchpad.branches = dict(getByUniqueName=lambda name: branch1)
+ self.launchpad.branches.getByUniqueName = lambda name: branch2
+ branch = self.launchpad.branches.getByUniqueName("foo")
+ self.assertEqual("lp:~user/project/branch2", branch.bzr_identity)
+
+ def test_replace_property_with_invalid_value(self):
+ """Values set on fake resource objects are validated."""
+ self.launchpad.me = dict(name="foo")
+ person = self.launchpad.me
+ self.assertRaises(IntegrityError, setattr, person, "name", 1)
+
+ def test_replace_resource(self):
+ """Resources already set on L{FakeLaunchpad} can be replaced."""
+ self.launchpad.me = dict(name="foo")
+ self.assertEqual("foo", self.launchpad.me.name)
+ self.launchpad.me = dict(name="bar")
+ self.assertEqual("bar", self.launchpad.me.name)
+
+ def test_add_property(self):
+ """Sample data set on a L{FakeLaunchpad} instance can be added to."""
+ self.launchpad.me = dict(name="foo")
+ person = self.launchpad.me
+ person.display_name = "Foo"
+ self.assertEqual("foo", person.name)
+ self.assertEqual("Foo", person.display_name)
+ self.assertEqual("foo", self.launchpad.me.name)
+ self.assertEqual("Foo", self.launchpad.me.display_name)
+
+ def test_add_property_to_empty_object(self):
+ """An empty object can be used when creating sample data."""
+ self.launchpad.me = dict()
+ self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
+ self.launchpad.me.name = "foo"
+ self.assertEqual("foo", self.launchpad.me.name)
+
+ def test_login(self):
+ """
+ L{FakeLaunchpad.login} ignores all parameters and returns a new
+ instance using the builtin WADL definition.
+ """
+ launchpad = FakeLaunchpad.login("name", "token", "secret")
+ self.assertTrue(isinstance(launchpad, FakeLaunchpad))
+
+ def test_get_token_and_login(self):
+ """
+ L{FakeLaunchpad.get_token_and_login} ignores all parameters and
+ returns a new instance using the builtin WADL definition.
+ """
+ launchpad = FakeLaunchpad.get_token_and_login("name")
+ self.assertTrue(isinstance(launchpad, FakeLaunchpad))
+
+ def test_login_with(self):
+ """
+ L{FakeLaunchpad.login_with} ignores all parameters and returns a new
+ instance using the builtin WADL definition.
+ """
+ launchpad = FakeLaunchpad.login_with("name")
+ self.assertTrue(isinstance(launchpad, FakeLaunchpad))
+
+ def test_lp_save(self):
+ """
+ Sample object have an C{lp_save} method that is a no-op by default.
+ """
+ self.launchpad.me = dict(name="foo")
+ self.assertTrue(self.launchpad.me.lp_save())
+
+ def test_custom_lp_save(self):
+ """A custom C{lp_save} method can be set on a L{FakeResource}."""
+ self.launchpad.me = dict(name="foo", lp_save=lambda: "custom")
+ self.assertEqual("custom", self.launchpad.me.lp_save())
+
+ def test_set_custom_lp_save(self):
+ """
+ A custom C{lp_save} method can be set on a L{FakeResource} after its
+ been created.
+ """
+ self.launchpad.me = dict(name="foo")
+ self.launchpad.me.lp_save = lambda: "custom"
+ self.assertEqual("custom", self.launchpad.me.lp_save())
diff --git a/src/launchpadlib/tests/test_credential_store.py b/src/launchpadlib/tests/test_credential_store.py
index 3df61b3..772101e 100644
--- a/src/launchpadlib/tests/test_credential_store.py
+++ b/src/launchpadlib/tests/test_credential_store.py
@@ -136,3 +136,21 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
with fake_keyring(self.keyring):
self.assertEquals(None, self.store.load("no such key"))
+
+ def test_keyring_returns_unicode(self):
+ # Kwallet is reported to sometimes return Unicode, which broke the
+ # credentials parsing. This test ensures a Unicode password is
+ # handled correctly. (See bug lp:877374)
+ class UnicodeInMemoryKeyring(InMemoryKeyring):
+ def get_password(self, service, username):
+ return unicode(
+ super(UnicodeInMemoryKeyring, self).get_password(
+ service, username))
+
+ self.keyring = UnicodeInMemoryKeyring()
+ with fake_keyring(self.keyring):
+ credential = self.make_credential("consumer key")
+ self.store.save(credential, "unique key")
+ credential2 = self.store.load("unique key")
+ self.assertEquals(
+ credential.consumer.key, credential2.consumer.key)
diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py
index 3d4ed02..ba3e4c3 100644
--- a/src/launchpadlib/tests/test_launchpad.py
+++ b/src/launchpadlib/tests/test_launchpad.py
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.
+# Copyright 2009, 2011 Canonical Ltd.
# This file is part of launchpadlib.
#
@@ -48,7 +48,6 @@ from launchpadlib.testing.helpers import (
)
from launchpadlib.credentials import (
KeyringCredentialStore,
- UnencryptedFileCredentialStore,
)
# A dummy service root for use in tests
@@ -580,8 +579,7 @@ class TestDeprecatedLoginMethods(KeyringTest):
# login() works but triggers a deprecation warning.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
- launchpad = NoNetworkLaunchpad.login(
- 'consumer', 'token', 'secret')
+ NoNetworkLaunchpad.login('consumer', 'token', 'secret')
self.assertEquals(len(caught), 1)
self.assertEquals(caught[0].category, DeprecationWarning)
@@ -589,7 +587,7 @@ class TestDeprecatedLoginMethods(KeyringTest):
# get_token_and_login() works but triggers a deprecation warning.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
- launchpad = NoNetworkLaunchpad.get_token_and_login('consumer')
+ NoNetworkLaunchpad.get_token_and_login('consumer')
self.assertEquals(len(caught), 1)
self.assertEquals(caught[0].category, DeprecationWarning)