diff options
author | SVN-Git Migration <python-modules-team@lists.alioth.debian.org> | 2015-10-08 12:22:04 -0700 |
---|---|---|
committer | SVN-Git Migration <python-modules-team@lists.alioth.debian.org> | 2015-10-08 12:22:04 -0700 |
commit | 7d1cd972ba81f86e9161d7708278678a4b8d5f68 (patch) | |
tree | 30e34783d56f7d5f34d4db5b44c69665c11bb5c9 /src | |
parent | 095b6a3014c89083dcb6bdee66a0699a2791a6a8 (diff) |
Imported Upstream version 1.9.11
Diffstat (limited to 'src')
-rw-r--r-- | src/launchpadlib.egg-info/PKG-INFO | 24 | ||||
-rw-r--r-- | src/launchpadlib.egg-info/SOURCES.txt | 214 | ||||
-rw-r--r-- | src/launchpadlib.egg-info/requires.txt | 1 | ||||
-rw-r--r-- | src/launchpadlib/NEWS.txt | 22 | ||||
-rw-r--r-- | src/launchpadlib/__init__.py | 4 | ||||
-rw-r--r-- | src/launchpadlib/credentials.py | 28 | ||||
-rw-r--r-- | src/launchpadlib/launchpad.py | 7 | ||||
-rw-r--r-- | src/launchpadlib/testing/launchpad.py | 537 | ||||
-rw-r--r-- | src/launchpadlib/testing/resources.py | 55 | ||||
-rw-r--r-- | src/launchpadlib/testing/tests/__init__.py | 0 | ||||
-rw-r--r-- | src/launchpadlib/testing/tests/test_launchpad.py | 393 | ||||
-rw-r--r-- | src/launchpadlib/tests/test_credential_store.py | 18 | ||||
-rw-r--r-- | src/launchpadlib/tests/test_launchpad.py | 8 |
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) |