diff --git a/bson/json_util.py b/bson/json_util.py index 3af508aa6..8fb87b22b 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -31,7 +31,7 @@ Example usage (serialization): >>> dumps([{'foo': [1, 2]}, ... {'bar': {'hello': 'world'}}, ... {'code': Code("function x() { return 1; }")}, - ... {'bin': Binary("\x01\x02\x03\x04")}]) + ... {'bin': Binary(b"\x01\x02\x03\x04")}]) '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]' Example usage (deserialization): diff --git a/doc/conf.py b/doc/conf.py index 18d11dd8a..7726ee2b2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,8 +4,17 @@ # # This file is execfile()d with the current directory set to its containing dir. -import sys, os -sys.path[0:0] = [os.path.abspath('..')] +import os +import sys + +_path = os.path.abspath('..') +sys.path[0:0] = [_path] +if sys.version_info[0] >= 3: + import glob + ver = '.'.join(map(str, sys.version_info[:2])) + _path = glob.glob( + os.path.join(os.path.abspath('..'), 'build', 'lib*' + ver))[0] + sys.path[0:0] = [_path] import pymongo @@ -65,9 +74,9 @@ pygments_style = 'sphinx' # -- Options for extensions ---------------------------------------------------- autoclass_content = 'init' -doctest_path = os.path.abspath('..') +doctest_path = [_path] -doctest_test_doctest_blocks = False +doctest_test_doctest_blocks = '' doctest_global_setup = """ from pymongo.mongo_client import MongoClient diff --git a/doc/examples/aggregation.rst b/doc/examples/aggregation.rst index 1b5c4202e..94258da20 100644 --- a/doc/examples/aggregation.rst +++ b/doc/examples/aggregation.rst @@ -51,15 +51,20 @@ eg "$sort": PyMongo version **>= 2.3**. .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> from bson.son import SON - >>> db.things.aggregate([ + >>> import pprint + >>> pprint.pprint(db.things.aggregate([ ... {"$unwind": "$tags"}, ... {"$group": {"_id": "$tags", "count": {"$sum": 1}}}, ... {"$sort": SON([("count", -1), ("_id", -1)])} - ... ]) + ... ])) ... - {u'ok': 1.0, u'result': [{u'count': 3, u'_id': u'cat'}, {u'count': 2, u'_id': u'dog'}, {u'count': 1, u'_id': u'mouse'}]} + {u'ok': 1.0, + u'result': [{u'_id': u'cat', u'count': 3}, + {u'_id': u'dog', u'count': 2}, + {u'_id': u'mouse', u'count': 1}]} As well as simple aggregations the aggregation framework provides projection @@ -115,7 +120,7 @@ iterate over the result collection: >>> result = db.things.map_reduce(mapper, reducer, "myresults") >>> for doc in result.find(): - ... print doc + ... pprint.pprint(doc) ... {u'_id': u'cat', u'value': 3.0} {u'_id': u'dog', u'value': 2.0} @@ -132,8 +137,12 @@ response to the map/reduce command, rather than just the result collection: .. doctest:: - >>> db.things.map_reduce(mapper, reducer, "myresults", full_response=True) - {u'counts': {u'input': 4, u'reduce': 2, u'emit': 6, u'output': 3}, u'timeMillis': ..., u'ok': ..., u'result': u'...'} + >>> pprint.pprint( + ... db.things.map_reduce(mapper, reducer, "myresults", full_response=True)) + {u'counts': {u'emit': 6, u'input': 4, u'output': 3, u'reduce': 2}, + u'ok': ..., + u'result': u'...', + u'timeMillis': ...} All of the optional map/reduce parameters are also supported, simply pass them as keyword arguments. In this example we use the `query` parameter to limit the @@ -141,9 +150,10 @@ documents that will be mapped over: .. doctest:: - >>> result = db.things.map_reduce(mapper, reducer, "myresults", query={"x": {"$lt": 2}}) - >>> for doc in result.find(): - ... print doc + >>> results = db.things.map_reduce( + ... mapper, reducer, "myresults", query={"x": {"$lt": 2}}) + >>> for doc in results.find(): + ... pprint.pprint(doc) ... {u'_id': u'cat', u'value': 1.0} {u'_id': u'dog', u'value': 1.0} @@ -155,8 +165,16 @@ result collection: .. doctest:: >>> from bson.son import SON - >>> db.things.map_reduce(mapper, reducer, out=SON([("replace", "results"), ("db", "outdb")]), full_response=True) - {u'counts': {u'input': 4, u'reduce': 2, u'emit': 6, u'output': 3}, u'timeMillis': ..., u'ok': ..., u'result': {u'db': ..., u'collection': ...}} + >>> pprint.pprint( + ... db.things.map_reduce( + ... mapper, + ... reducer, + ... out=SON([("replace", "results"), ("db", "outdb")]), + ... full_response=True)) + {u'counts': {u'emit': 6, u'input': 4, u'output': 3, u'reduce': 2}, + u'ok': ..., + u'result': {u'collection': ..., u'db': ...}, + u'timeMillis': ...} .. seealso:: The full list of options for MongoDB's `map reduce engine `_ @@ -184,7 +202,7 @@ Here we are doing a simple group and count of the occurrences of ``x`` values: ... >>> results = db.things.group(key={"x":1}, condition={}, initial={"count": 0}, reduce=reducer) >>> for doc in results: - ... print doc + ... pprint.pprint(doc) {u'count': 1.0, u'x': 1.0} {u'count': 2.0, u'x': 2.0} {u'count': 1.0, u'x': 3.0} diff --git a/doc/examples/bulk.rst b/doc/examples/bulk.rst index 5e2339549..7829c9d05 100644 --- a/doc/examples/bulk.rst +++ b/doc/examples/bulk.rst @@ -27,7 +27,7 @@ bulk insert operations. >>> import pymongo >>> db = pymongo.MongoClient().bulk_example - >>> db.test.insert(({'i': i} for i in xrange(10000))) + >>> db.test.insert(({'i': i} for i in range(10000))) [...] >>> db.test.count() 10000 @@ -58,6 +58,7 @@ order provided for serial execution. The return value is a document describing the type and count of operations performed. .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> from pprint import pprint >>> @@ -95,6 +96,7 @@ occurred and details about the failure - including the operation that caused the failure. .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> from pymongo.errors import BulkWriteError >>> bulk = db.test.initialize_ordered_bulk_op() @@ -106,7 +108,7 @@ the failure. >>> try: ... bulk.execute() ... except BulkWriteError as bwe: - ... pprint(bwe.details) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... pprint(bwe.details) ... {'nInserted': 0, 'nMatched': 1, @@ -134,6 +136,7 @@ constraint on _id. Since we are doing unordered execution the second and fourth operations succeed. .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> bulk = db.test.initialize_unordered_bulk_op() >>> bulk.insert({'_id': 1}) @@ -143,7 +146,7 @@ and fourth operations succeed. >>> try: ... bulk.execute() ... except BulkWriteError as bwe: - ... pprint(bwe.details) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... pprint(bwe.details) ... {'nInserted': 0, 'nMatched': 1, @@ -172,6 +175,7 @@ errors (e.g. wtimeout) will be reported after all operations are attempted, regardless of execution order. .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> bulk = db.test.initialize_ordered_bulk_op() >>> bulk.insert({'a': 0}) diff --git a/doc/examples/custom_type.rst b/doc/examples/custom_type.rst index 8c995c478..e1e28615c 100644 --- a/doc/examples/custom_type.rst +++ b/doc/examples/custom_type.rst @@ -68,10 +68,12 @@ use them with PyMongo: .. doctest:: + >>> import pprint >>> db.test.insert({"custom": encode_custom(Custom(5))}) ObjectId('...') - >>> db.test.find_one() - {u'_id': ObjectId('...'), u'custom': {u'x': 5, u'_type': u'custom'}} + >>> pprint.pprint(db.test.find_one()) + {u'_id': ObjectId('...'), + u'custom': {u'_type': u'custom', u'x': 5}} >>> decode_custom(db.test.find_one()["custom"]) >>> decode_custom(db.test.find_one()["custom"]).x() @@ -122,8 +124,9 @@ After doing so we can save and restore :class:`Custom` instances seamlessly: {...} >>> db.test.insert({"custom": Custom(5)}) ObjectId('...') - >>> db.test.find_one() - {u'_id': ObjectId('...'), u'custom': } + >>> pprint.pprint(db.test.find_one()) + {u'_id': ObjectId('...'), + u'custom': } >>> db.test.find_one()["custom"].x() 5 @@ -139,8 +142,9 @@ This allows us to see what was actually saved to the database: .. doctest:: - >>> db.test.find_one() - {u'_id': ObjectId('...'), u'custom': {u'x': 5, u'_type': u'custom'}} + >>> pprint.pprint(db.test.find_one()) + {u'_id': ObjectId('...'), + u'custom': {u'_type': u'custom', u'x': 5}} which is the same format that we encode to with our :meth:`encode_custom` method! @@ -163,7 +167,7 @@ from :class:`~bson.binary.Binary` instances: >>> from bson.binary import Binary >>> def to_binary(custom): - ... return Binary(str(custom.x()), 128) + ... return Binary(str(custom.x()).encode(), 128) ... >>> def from_binary(binary): ... return Custom(int(binary)) @@ -209,8 +213,9 @@ seamlessly: >>> db.test.insert({"custom": Custom(5)}) ObjectId('...') - >>> db.test.find_one() - {u'_id': ObjectId('...'), u'custom': } + >>> pprint.pprint(db.test.find_one()) + {u'_id': ObjectId('...'), + u'custom': } >>> db.test.find_one()["custom"].x() 5 @@ -222,5 +227,5 @@ clearing out the manipulators and repeating our .. doctest:: >>> db = client.custom_type_example - >>> db.test.find_one() + >>> pprint.pprint(db.test.find_one()) {u'_id': ObjectId('...'), u'custom': Binary('5', 128)} diff --git a/doc/examples/geo.rst b/doc/examples/geo.rst index 8555386db..9ecca087f 100644 --- a/doc/examples/geo.rst +++ b/doc/examples/geo.rst @@ -49,12 +49,13 @@ Using the geospatial index we can find documents near another point: .. doctest:: + >>> import pprint >>> for doc in db.places.find({"loc": {"$near": [3, 6]}}).limit(3): - ... repr(doc) # doctest: +ELLIPSIS + ... pprint.pprint(doc) ... - "{u'loc': [2, 5], u'_id': ObjectId('...')}" - "{u'loc': [4, 4], u'_id': ObjectId('...')}" - "{u'loc': [1, 2], u'_id': ObjectId('...')}" + {u'_id': ObjectId('...'), u'loc': [2, 5]} + {u'_id': ObjectId('...'), u'loc': [4, 4]} + {u'_id': ObjectId('...'), u'loc': [1, 2]} The $maxDistance operator requires the use of :class:`~bson.son.SON`: @@ -63,11 +64,11 @@ The $maxDistance operator requires the use of :class:`~bson.son.SON`: >>> from bson.son import SON >>> query = {"loc": SON([("$near", [3, 6]), ("$maxDistance", 100)])} >>> for doc in db.places.find(query).limit(3): - ... repr(doc) # doctest: +ELLIPSIS + ... pprint.pprint(doc) ... - "{u'loc': [2, 5], u'_id': ObjectId('...')}" - "{u'loc': [4, 4], u'_id': ObjectId('...')}" - "{u'loc': [1, 2], u'_id': ObjectId('...')}" + {u'_id': ObjectId('...'), u'loc': [2, 5]} + {u'_id': ObjectId('...'), u'loc': [4, 4]} + {u'_id': ObjectId('...'), u'loc': [1, 2]} It's also possible to query for all items within a given rectangle (specified by lower-left and upper-right coordinates): @@ -76,10 +77,10 @@ It's also possible to query for all items within a given rectangle >>> query = {"loc": {"$within": {"$box": [[2, 2], [5, 6]]}}} >>> for doc in db.places.find(query).sort('_id'): - ... repr(doc) # doctest: +ELLIPSIS + ... pprint.pprint(doc) ... - "{u'loc': [2, 5], u'_id': ObjectId('...')}" - "{u'loc': [4, 4], u'_id': ObjectId('...')}" + {u'_id': ObjectId('...'), u'loc': [2, 5]} + {u'_id': ObjectId('...'), u'loc': [4, 4]} Or circle (specified by center point and radius): @@ -87,11 +88,11 @@ Or circle (specified by center point and radius): >>> query = {"loc": {"$within": {"$center": [[0, 0], 6]}}} >>> for doc in db.places.find(query).sort('_id'): - ... repr(doc) # doctest: +ELLIPSIS + ... pprint.pprint(doc) ... - "{u'loc': [2, 5], u'_id': ObjectId('...')}" - "{u'loc': [1, 2], u'_id': ObjectId('...')}" - "{u'loc': [4, 4], u'_id': ObjectId('...')}" + {u'_id': ObjectId('...'), u'loc': [2, 5]} + {u'_id': ObjectId('...'), u'loc': [1, 2]} + {u'_id': ObjectId('...'), u'loc': [4, 4]} geoNear queries are also supported using :class:`~bson.son.SON`:: diff --git a/doc/examples/gridfs.rst b/doc/examples/gridfs.rst index 08be41ddf..db55bd2b5 100644 --- a/doc/examples/gridfs.rst +++ b/doc/examples/gridfs.rst @@ -42,7 +42,7 @@ interface (the :meth:`~gridfs.GridFS.put` and .. doctest:: - >>> a = fs.put("hello world") + >>> a = fs.put(b"hello world") :meth:`~gridfs.GridFS.put` creates a new file in GridFS, and returns the value of the file document's ``"_id"`` key. Given that ``"_id"`` diff --git a/doc/faq.rst b/doc/faq.rst index f2183e75b..dba88e5cd 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -126,31 +126,25 @@ is displayed: PyMongo represents BSON documents as Python dicts by default, and the order of keys in dicts is not defined. That is, a dict declared with the "a" key -first is the same, to Python, as one with "b" first: +first is the same, to Python, as one with "b" first:: -.. doctest:: key-order - - >>> print {'a': 1.0, 'b': 1.0} + >>> print({'a': 1.0, 'b': 1.0}) {'a': 1.0, 'b': 1.0} - >>> print {'b': 1.0, 'a': 1.0} + >>> print({'b': 1.0, 'a': 1.0}) {'a': 1.0, 'b': 1.0} Therefore, Python dicts are not guaranteed to show keys in the order they are -stored in BSON. Here, "a" is shown before "b": +stored in BSON. Here, "a" is shown before "b":: -.. doctest:: key-order - - >>> print collection.find_one() + >>> print(collection.find_one()) {u'_id': 1.0, u'subdocument': {u'a': 1.0, u'b': 1.0}} To preserve order when reading BSON, use the :class:`~bson.son.SON` class, which is a dict that remembers its key order. Now, documents and subdocuments -in query results are represented with :class:`~bson.son.SON` objects: - -.. doctest:: key-order +in query results are represented with :class:`~bson.son.SON` objects:: >>> from bson.son import SON - >>> print collection.find_one(as_class=SON) + >>> print(collection.find_one(as_class=SON)) SON([(u'_id', 1.0), (u'subdocument', SON([(u'b', 1.0), (u'a', 1.0)]))]) The subdocument's actual storage layout is now visible: "b" is before "a". @@ -158,25 +152,19 @@ The subdocument's actual storage layout is now visible: "b" is before "a". Because a dict's key order is not defined, you cannot predict how it will be serialized **to** BSON. But MongoDB considers subdocuments equal only if their keys have the same order. So if you use a dict to query on a subdocument it may -not match: - -.. doctest:: key-order +not match:: >>> collection.find_one({'subdocument': {'a': 1.0, 'b': 1.0}}) is None True -Swapping the key order in your query makes no difference: - -.. doctest:: key-order +Swapping the key order in your query makes no difference:: >>> collection.find_one({'subdocument': {'b': 1.0, 'a': 1.0}}) is None True ... because, as we saw above, Python considers the two dicts the same. -There are two solutions. First, you can match the subdocument field-by-field: - -.. doctest:: key-order +There are two solutions. First, you can match the subdocument field-by-field:: >>> collection.find_one({'subdocument.a': 1.0, ... 'subdocument.b': 1.0}) @@ -187,9 +175,7 @@ regardless of the order you specify them in Python or the order they are stored in BSON. Additionally, this query now matches subdocuments with additional keys besides "a" and "b", whereas the previous query required an exact match. -The second solution is to use a :class:`~bson.son.SON` to specify the key order: - -.. doctest:: key-order +The second solution is to use a :class:`~bson.son.SON` to specify the key order:: >>> query = {'subdocument': SON([('b', 1.0), ('a', 1.0)])} >>> collection.find_one(query) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 3ae5fbef3..cff56d749 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -162,8 +162,13 @@ document from the posts collection: .. doctest:: - >>> posts.find_one() - {u'date': datetime.datetime(...), u'text': u'My first blog post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'mongodb', u'python', u'pymongo']} + >>> import pprint + >>> pprint.pprint(posts.find_one()) + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'mongodb', u'python', u'pymongo'], + u'text': u'My first blog post!'} The result is a dictionary matching the one that we inserted previously. @@ -176,8 +181,12 @@ our results to a document with author "Mike" we do: .. doctest:: - >>> posts.find_one({"author": "Mike"}) - {u'date': datetime.datetime(...), u'text': u'My first blog post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'mongodb', u'python', u'pymongo']} + >>> pprint.pprint(posts.find_one({"author": "Mike"})) + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'mongodb', u'python', u'pymongo'], + u'text': u'My first blog post!'} If we try with a different author, like "Eliot", we'll get no result: @@ -194,10 +203,14 @@ We can also find a post by its ``_id``, which in our example is an ObjectId: .. doctest:: - >>> post_id - ObjectId(...) - >>> posts.find_one({"_id": post_id}) - {u'date': datetime.datetime(...), u'text': u'My first blog post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'mongodb', u'python', u'pymongo']} + >>> post_id + ObjectId(...) + >>> pprint.pprint(posts.find_one({"_id": post_id})) + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'mongodb', u'python', u'pymongo'], + u'text': u'My first blog post!'} Note that an ObjectId is not the same as its string representation: @@ -282,11 +295,23 @@ document in the ``posts`` collection: .. doctest:: >>> for post in posts.find(): - ... post + ... pprint.pprint(post) ... - {u'date': datetime.datetime(...), u'text': u'My first blog post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'mongodb', u'python', u'pymongo']} - {u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'bulk', u'insert']} - {u'date': datetime.datetime(2009, 11, 10, 10, 45), u'text': u'and pretty easy too!', u'_id': ObjectId('...'), u'author': u'Eliot', u'title': u'MongoDB is fun'} + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'mongodb', u'python', u'pymongo'], + u'text': u'My first blog post!'} + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'bulk', u'insert'], + u'text': u'Another post!'} + {u'_id': ObjectId('...'), + u'author': u'Eliot', + u'date': datetime.datetime(...), + u'text': u'and pretty easy too!', + u'title': u'MongoDB is fun'} Just like we did with :meth:`~pymongo.collection.Collection.find_one`, we can pass a document to :meth:`~pymongo.collection.Collection.find` @@ -296,10 +321,18 @@ author is "Mike": .. doctest:: >>> for post in posts.find({"author": "Mike"}): - ... post + ... pprint.pprint(post) ... - {u'date': datetime.datetime(...), u'text': u'My first blog post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'mongodb', u'python', u'pymongo']} - {u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'bulk', u'insert']} + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'mongodb', u'python', u'pymongo'], + u'text': u'My first blog post!'} + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'bulk', u'insert'], + u'text': u'Another post!'} Counting -------- @@ -331,10 +364,18 @@ than a certain date, but also sort the results by author: >>> d = datetime.datetime(2009, 11, 12, 12) >>> for post in posts.find({"date": {"$lt": d}}).sort("author"): - ... print post + ... pprint.pprint(post) ... - {u'date': datetime.datetime(2009, 11, 10, 10, 45), u'text': u'and pretty easy too!', u'_id': ObjectId('...'), u'author': u'Eliot', u'title': u'MongoDB is fun'} - {u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!', u'_id': ObjectId('...'), u'author': u'Mike', u'tags': [u'bulk', u'insert']} + {u'_id': ObjectId('...'), + u'author': u'Eliot', + u'date': datetime.datetime(...), + u'text': u'and pretty easy too!', + u'title': u'MongoDB is fun'} + {u'_id': ObjectId('...'), + u'author': u'Mike', + u'date': datetime.datetime(...), + u'tags': [u'bulk', u'insert'], + u'text': u'Another post!'} Here we use the special ``"$lt"`` operator to do a range query, and also call :meth:`~pymongo.cursor.Cursor.sort` to sort the results @@ -355,8 +396,8 @@ First, we'll need to create the index: >>> result = db.profiles.create_index([('user_id', pymongo.ASCENDING)], ... unique=True) - >>> list(db.profiles.index_information()) - [u'user_id_1', u'_id_'] + >>> sorted(list(db.profiles.index_information())) + [u'_id_', u'user_id_1'] Notice that we have two indexes now: one is the index on ``_id`` that MongoDB creates automatically, and the other is the index on ``user_id`` we just diff --git a/pymongo/collection.py b/pymongo/collection.py index 0074b1a6d..88ceb80d7 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -645,12 +645,13 @@ class Collection(common.BaseObject): >>> db.test.insert({"x": "y", "a": "b"}) ObjectId('...') - >>> list(db.test.find()) - [{u'a': u'b', u'x': u'y', u'_id': ObjectId('...')}] + >>> import pprint + >>> pprint.pprint(list(db.test.find())) + [{u'_id': ObjectId('...'), u'a': u'b', u'x': u'y'}] >>> db.test.update({"x": "y"}, {"$set": {"a": "c"}}) {...} - >>> list(db.test.find()) - [{u'a': u'c', u'x': u'y', u'_id': ObjectId('...')}] + >>> pprint.pprint(list(db.test.find())) + [{u'_id': ObjectId('...'), u'a': u'c', u'x': u'y'}] :Parameters: - `spec`: a ``dict`` or :class:`~bson.son.SON` instance diff --git a/setup.py b/setup.py index 877cdd5c8..765645cec 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ import glob import os import platform import re -import subprocess import sys import warnings @@ -22,17 +21,24 @@ except ImportError: # we have to. try: from setuptools import setup + from setuptools.command.build_py import build_py except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup + from setuptools.command.build_py import build_py -from distutils.cmd import Command from distutils.command.build_ext import build_ext from distutils.errors import CCompilerError from distutils.errors import DistutilsPlatformError, DistutilsExecError from distutils.core import Extension +try: + import sphinx + _HAVE_SPHINX = True +except ImportError: + _HAVE_SPHINX = False + version = "2.9.4.dev0" f = open("README.rst") @@ -88,22 +94,70 @@ if "test" in sys.argv or "nosetests" in sys.argv: should_run_tests = True -class doc(Command): +class doc(build_py): description = "generate or test documentation" - user_options = [("test", "t", - "run doctests instead of generating documentation")] + build_py.user_options.append( + ("test", "t", "run doctests instead of generating documentation")) - boolean_options = ["test"] + build_py.boolean_options.append('test') def initialize_options(self): self.test = False - - def finalize_options(self): - pass + build_py.initialize_options(self) def run(self): + if not _HAVE_SPHINX: + raise RuntimeError( + "You must install Sphinx to build or test the documentation.") + + if PY3: + import doctest + from doctest import OutputChecker as _OutputChecker + + # Match u or U (possibly followed by r or R), removing it. + # r/R can follow u/U but not precede it. Don't match the + # single character string 'u' or 'U'. + _u_literal_re = re.compile( + r"(\W|^)(?= 3: + # The docstrings are written with python 2.x in mind. + # To make the doctests pass in python 3 we have to + # strip the 'u' prefix from the expected results. The + # actual results won't have that prefix. + want = re.sub(_u_literal_re, r'\1\2', want) + # We also have to strip the 'b' prefix from the actual + # results since python 2.x expected results won't have + # that prefix. + got = re.sub(_b_literal_re, r'\1\2', got) + return super( + _StringPrefixFixer, self).check_output( + want, got, optionflags) + + def output_difference(self, example, got, optionflags): + if sys.version_info[0] >= 3: + example.want = re.sub( + _u_literal_re, r'\1\2', example.want) + got = re.sub(_b_literal_re, r'\1\2', got) + return super( + _StringPrefixFixer, self).output_difference( + example, got, optionflags) + + doctest.OutputChecker = _StringPrefixFixer + + # No need to run build_py for python 2.x. + build_py.run(self) + if self.test: path = "doc/_build/doctest" mode = "doctest" @@ -116,8 +170,7 @@ class doc(Command): except: pass - status = subprocess.call(["sphinx-build", "-E", - "-b", mode, "doc", path]) + status = sphinx.build_main(["-E", "-b", mode, "doc", path]) if status: raise RuntimeError("documentation step '%s' failed" % (mode,))