From e7da94b249a014c97ce01d6168cb1d11b6fbe7a3 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 13 Feb 2016 04:34:35 -0500 Subject: [PATCH 01/10] Add a .one method to RecordCollections It's a common use-case to want one and only one result from a query. This adds a .one method to RecordCollections that is parallel to .all. --- records.py | 24 ++++++++++++++++++++++++ tests/test_records.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/records.py b/records.py index 74c50c4..b599e9a 100644 --- a/records.py +++ b/records.py @@ -188,6 +188,30 @@ class RecordCollection(object): def as_dict(self, ordered=False): return self.all(as_dict=not(ordered), as_ordereddict=ordered) + def one(self, default=None, as_dict=False, as_ordereddict=False): + """Returns a single record for the RecordCollection, or `default`.""" + + # Try to get a record, or return default. + try: + record = next(self) + except StopIteration: + return default + + # Ensure that we don't have more than one row. + try: + next(self) + except StopIteration: + pass + else: + raise ValueError('RecordCollection contains too many rows.') + + # Cast and return. + if as_dict: + return record.as_dict() + elif as_ordereddict: + return record.as_dict(ordered=True) + else: + return record class Database(object): """A Database connection.""" diff --git a/tests/test_records.py b/tests/test_records.py index 1af26e9..8df6f78 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -2,6 +2,8 @@ from collections import namedtuple import records +from pytest import raises + IdRecord = namedtuple('IdRecord', 'id') @@ -49,6 +51,32 @@ class TestRecordCollection: assert len(rows) == 10 + # all + + def test_all_returns_a_list_of_records(self): + rows = records.RecordCollection(IdRecord(i) for i in range(3)) + assert rows.all() == [IdRecord(0), IdRecord(1), IdRecord(2)] + + + # one + + def test_one_returns_a_single_record(self): + rows = records.RecordCollection(IdRecord(i) for i in range(1)) + assert rows.one() == IdRecord(0) + + def test_one_defaults_to_None(self): + rows = records.RecordCollection(iter([])) + assert rows.one() is None + + def test_one_default_is_overridable(self): + rows = records.RecordCollection(iter([])) + assert rows.one('Cheese') == 'Cheese' + + def test_one_raises_when_more_than_one(self): + rows = records.RecordCollection(IdRecord(i) for i in range(3)) + raises(ValueError, rows.one) + + class TestRecord: def test_record_dir(self): From 1d1d42324539d0ed2499d7c35916daae16af135a Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 13 Feb 2016 04:44:42 -0500 Subject: [PATCH 02/10] Raise default if it's an exception --- records.py | 14 ++++++++++++++ tests/test_records.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/records.py b/records.py index b599e9a..bea5fa2 100644 --- a/records.py +++ b/records.py @@ -4,6 +4,7 @@ import os from code import interact from datetime import datetime from collections import OrderedDict +from inspect import isclass import tablib from docopt import docopt @@ -13,6 +14,17 @@ from sqlalchemy.ext.declarative import declarative_base DATABASE_URL = os.environ.get('DATABASE_URL') +def isexception(obj): + """Given an object, return a boolean indicating whether it is an instance + or subclass of :py:class:`Exception`. + """ + if isinstance(obj, Exception): + return True + if isclass(obj) and issubclass(obj, Exception): + return True + return False + + class Record(object): """A row, from a query, from a database.""" __slots__ = ('_keys', '_values') @@ -195,6 +207,8 @@ class RecordCollection(object): try: record = next(self) except StopIteration: + if isexception(default): + raise default return default # Ensure that we don't have more than one row. diff --git a/tests/test_records.py b/tests/test_records.py index 8df6f78..9166446 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -76,6 +76,16 @@ class TestRecordCollection: rows = records.RecordCollection(IdRecord(i) for i in range(3)) raises(ValueError, rows.one) + def test_one_raises_default_if_its_an_exception_subclass(self): + rows = records.RecordCollection(IdRecord(i) for i in range(1)) + class Cheese(Exception): pass + raises(Cheese, rows.one, Cheese) + + def test_one_raises_default_if_its_an_exception_instance(self): + rows = records.RecordCollection(IdRecord(i) for i in range(1)) + class Cheese(Exception): pass + raises(Cheese, rows.one, Cheese('cheddar')) + class TestRecord: From ef9938623b4e36197e58f3717a83cdbb9b47cf86 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 13 Feb 2016 04:57:02 -0500 Subject: [PATCH 03/10] Note .one in the README --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 3087192..f6a6a43 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,13 @@ Or store a copy of your record collection for later reference: >>> rows.all() [, , , ...] +If you're only expecting one result: + +.. code:: python + + >>> rows.one() + + Other options include ``rows.as_dict()`` and ``rows.as_dict(ordered=True)``. ☤ Features From fafaca6a969101870a379b1b802a275e7a0bbe66 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 13 Feb 2016 11:02:11 -0500 Subject: [PATCH 04/10] Fix tests for raising one(default) --- tests/test_records.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 9166446..f45b141 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -77,12 +77,12 @@ class TestRecordCollection: raises(ValueError, rows.one) def test_one_raises_default_if_its_an_exception_subclass(self): - rows = records.RecordCollection(IdRecord(i) for i in range(1)) + rows = records.RecordCollection(iter([])) class Cheese(Exception): pass raises(Cheese, rows.one, Cheese) def test_one_raises_default_if_its_an_exception_instance(self): - rows = records.RecordCollection(IdRecord(i) for i in range(1)) + rows = records.RecordCollection(iter([])) class Cheese(Exception): pass raises(Cheese, rows.one, Cheese('cheddar')) From 85fbec787a71f4f97c61211ad2ec256cb5c8bcdc Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Sat, 13 Feb 2016 11:03:13 -0500 Subject: [PATCH 05/10] Switch to index access for idiomaticity --- records.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/records.py b/records.py index bea5fa2..5afc071 100644 --- a/records.py +++ b/records.py @@ -205,16 +205,16 @@ class RecordCollection(object): # Try to get a record, or return default. try: - record = next(self) - except StopIteration: + record = self[0] + except IndexError: if isexception(default): raise default return default # Ensure that we don't have more than one row. try: - next(self) - except StopIteration: + self[1] + except IndexError: pass else: raise ValueError('RecordCollection contains too many rows.') From 7588c1ca2be7befd5145510fdff707e4d443775c Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 21 Nov 2016 19:41:22 -0500 Subject: [PATCH 06/10] Rename .one to .first --- records.py | 2 +- tests/test_records.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/records.py b/records.py index 5afc071..afa0721 100644 --- a/records.py +++ b/records.py @@ -200,7 +200,7 @@ class RecordCollection(object): def as_dict(self, ordered=False): return self.all(as_dict=not(ordered), as_ordereddict=ordered) - def one(self, default=None, as_dict=False, as_ordereddict=False): + def first(self, default=None, as_dict=False, as_ordereddict=False): """Returns a single record for the RecordCollection, or `default`.""" # Try to get a record, or return default. diff --git a/tests/test_records.py b/tests/test_records.py index f45b141..b5d124f 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -58,33 +58,33 @@ class TestRecordCollection: assert rows.all() == [IdRecord(0), IdRecord(1), IdRecord(2)] - # one + # first - def test_one_returns_a_single_record(self): + def test_first_returns_a_single_record(self): rows = records.RecordCollection(IdRecord(i) for i in range(1)) - assert rows.one() == IdRecord(0) + assert rows.first() == IdRecord(0) - def test_one_defaults_to_None(self): + def test_first_defaults_to_Nfirst(self): rows = records.RecordCollection(iter([])) - assert rows.one() is None + assert rows.first() is None - def test_one_default_is_overridable(self): + def test_first_default_is_overridable(self): rows = records.RecordCollection(iter([])) - assert rows.one('Cheese') == 'Cheese' + assert rows.first('Cheese') == 'Cheese' - def test_one_raises_when_more_than_one(self): + def test_first_raises_when_more_than_first(self): rows = records.RecordCollection(IdRecord(i) for i in range(3)) - raises(ValueError, rows.one) + raises(ValueError, rows.first) - def test_one_raises_default_if_its_an_exception_subclass(self): + def test_first_raises_default_if_its_an_exception_subclass(self): rows = records.RecordCollection(iter([])) class Cheese(Exception): pass - raises(Cheese, rows.one, Cheese) + raises(Cheese, rows.first, Cheese) - def test_one_raises_default_if_its_an_exception_instance(self): + def test_first_raises_default_if_its_an_exception_instance(self): rows = records.RecordCollection(iter([])) class Cheese(Exception): pass - raises(Cheese, rows.one, Cheese('cheddar')) + raises(Cheese, rows.first, Cheese('cheddar')) class TestRecord: From 3b7438f130fe2b37709a5ce9b08d4fa73e007e50 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 21 Nov 2016 19:41:51 -0500 Subject: [PATCH 07/10] Update docstring/comment re: raising default --- records.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/records.py b/records.py index afa0721..bd3cdb2 100644 --- a/records.py +++ b/records.py @@ -201,9 +201,11 @@ class RecordCollection(object): return self.all(as_dict=not(ordered), as_ordereddict=ordered) def first(self, default=None, as_dict=False, as_ordereddict=False): - """Returns a single record for the RecordCollection, or `default`.""" + """Returns a single record for the RecordCollection, or `default`. If + `default` is an instance or subclass of Exception, then raise it + instead of returning it.""" - # Try to get a record, or return default. + # Try to get a record, or return/raise default. try: record = self[0] except IndexError: From 16321e4ae45c5268a022794e46e4bd11a28e0fd9 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 21 Nov 2016 19:44:51 -0500 Subject: [PATCH 08/10] Update README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f6a6a43..e8df2d3 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ If you're only expecting one result: .. code:: python - >>> rows.one() + >>> rows.first() Other options include ``rows.as_dict()`` and ``rows.as_dict(ordered=True)``. From 617f02c62165e170a798939d1a3e94beaff6384a Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 21 Nov 2016 19:45:09 -0500 Subject: [PATCH 09/10] Prune unused imports while we're here --- records.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/records.py b/records.py index bd3cdb2..f65e8ff 100644 --- a/records.py +++ b/records.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- import os -from code import interact -from datetime import datetime from collections import OrderedDict from inspect import isclass From 1217c9eea26ef0cb4a33bbfa4022b60ac721a690 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Mon, 21 Nov 2016 19:46:42 -0500 Subject: [PATCH 10/10] Fix regression in test name :o) --- tests/test_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index b5d124f..722591f 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -64,7 +64,7 @@ class TestRecordCollection: rows = records.RecordCollection(IdRecord(i) for i in range(1)) assert rows.first() == IdRecord(0) - def test_first_defaults_to_Nfirst(self): + def test_first_defaults_to_None(self): rows = records.RecordCollection(iter([])) assert rows.first() is None