From 0a35922d12c862c59ee9c565542eeae0883d052b Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 4 May 2013 13:15:51 +0100 Subject: [PATCH 1/7] Move config loader into the OpenPhoto class. This allows config files to be used everywhere, not just from the commandline. --- README.markdown | 11 +++-- openphoto/__init__.py | 14 ++++-- openphoto/main.py | 73 +++++++++--------------------- openphoto/openphoto_http.py | 88 +++++++++++++++++++++++++++++++------ 4 files changed, 113 insertions(+), 73 deletions(-) diff --git a/README.markdown b/README.markdown index c2022fd..34adbe8 100644 --- a/README.markdown +++ b/README.markdown @@ -46,7 +46,7 @@ The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenp ### Using from the command line -When using the command line tool, you'll want to export your authentication credentials to the environment. +When using the command line tool, you'll want to export your authentication credentials to the environment. The command line tool will look for the following config file in ~/.config/openphoto/default (the -c switch lets you specify a different config file): @@ -63,7 +63,7 @@ These are the options you can pass to the shell program: -h # Display help text -c config_file # Either the name of a config file in ~/.config/openphoto/ or a full path to a config file - -H hostname # Overrides config_file for unauthenticated API calls [default=localhost] + -H hostname # Overrides config_file for unauthenticated API calls -e endpoint # [default=/photos/list.json] -X method # [default=GET] -F params # e.g. -F 'title=my title' -F 'tags=mytag1,mytag2' @@ -113,5 +113,8 @@ You can run commands to the OpenPhoto API from your shell! #### Getting your credentials -You can get your credentals by clicking on the arrow next to your email address once you're logged into your site and then clicking on settings. -If you don't have any credentials then you can create one for yourself using the "Create a new app" button. +To get your credentials: + * Log into your Trovebox site + * Click the arrow on the top-right and select 'Settings'. + * Click the 'Create a new app' button. + * Click the 'View' link beside the newly created app. diff --git a/openphoto/__init__.py b/openphoto/__init__.py index 7e497bd..295940c 100644 --- a/openphoto/__init__.py +++ b/openphoto/__init__.py @@ -5,14 +5,20 @@ import api_tag import api_album class OpenPhoto(OpenPhotoHttp): - """ Client library for OpenPhoto """ - def __init__(self, host, + """ + Client library for OpenPhoto + If no parameters are specified, config is loaded from the default + location (~/.config/openphoto/default). + The config_file parameter is used to specify an alternate config file. + If the host parameter is specified, no config file is loaded. + """ + def __init__(self, config_file=None, host=None, consumer_key='', consumer_secret='', token='', token_secret=''): - OpenPhotoHttp.__init__(self, host, + OpenPhotoHttp.__init__(self, config_file, host, consumer_key, consumer_secret, token, token_secret) - + self.photos = api_photo.ApiPhotos(self) self.photo = api_photo.ApiPhoto(self) self.tags = api_tag.ApiTags(self) diff --git a/openphoto/main.py b/openphoto/main.py index 0a06f53..30d3eac 100644 --- a/openphoto/main.py +++ b/openphoto/main.py @@ -3,8 +3,6 @@ import os import sys import string import urllib -import StringIO -import ConfigParser from optparse import OptionParser try: @@ -14,47 +12,6 @@ except ImportError: from openphoto import OpenPhoto -def get_config_path(config_file): - config_path = os.getenv('XDG_CONFIG_HOME') - if not config_path: - config_path = os.path.join(os.getenv('HOME'), ".config") - if not config_file: - config_file = "default" - return os.path.join(config_path, "openphoto", config_file) - -def read_config(config_file): - """ - Loads config data from the specified file. - If config_file doesn't exist, returns an empty authentication config for localhost. - """ - section = "DUMMY" - defaults = {'host': 'localhost', - 'consumerKey': '', 'consumerSecret': '', - 'token': '', 'tokenSecret':'', - } - # Insert an section header at the start of the config file, so ConfigParser can understand it - # Also prepend a [DEFAULT] section, since it's the only way to specify case-sensitive defaults - buf = StringIO.StringIO() - buf.write("[DEFAULT]\n") - for key in defaults: - buf.write("%s=%s\n" % (key, defaults[key])) - buf.write('[%s]\n' % section) - if os.path.isfile(config_file): - buf.write(open(config_file).read()) - else: - print "Config file '%s' doesn't exist - authentication won't be used" % config_file - - buf.seek(0, os.SEEK_SET) - parser = ConfigParser.SafeConfigParser() - parser.optionxform = str # Case-sensitive options - parser.readfp(buf) - - # Trim quotes - config = parser.items(section) - config = [(item[0], item[1].replace('"', '')) for item in config] - config = [(item[0], item[1].replace("'", "")) for item in config] - return dict(config) - ################################################################# def main(args=sys.argv[1:]): @@ -85,16 +42,28 @@ def main(args=sys.argv[1:]): # Host option overrides config file settings if options.host: - config = {'host': options.host, 'consumerKey': '', 'consumerSecret': '', - 'token': '', 'tokenSecret': ''} + client = OpenPhoto(host=options.host) else: - config_path = get_config_path(options.config_file) - config = read_config(config_path) - if options.verbose: - print "Using config from '%s'" % config_path - - client = OpenPhoto(config['host'], config['consumerKey'], config['consumerSecret'], - config['token'], config['tokenSecret']) + try: + client = OpenPhoto(config_file=options.config_file) + except IOError as error: + print error + print + print "You must create a configuration file with the following contents:" + print " host = your.host.com" + print " consumerKey = your_consumer_key" + print " consumerSecret = your_consumer_secret" + print " token = your_access_token" + print " tokenSecret = your_access_token_secret" + print + print "To get your credentials:" + print " * Log into your Trovebox site" + print " * Click the arrow on the top-right and select 'Settings'." + print " * Click the 'Create a new app' button." + print " * Click the 'View' link beside the newly created app." + print + print error + sys.exit(1) if options.method == "GET": result = client.get(options.endpoint, process_response=False, **params) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 59431f6..80f8938 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -1,9 +1,12 @@ +import os import oauth2 as oauth import urlparse import urllib import urllib2 import httplib2 import logging +import StringIO +import ConfigParser try: import json except ImportError: @@ -17,17 +20,37 @@ DUPLICATE_RESPONSE = {"code": 409, "message": "This photo already exists"} class OpenPhotoHttp: - """ Base class to handle HTTP requests to an OpenPhoto server """ - def __init__(self, host, consumer_key='', consumer_secret='', + """ + Base class to handle HTTP requests to an OpenPhoto server. + If no parameters are specified, config is loaded from the default + location (~/.config/openphoto/default). + The config_file parameter is used to specify an alternate config file. + If the host parameter is specified, no config file is loaded. + """ + def __init__(self, config_file=None, host=None, + consumer_key='', consumer_secret='', token='', token_secret=''): - self._host = host - self._consumer_key = consumer_key - self._consumer_secret = consumer_secret - self._token = token - self._token_secret = token_secret self._logger = logging.getLogger("openphoto") + if host is None: + self.config_path = self._get_config_path(config_file) + config = self._read_config(self.config_path) + self._host = config['host'] + self._consumer_key = config['consumerKey'] + self._consumer_secret = config['consumerSecret'] + self._token = config['token'] + self._token_secret = config['tokenSecret'] + else: + self._host = host + self._consumer_key = consumer_key + self._consumer_secret = consumer_secret + self._token = token + self._token_secret = token_secret + + if host is not None and config_file is not None: + raise ValueError("Cannot specify both host and config_file") + # Remember the most recent HTTP request and response self.last_url = None self.last_params = None @@ -36,9 +59,9 @@ class OpenPhotoHttp: def get(self, endpoint, process_response=True, **params): """ Performs an HTTP GET from the specified endpoint (API path), - passing parameters if given. - Returns the decoded JSON dictionary, and raises exceptions if an - error code is received. + passing parameters if given. + Returns the decoded JSON dictionary, and raises exceptions if an + error code is received. Returns the raw response if process_response=False """ params = self._process_params(params) @@ -71,9 +94,9 @@ class OpenPhotoHttp: def post(self, endpoint, process_response=True, files = {}, **params): """ Performs an HTTP POST to the specified endpoint (API path), - passing parameters if given. - Returns the decoded JSON dictionary, and raises exceptions if an - error code is received. + passing parameters if given. + Returns the decoded JSON dictionary, and raises exceptions if an + error code is received. Returns the raw response if process_response=False """ params = self._process_params(params) @@ -186,3 +209,42 @@ class OpenPhotoHttp: return [] else: return result + + @staticmethod + def _get_config_path(config_file): + config_path = os.getenv('XDG_CONFIG_HOME') + if not config_path: + config_path = os.path.join(os.getenv('HOME'), ".config") + if not config_file: + config_file = "default" + return os.path.join(config_path, "openphoto", config_file) + + def _read_config(self, config_file): + """ + Loads config data from the specified file. + If config_file doesn't exist, returns an empty authentication config for localhost. + """ + section = "DUMMY" + defaults = {'host': 'localhost', + 'consumerKey': '', 'consumerSecret': '', + 'token': '', 'tokenSecret':'', + } + # Insert an section header at the start of the config file, so ConfigParser can understand it + # Also prepend a [DEFAULT] section, since it's the only way to specify case-sensitive defaults + buf = StringIO.StringIO() + buf.write("[DEFAULT]\n") + for key in defaults: + buf.write("%s=%s\n" % (key, defaults[key])) + buf.write('[%s]\n' % section) + buf.write(open(config_file).read()) + + buf.seek(0, os.SEEK_SET) + parser = ConfigParser.SafeConfigParser() + parser.optionxform = str # Case-sensitive options + parser.readfp(buf) + + # Trim quotes + config = parser.items(section) + config = [(item[0], item[1].replace('"', '')) for item in config] + config = [(item[0], item[1].replace("'", "")) for item in config] + return dict(config) From 59e8c0772eb8a944cba7bc197db0c86b024b88a6 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 4 May 2013 14:33:38 +0200 Subject: [PATCH 2/7] Re-ordered README since credentials now apply to all access methods, not just CLI --- README.markdown | 101 ++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/README.markdown b/README.markdown index 34adbe8..b3623ee 100644 --- a/README.markdown +++ b/README.markdown @@ -8,47 +8,10 @@ Open Photo API / Python Library python setup.py install ---------------------------------------- + +### Credentials - -### How to use the library - -To use the library you need to first ``import openphoto`` then instantiate an instance of the class and start making calls. - -You can use the library in one of two ways: - - * Direct GET/POST calls to the server - * Access via Python classes/methods - - -### Direct GET/POST: - - from openphoto import OpenPhoto - client = OpenPhoto(host, consumerKey, consumerSecret, token, tokenSecret) - resp = client.get("/photos/list.json") - resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"]) - - -### Python classes/methods - - from openphoto import OpenPhoto - client = OpenPhoto(host, consumerKey, consumerSecret, token, tokenSecret) - photos = client.photos.list() - photos[0].update(tags=["tag1", "tag2"]) - print photos[0].tags - -The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenphotoproject.org/documentation) endpoint layout. For example, the calls in the example above use the following API endpoints: - -* client.photos.list() -> /photos/list.json -* photos[0].update() -> /photo/<id>/update.json - ----------------------------------------- - - -### Using from the command line - -When using the command line tool, you'll want to export your authentication credentials to the environment. -The command line tool will look for the following config file in ~/.config/openphoto/default -(the -c switch lets you specify a different config file): +For full access to your photos, you need to create the following config file in ``~/.config/openphoto/default`` # ~/.config/openphoto/default host = your.host.com @@ -57,7 +20,51 @@ The command line tool will look for the following config file in ~/.config/openp token = your_access_token tokenSecret = your_access_token_secret -Click here for instructions on getting credentials. +The ``config_file`` switch lets you specify a different config file. + +To get your credentials: + * Log into your Trovebox site + * Click the arrow on the top-right and select 'Settings' + * Click the 'Create a new app' button + * Click the 'View' link beside the newly created app + +---------------------------------------- + +### How to use the library + +You can use the library in one of two ways: + + * Direct GET/POST calls to the server + * Access via Python classes/methods + + +#### Direct GET/POST: + + from openphoto import OpenPhoto + client = OpenPhoto() + resp = client.get("/photos/list.json") + resp = client.post("/photo/62/update.json", tags=["tag1", "tag2"]) + + +#### Python classes/methods + + from openphoto import OpenPhoto + client = OpenPhoto() + photos = client.photos.list() + photos[0].update(tags=["tag1", "tag2"]) + print photos[0].tags + +The OpenPhoto Python class hierarchy mirrors the [OpenPhoto API](http://theopenphotoproject.org/documentation) endpoint layout. For example, the calls in the example above use the following API endpoints: + +* ``client.photos.list() -> /photos/list.json`` +* ``photos[0].update() -> /photo//update.json`` + +---------------------------------------- + + +### Using from the command line + +You can run commands to the OpenPhoto API from your shell! These are the options you can pass to the shell program: @@ -70,7 +77,8 @@ These are the options you can pass to the shell program: -p # Pretty print the json -v # Verbose output -You can run commands to the OpenPhoto API from your shell! + +#### Command line examples # Upload a public photo to the host specified in ~/.config/openphoto/default openphoto -p -X POST -e /photo/upload.json -F 'photo=@/path/to/photo/jpg' -F 'permission=1' @@ -109,12 +117,3 @@ You can run commands to the OpenPhoto API from your shell! ... } } - - -#### Getting your credentials - -To get your credentials: - * Log into your Trovebox site - * Click the arrow on the top-right and select 'Settings'. - * Click the 'Create a new app' button. - * Click the 'View' link beside the newly created app. From ab77ef9d87557ddc0835905577dedaea741e272d Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 4 May 2013 13:44:48 +0100 Subject: [PATCH 3/7] Update tests to use config files Uses 'test' by default Can be overridden using OPENPHOTO_TEST_CONFIG env var --- tests/test_base.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index ad425b1..2494e5b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,22 +1,8 @@ +import os import unittest import logging import openphoto -try: - import tokens -except ImportError: - print ("********************************************************************\n" - "You need to create a 'tokens.py' file containing the following:\n\n" - " host = \"\"\n" - " consumer_key = \"\"\n" - " consumer_secret = \"\"\n" - " token = \"\"\n" - " token_secret = \"\"\n" - " host = \"\"\n\n" - "WARNING: Don't use a production OpenPhoto instance for this!\n" - "********************************************************************\n") - raise - class TestBase(unittest.TestCase): TEST_TITLE = "Test Image - delete me!" TEST_TAG = "test_tag" @@ -36,24 +22,23 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): """ Ensure there is nothing on the server before running any tests """ - cls.client = openphoto.OpenPhoto(tokens.host, - tokens.consumer_key, tokens.consumer_secret, - tokens.token, tokens.token_secret) + config_file = os.getenv("OPENPHOTO_TEST_CONFIG", "test") + cls.client = openphoto.OpenPhoto(config_file=config_file) if cls.client.photos.list() != []: raise ValueError("The test server (%s) contains photos. " "Please delete them before running the tests" - % tokens.host) + % cls.client._host) if cls.client.tags.list() != []: raise ValueError("The test server (%s) contains tags. " "Please delete them before running the tests" - % tokens.host) + % cls.client._host) if cls.client.albums.list() != []: raise ValueError("The test server (%s) contains albums. " "Please delete them before running the tests" - % tokens.host) + % cls.client._host) @classmethod def tearDownClass(cls): From 483d0bf847f5151d3231d016f1dbb9ff0ce84d40 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Sat, 4 May 2013 14:48:47 +0200 Subject: [PATCH 4/7] Update test README with config file details --- tests/README.markdown | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/README.markdown b/tests/README.markdown index 92e250f..4d722a4 100644 --- a/tests/README.markdown +++ b/tests/README.markdown @@ -5,23 +5,27 @@ Tests for the Open Photo API / Python Library ---------------------------------------- ### Requirements -A computer, Python 2.7 and an empty OpenPhoto instance. +A computer, Python 2.7 and an empty OpenPhoto test host. --------------------------------------- ### Setting up -Create a tests/tokens.py file containing the following: +Create a ``~/.config/openphoto/test`` config file containing the following: - # tests/tokens.py - consumer_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - consumer_secret = "xxxxxxxxxx" - token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - token_secret = "xxxxxxxxxx" - host = "your_hostname" + # ~/.config/openphoto/test + host = your.host.com + consumerKey = your_consumer_key + consumerSecret = your_consumer_secret + token = your_access_token + tokenSecret = your_access_token_secret Make sure this is an empty test server, **not a production OpenPhoto server!!!** +You can specify an alternate test config file with the following environment variable: + + export OPENPHOTO_TEST_CONFIG=test2 + --------------------------------------- ### Running the tests From 15d257a0a756d749000d09c7bc12106b3aae7119 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 6 May 2013 16:18:52 +0100 Subject: [PATCH 5/7] Handle quoted config keys --- openphoto/openphoto_http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openphoto/openphoto_http.py b/openphoto/openphoto_http.py index 80f8938..066a3bc 100644 --- a/openphoto/openphoto_http.py +++ b/openphoto/openphoto_http.py @@ -245,6 +245,6 @@ class OpenPhotoHttp: # Trim quotes config = parser.items(section) - config = [(item[0], item[1].replace('"', '')) for item in config] - config = [(item[0], item[1].replace("'", "")) for item in config] + config = [(item[0].replace('"', ''), item[1].replace('"', '')) for item in config] + config = [(item[0].replace("'", ""), item[1].replace("'", "")) for item in config] return dict(config) From 896af0afae4b6dc736f4c3cf49e0622f06c925fa Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 6 May 2013 16:19:09 +0100 Subject: [PATCH 6/7] Add config unit tests --- tests/test_config.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_config.py diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..0d9533b --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,74 @@ +import unittest +import os +import shutil +import openphoto + +CONFIG_HOME_PATH = os.path.join("test", "config") +CONFIG_PATH = os.path.join(CONFIG_HOME_PATH, "openphoto") + +class TestConfig(unittest.TestCase): + def setUp(self): + """ Override XDG_CONFIG_HOME env var, to use test configs """ + try: + self.original_xdg_config_home = os.environ["XDG_CONFIG_HOME"] + except KeyError: + self.original_xdg_config_home = None + os.environ["XDG_CONFIG_HOME"] = CONFIG_HOME_PATH + os.makedirs(CONFIG_PATH) + + def tearDown(self): + if self.original_xdg_config_home is None: + del os.environ["XDG_CONFIG_HOME"] + else: + os.environ["XDG_CONFIG_HOME"] = self.original_xdg_config_home + shutil.rmtree(CONFIG_HOME_PATH, ignore_errors=True) + + def create_config(self, config_file, host): + f = open(os.path.join(CONFIG_PATH, config_file), "w") + f.write("host = %s\n" % host) + f.write("# Comment\n\n") + f.write("consumerKey = \"%s_consumer_key\"\n" % config_file) + f.write("\"consumerSecret\" = %s_consumer_secret\n" % config_file) + f.write("'token'=%s_token\n" % config_file) + f.write("tokenSecret = '%s_token_secret'\n" % config_file) + + def test_default_config(self): + self.create_config("default", "Test Default Host") + client = openphoto.OpenPhoto() + self.assertEqual(client._host, "Test Default Host") + self.assertEqual(client._consumer_key, "default_consumer_key") + self.assertEqual(client._consumer_secret, "default_consumer_secret") + self.assertEqual(client._token, "default_token") + self.assertEqual(client._token_secret, "default_token_secret") + + def test_custom_config(self): + self.create_config("default", "Test Default Host") + self.create_config("custom", "Test Custom Host") + client = openphoto.OpenPhoto(config_file="custom") + self.assertEqual(client._host, "Test Custom Host") + self.assertEqual(client._consumer_key, "custom_consumer_key") + self.assertEqual(client._consumer_secret, "custom_consumer_secret") + self.assertEqual(client._token, "custom_token") + self.assertEqual(client._token_secret, "custom_token_secret") + + def test_host_override(self): + self.create_config("default", "Test Default Host") + client = openphoto.OpenPhoto(host="host_override") + self.assertEqual(client._host, "host_override") + self.assertEqual(client._consumer_key, "") + self.assertEqual(client._consumer_secret, "") + self.assertEqual(client._token, "") + self.assertEqual(client._token_secret, "") + + def test_missing_config_files(self): + with self.assertRaises(IOError): + openphoto.OpenPhoto() + with self.assertRaises(IOError): + openphoto.OpenPhoto(config_file="custom") + + def test_host_and_config_file_raises_exception(self): + self.create_config("custom", "Test Custon Host") + with self.assertRaises(ValueError): + openphoto.OpenPhoto(config_file="custom", host="host_override") + + From 53b3f7c6cf2fbb900d696543501e09c664add339 Mon Sep 17 00:00:00 2001 From: sneakypete81 Date: Mon, 6 May 2013 16:43:28 +0100 Subject: [PATCH 7/7] Config unit test tweaks --- tests/test_config.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 0d9533b..3ce3c03 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,6 +33,7 @@ class TestConfig(unittest.TestCase): f.write("tokenSecret = '%s_token_secret'\n" % config_file) def test_default_config(self): + """ Ensure the default config is loaded """ self.create_config("default", "Test Default Host") client = openphoto.OpenPhoto() self.assertEqual(client._host, "Test Default Host") @@ -42,6 +43,7 @@ class TestConfig(unittest.TestCase): self.assertEqual(client._token_secret, "default_token_secret") def test_custom_config(self): + """ Ensure a custom config can be loaded """ self.create_config("default", "Test Default Host") self.create_config("custom", "Test Custom Host") client = openphoto.OpenPhoto(config_file="custom") @@ -51,7 +53,19 @@ class TestConfig(unittest.TestCase): self.assertEqual(client._token, "custom_token") self.assertEqual(client._token_secret, "custom_token_secret") + def test_full_config_path(self): + """ Ensure a full custom config path can be loaded """ + self.create_config("path", "Test Path Host") + full_path = os.path.abspath(CONFIG_PATH) + client = openphoto.OpenPhoto(config_file=os.path.join(full_path, "path")) + self.assertEqual(client._host, "Test Path Host") + self.assertEqual(client._consumer_key, "path_consumer_key") + self.assertEqual(client._consumer_secret, "path_consumer_secret") + self.assertEqual(client._token, "path_token") + self.assertEqual(client._token_secret, "path_token_secret") + def test_host_override(self): + """ Ensure that specifying a host overrides the default config """ self.create_config("default", "Test Default Host") client = openphoto.OpenPhoto(host="host_override") self.assertEqual(client._host, "host_override") @@ -60,14 +74,16 @@ class TestConfig(unittest.TestCase): self.assertEqual(client._token, "") self.assertEqual(client._token_secret, "") - def test_missing_config_files(self): + def test_missing_config_files_raise_exceptions(self): + """ Ensure that missing config files raise exceptions """ with self.assertRaises(IOError): openphoto.OpenPhoto() with self.assertRaises(IOError): openphoto.OpenPhoto(config_file="custom") def test_host_and_config_file_raises_exception(self): - self.create_config("custom", "Test Custon Host") + """ It's not valid to specify both a host and a config_file """ + self.create_config("custom", "Test Custom Host") with self.assertRaises(ValueError): openphoto.OpenPhoto(config_file="custom", host="host_override")