Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Import from 0.3.1.

  • Loading branch information...
commit d390b8f4485c770da84cb4212da8af9f85a53f7b 1 parent 7159409
@JNRowe authored
View
478 ISBN.py 100755 → 100644
@@ -1,9 +1,7 @@
#! /usr/bin/python -tt
# vim: set sw=4 sts=4 et tw=80 fileencoding=utf-8:
#
-"""
-ISBN - A module for working with 10- and 13-digit ISBNs
-"""
+"""ISBN - A module for working with 10- and 13-digit ISBNs"""
# Copyright (C) 2007 James Rowe
#
# This program is free software: you can redistribute it and/or modify
@@ -20,8 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-__version__ = "0.3.0"
-__date__ = "2007-05-18"
+__version__ = "0.3.1"
+__date__ = "2008-05-20"
__author__ = "James Rowe <jnrowe@ukfsn.org>"
__copyright__ = "Copyright (C) 2007 James Rowe"
__license__ = "GNU General Public License Version 3"
@@ -30,45 +28,52 @@
from email.utils import parseaddr
-__doc__ += """
+__doc__ += """.
+
This module supports the calculation of ISBN checksums with
-C{calculate_checksum()}, the conversion between ISBN-10 and ISBN-13 with
-C{convert()} and the validation of ISBNs with C{validate()}.
-
-All the ISBNs must be passed in as C{str} types, even if it would seem
-reasonable to accept some C{int} forms. The reason behind this is English
-speaking countries use C{0} for their group identifier, and Python would treat
-ISBNs beginning with C{0} as octal representations producing incorrect results.
-While it may be feasible to allow some cases as non-C{str} types the complexity
+``calculate_checksum()``, the conversion between ISBN-10 and ISBN-13 with
+``convert()`` and the validation of ISBNs with ``validate()``.
+
+All the ISBNs must be passed in as ``str`` types, even if it would seem
+reasonable to accept some ``int`` forms. The reason behind this is English
+speaking countries use ``0`` for their group identifier, and Python would treat
+ISBNs beginning with ``0`` as octal representations producing incorrect results.
+While it may be feasible to allow some cases as non-``str`` types the complexity
in design and usage isn't worth the minimal benefit.
The functions in this module also support 9-digit SBNs for people with older
books in their collection.
-@version: %s
-@author: U{%s <mailto:%s>}
-@copyright: %s
-@status: WIP
-@license: %s
+:version: %s
+:author: `%s <mailto:%s>`__
+:copyright: %s
+:status: WIP
+:license: %s
""" % ((__version__, ) + parseaddr(__author__) + (__copyright__, __license__))
-class ISBN(object):
- """
- Class for representing ISBN objects
- @ivar _isbn: Possibly formatted ISBN string
- @ivar isbn: Code only ISBN string
+class Isbn(object):
+ """Class for representing ISBN objects
+
+ :Ivariables:
+ _isbn : `str`
+ Possibly formatted ISBN string
+ isbn : `str`
+ Code only ISBN string
+
"""
__slots__ = ('_isbn', 'isbn')
def __init__(self, isbn):
- """
- Initialise a new C{ISBN} object
+ """Initialise a new `Isbn` object
+
+ :Parameters:
+ isbn : `str`
+ ISBN string
- @type isbn: C{str}
- @param isbn: ISBN string
"""
+ super(Isbn, self).__init__()
self._isbn = isbn
if len(isbn) in (9, 12):
self.isbn = _isbn_cleanse(isbn, False)
@@ -76,46 +81,46 @@ def __init__(self, isbn):
self.isbn = _isbn_cleanse(isbn)
def __repr__(self):
- """
- Self-documenting string representation
+ """Self-documenting string representation
- >>> ISBN("9780521871723")
- ISBN('9780521871723')
- >>> ISBN("3540009787")
- ISBN('3540009787')
+ >>> Isbn("9780521871723")
+ Isbn('9780521871723')
+ >>> Isbn("3540009787")
+ Isbn('3540009787')
+
+ :rtype: `str`
+ :return: String to recreate `Isbn` object
- @rtype: C{str}
- @return: String to recreate C{ISBN} object
"""
return "%s(%r)" % (self.__class__.__name__, self.isbn)
def __str__(self):
- """
- Pretty printed ISBN string
+ """Pretty printed ISBN string
- >>> print ISBN("9780521871723")
- 9780521871723
- >>> print ISBN("978-052-187-1723")
- 978-052-187-1723
- >>> print ISBN("3540009787")
- 3540009787
+ >>> print(Isbn("9780521871723"))
+ ISBN 9780521871723
+ >>> print(Isbn("978-052-187-1723"))
+ ISBN 978-052-187-1723
+ >>> print(Isbn("3540009787"))
+ ISBN 3540009787
+
+ :rtype: `str`
+ :return: Human readable string representation of `Isbn` object
- @rtype: C{str}
- @return: Human readable string representation of C{ISBN} object
"""
- return self._isbn
+ return "ISBN %s" % self._isbn
def calculate_checksum(self):
- """
- Calculate ISBN checksum
+ """Calculate ISBN checksum
- >>> ISBN("978-052-187-1723").calculate_checksum()
+ >>> Isbn("978-052-187-1723").calculate_checksum()
'3'
- >>> ISBN("3540009787").calculate_checksum()
+ >>> Isbn("3540009787").calculate_checksum()
'7'
- @rtype: C{str}
- @return: ISBN checksum value
+ :rtype: `str`
+ :return: ISBN checksum value
+
"""
if len(self.isbn) in (9, 12):
return calculate_checksum(self.isbn)
@@ -123,136 +128,226 @@ def calculate_checksum(self):
return calculate_checksum(self.isbn[:-1])
def convert(self, code="978"):
- """
- Convert ISBNs between ISBN-10 and ISBN-13
+ """Convert ISBNs between ISBN-10 and ISBN-13
- >>> ISBN("0071148167").convert()
+ >>> Isbn("0071148167").convert()
'9780071148160'
- >>> ISBN("9780071148160").convert()
+ >>> Isbn("9780071148160").convert()
'0071148167'
- @type code: C{str}
- @param code: ISBN-13 prefix code
- @rtype: C{str}
- @return: Converted ISBN
+ :Parameters:
+ code : `str`
+ ISBN-13 prefix code
+ :rtype: `str`
+ :return: Converted ISBN
+
"""
return convert(self.isbn, code)
def validate(self):
- """
- Validate an ISBN value
+ """Validate an ISBN value
- >>> ISBN("978-052-187-1723").validate()
+ >>> Isbn("978-052-187-1723").validate()
True
- >>> ISBN("978-052-187-1720").validate()
+ >>> Isbn("978-052-187-1720").validate()
False
- >>> ISBN("3540009787").validate()
+ >>> Isbn("3540009787").validate()
True
- >>> ISBN("354000978x").validate()
+ >>> Isbn("354000978x").validate()
False
- @rtype: C{bool}
- @return: True if ISBN is valid
+ :rtype: `bool`
+ :return: True if ISBN is valid
+
"""
return validate(self.isbn)
-class ISBN10(ISBN):
- """
- Class for representing ISBN-10 objects
+ def to_url(self, site="amazon"):
+ """Generate a link to an online book site
+
+ >>> print Isbn("0071148167").to_url()
+ http://amazon.com/dp/0071148167
+
+ :Parameters:
+ site : `str`
+ Site to create link to
+ :rtype: `str`
+ :return: URL on `site` for book
+ :raise ValueError: Unknown site value
+
+ """
+ if site == "amazon":
+ return "http://amazon.com/dp/%s" % self.isbn
+ else:
+ raise ValueError("Unknown site `%s'." % site)
+
+ def to_urn(self):
+ """Generate a RFC 3187 URN
+
+ `RFC 3187 <http://tools.ietf.org/html/rfc3187>`__ is the accepted way to
+ use ISBNs as uniform resource names.
+
+ >>> print Isbn("0071148167").to_urn()
+ URN:ISBN:0071148167
+
+ :rtype: `str`
+ :return: RFC 3187 URN
+
+ """
+ return "URN:ISBN:%s" % self._isbn
+
+
+class Isbn10(Isbn):
+ """Class for representing ISBN-10 objects
+
+ :see: `Isbn`
- @see: C{ISBN}
"""
def __init__(self, isbn):
- """
- Initialise a new C{ISBN10} object
+ """Initialise a new `Isbn10` object
+
+ :Parameters:
+ isbn : `str`
+ ISBN-10 string
- @type isbn: C{str}
- @param isbn: ISBN-10 string
"""
- if len(isbn) == 9:
- self._isbn = isbn
- self.isbn = _isbn_cleanse(isbn, False)
- else:
- self.isbn = _isbn_cleanse(isbn)
+ super(Isbn10, self).__init__(isbn)
def calculate_checksum(self):
- """
- Calculate ISBN-10 checksum
+ """Calculate ISBN-10 checksum
- >>> ISBN10("3540009787").calculate_checksum()
+ >>> Isbn10("3540009787").calculate_checksum()
'7'
- @rtype: C{str}
- @return: ISBN-10 checksum value
+ :rtype: `str`
+ :return: ISBN-10 checksum value
+
"""
- if len(self.isbn) == 9:
- return calculate_checksum(self.isbn)
- else:
- return calculate_checksum(self.isbn[:-1])
+ return calculate_checksum(self.isbn[:9])
def convert(self, code="978"):
- """
- Convert ISBN-10 to ISBN-13
+ """Convert ISBN-10 to ISBN-13
- >>> ISBN10("0071148167").convert()
+ >>> Isbn10("0071148167").convert()
'9780071148160'
- @type code: C{str}
- @param code: ISBN-13 prefix code
- @rtype: C{str}
- @return: ISBN-13 string
+ :Parameters:
+ code : `str`
+ ISBN-13 prefix code
+ :rtype: `str`
+ :return: ISBN-13 string
+
"""
return convert(self.isbn, code)
-class ISBN13(ISBN):
- """
- Class for representing ISBN-13 objects
- @see: C{ISBN}
+class Sbn(Isbn10):
+ """Class for representing SBN objects
+
+ :see: `Isbn10`
+
"""
- def __init__(self, isbn):
+ def __init__(self, sbn):
+ """Initialise a new `Sbn` object
+
+ :Parameters:
+ sbn : `str`
+ SBN string
+
"""
- Initialise a new C{ISBN13} object
+ isbn = "0" + sbn
+ super(Sbn, self).__init__(isbn)
+
+ def __repr__(self):
+ """Self-documenting string representation
+
+ >>> Sbn("521871723")
+ Sbn('521871723')
+
+ :rtype: `str`
+ :return: String to recreate `Sbn` object
- @type isbn: C{str}
- @param isbn: ISBN-13 string
"""
- if len(isbn) == 12:
- self._isbn = isbn
- self.isbn = _isbn_cleanse(isbn, False)
- else:
- self.isbn = _isbn_cleanse(isbn)
+ return "Sbn(%r)" % self.isbn[1:]
def calculate_checksum(self):
+ """Calculate SBN checksum
+
+ >>> Sbn("07114816").calculate_checksum()
+ '7'
+ >>> Sbn("071148167").calculate_checksum()
+ '7'
+
+ :rtype: `str`
+ :return: SBN checksum value
+
"""
- Calculate ISBN-13 checksum
+ return calculate_checksum(self.isbn[:9])
- >>> ISBN13("978-052-187-1723").calculate_checksum()
- '3'
+ def convert(self, code="978"):
+ """Convert SBN to ISBN-13
+
+ :see: `Isbn10.convert`
+
+ >>> Sbn("071148167").convert()
+ '9780071148160'
+
+ :Parameters:
+ code : `str`
+ ISBN-13 prefix code
+ :rtype: `str`
+ :return: ISBN-13 string
- @rtype: C{str}
- @return: ISBN-13 checksum value
"""
- if len(self.isbn) == 12:
- return calculate_checksum(self.isbn)
- else:
- return calculate_checksum(self.isbn[:-1])
+ return super(Sbn, self).convert(code)
+
+
+class Isbn13(Isbn):
+ """Class for representing ISBN-13 objects
+
+ :see: `Isbn`
+
+ """
+ def __init__(self, isbn):
+ """Initialise a new `Isbn13` object
+
+ :Parameters:
+ isbn : `str`
+ ISBN-13 string
+
+ """
+ super(Isbn13, self).__init__(isbn)
+
+ def calculate_checksum(self):
+ """Calculate ISBN-13 checksum
+
+ >>> Isbn13("978-052-187-1723").calculate_checksum()
+ '3'
+
+ :rtype: `str`
+ :return: ISBN-13 checksum value
- def convert(self):
"""
- Convert ISBN-13 to ISBN-10
+ return calculate_checksum(self.isbn[:12])
- >>> ISBN13("9780071148160").convert()
+ def convert(self, code=None):
+ """Convert ISBN-13 to ISBN-10
+
+ >>> Isbn13("9780071148160").convert()
'0071148167'
- @rtype: C{str}
- @return: ISBN-10 string
+ :Parameters:
+ code : Any
+ Ignored, only for compatibility with `Isbn`
+ :rtype: `str`
+ :return: ISBN-10 string
+
"""
return convert(self.isbn)
+
def _isbn_cleanse(isbn, checksum=True):
- """
- Check ISBN is a string, and passes basic sanity checks
+ """Check ISBN is a string, and passes basic sanity checks
>>> for isbn in test_isbns.values():
... if isbn.startswith("0"):
@@ -286,77 +381,85 @@ def _isbn_cleanse(isbn, checksum=True):
>>> _isbn_cleanse("012345678-b")
Traceback (most recent call last):
...
- ValueError: Invalid ISBN string(non-digit or X checksum)
-
- @type isbn: C{str}
- @param isbn: SBN, ISBN-10 or ISBN-13
- @type checksum: C{bool}
- @param checksum: True if C{isbn} includes checksum character
- @rtype: C{str}
- @return: ISBN with hyphenation removed
- @raise TypeError: C{isbn} is not a C{str} type
- @raise ValueError: Incorrect length for C{isbn}
- @raise ValueError: Incorrect SBN or ISBN formatting
+ ValueError: Invalid ISBN-10 string(non-digit or X checksum)
+ >>> _isbn_cleanse("012345678901b")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid ISBN-13 string(non-digit checksum)
+
+ :Parameters:
+ isbn : `str`
+ SBN, ISBN-10 or ISBN-13
+ checksum : `bool`
+ True if `isbn` includes checksum character
+ :rtype: `str`
+ :return: ISBN with hyphenation removed, including when called with a SBN
+ :raise TypeError: `isbn` is not a `str` type
+ :raise ValueError: Incorrect length for `isbn`
+ :raise ValueError: Incorrect SBN or ISBN formatting
+
"""
- if isinstance(isbn, basestring):
+ try:
isbn = isbn.replace("-", "")
- else:
+ except AttributeError:
raise TypeError("ISBN must be a string `%s'" % isbn)
if not isbn[:-1].isdigit():
raise ValueError("Invalid ISBN string(non-digit parts)")
if checksum:
- if not (isbn[-1].isdigit() or isbn[-1] in "Xx"):
- raise ValueError("Invalid ISBN string(non-digit or X checksum)")
if len(isbn) == 9:
isbn = "0" + isbn
- if not len(isbn) in (10, 13):
+ if len(isbn) == 10:
+ if not (isbn[-1].isdigit() or isbn[-1] in "Xx"):
+ raise ValueError("Invalid ISBN-10 string(non-digit or X checksum)")
+ elif len(isbn) == 13:
+ if not isbn[-1].isdigit():
+ raise ValueError("Invalid ISBN-13 string(non-digit checksum)")
+ else:
raise ValueError("ISBN must be either 10 or 13 characters long")
else:
- if not isbn[-1].isdigit():
- raise ValueError("Invalid ISBN string(non-digit parts)")
if len(isbn) == 8:
isbn = "0" + isbn
+ if not isbn[-1].isdigit():
+ raise ValueError("Invalid ISBN string(non-digit parts)")
if not len(isbn) in (9, 12):
raise ValueError("ISBN must be either 9 or 12 characters long "
"without checksum")
return isbn
def calculate_checksum(isbn):
- """
- Calculate ISBN checksum
+ """Calculate ISBN checksum
>>> for isbn in test_isbns.values():
... if not calculate_checksum(isbn[:-1]) == isbn[-1]:
... print("ISBN checksum failure `%s'" % isbn)
- @type isbn: C{str}
- @param isbn: SBN, ISBN-10 or ISBN-13
- @rtype: C{str}
- @return: Checksum for given C{isbn}
+ :Parameters:
+ isbn : `str`
+ SBN, ISBN-10 or ISBN-13
+ :rtype: `str`
+ :return: Checksum for given ISBN or SBN
+
"""
isbn = [int(i) for i in _isbn_cleanse(isbn, checksum=False)]
if len(isbn) == 9:
- products = [isbn[i] * (10 - i) for i in range(9)]
- remainder = sum(products) % 11
- check = 11 - remainder
+ products = [isbn[n] * i for n, i in enumerate(range(1, 10))]
+ check = sum(products) % 11
if check == 10:
check = "X"
elif check == 11:
check = 0
else:
products = [(isbn[i] if i % 2 == 0 else isbn[i] * 3) for i in range(12)]
- remainder = sum(products) % 10
- check = 10 - remainder
+ check = 10 - sum(products) % 10
if check == 10:
check = 0
return str(check)
def convert(isbn, code="978"):
- """
- Convert ISBNs between ISBN-10 and ISBN-13
+ """Convert ISBNs between ISBN-10 and ISBN-13
No attempt to hyphenate converted ISBNs is made, because the specification
- requires that I{any} hyphenation must be correct but allows ISBNs without
+ requires that *any* hyphenation must be correct but allows ISBNs without
hyphenation.
>>> for isbn in test_isbns.values():
@@ -365,52 +468,59 @@ def convert(isbn, code="978"):
>>> convert("0000000000000")
Traceback (most recent call last):
...
- ValueError: `000' is not a Bookland code
-
- @type isbn: C{str}
- @param isbn: SBN, ISBN-10 or ISBN-13
- @type code: C{str}
- @param code: EAN Bookland code
- @rtype: C{str}
- @return: Converted ISBN-10 or ISBN-13
- @raise ValueError: When ISBN-13 isn't a Bookland ISBN
+ ValueError: Only ISBN-13s with 978 Bookland code can be converted to ISBN-10.
+
+ :Parameters:
+ isbn : `str`
+ SBN, ISBN-10 or ISBN-13
+ code : `str`
+ EAN Bookland code
+ :rtype: `str`
+ :return: Converted ISBN-10 or ISBN-13
+ :raise ValueError: When ISBN-13 isn't a Bookland `978' ISBN
+
"""
isbn = _isbn_cleanse(isbn)
if len(isbn) == 10:
isbn = code + isbn[:-1]
return isbn + calculate_checksum(isbn)
else:
- if isbn.startswith(("978", "979")):
+ if isbn.startswith("978"):
return isbn[3:-1] + calculate_checksum(isbn[3:-1])
else:
- raise ValueError("`%s' is not a Bookland code" % isbn[:3])
+ raise ValueError("Only ISBN-13s with 978 Bookland code can be "
+ "converted to ISBN-10.")
def validate(isbn):
- """
- Validate ISBNs
+ """Validate ISBNs
- @warn: Publishers have been known to go to press with broken ISBNs, and
- therefore validation failures do not completely guarantee an ISBN is
- incorrectly entered. It should however be noted that it is massively more
- likely I{you} have entered an invalid ISBN than the published ISBN is
- incorrectly produced. An example of this probability in the real world is
- that U{Amazon <http://www.amazon.com/>} consider it so unlikely that they
- refuse to search for invalid published ISBNs.
+ :warn: Publishers have been known to go to press with broken ISBNs, and
+ therefore validation failures do not completely guarantee an ISBN is
+ incorrectly entered. It should however be noted that it is massively
+ more likely *you* have entered an invalid ISBN than the published ISBN
+ is incorrectly produced. An example of this probability in the real
+ world is that `Amazon <http://www.amazon.com/>`__ consider it so
+ unlikely that they refuse to search for invalid published ISBNs.
Valid ISBNs
+
>>> for isbn in test_isbns.values():
... if not validate(isbn):
... print("ISBN validation failure `%s'" % isbn)
Invalid ISBNs
+
>>> for isbn in ("1-234-56789-0", "2-345-6789-1", "3-456-7890-X"):
... if validate(isbn):
... print("ISBN invalidation failure `%s'" % isbn)
- @type isbn: C{str}
- @param isbn: SBN, ISBN-10 or ISBN-13
- @rtype: C{bool}
- @return: C{True} if ISBN is valid
+ :Parameters:
+ isbn : `str`
+ SBN, ISBN-10 or ISBN-13
+ :rtype: `bool`
+ :return: `True` if ISBN is valid
+
"""
isbn = _isbn_cleanse(isbn)
- return isbn[-1] == calculate_checksum(isbn[:-1])
+ return isbn[-1].upper() == calculate_checksum(isbn[:-1])
+
View
8 NEWS
@@ -4,6 +4,14 @@
User-visible changes
--------------------
+.. contents::
+
+0.3.1 - 2008-05-20
+------------------
+
+ * Fixed installs with easy_install
+ * SBNs can now be represented with the ``SBN`` object
+
0.3.0 - 2007-12-11
------------------
View
50 README
@@ -22,9 +22,9 @@ have installed, drop me a mail_ and I'll endeavour to fix it.
The module have been tested on many UNIX-like systems, including Linux,
Solaris and OS X, but it should work fine on other systems too. The
-module contains a large collection of ``doctest`` tests that will be run
-if executed directly or with Python's ``-m`` switch. You can also run
-the testsuite by executing ``make check`` in the source tree.
+module contains a large collection of ``doctest`` tests that can be run
+with ``./setup.py test_code``. The examples in this file can be tested
+with ``./setup.py test_doc``.
.. [#] The module may work with older Python versions, but it has only
been tested with v2.5.
@@ -43,21 +43,63 @@ goes:
True
>>> ISBN.convert(Permutation_City)
'9781857982183'
+ >>> print("ISBN %s" % Permutation_City)
+ ISBN 1-85798-218-5
or to process ISBNs using the object pattern use:
.. code-block:: pycon
- >>> Permutation_City = ISBN.ISBN10("1-85798-218-5")
+ >>> Permutation_City = ISBN.Isbn10("1-85798-218-5")
>>> Permutation_City.validate()
True
>>> Permutation_City.convert()
'9781857982183'
+ >>> print(Permutation_City)
+ ISBN 1-85798-218-5
All independent functions contain hopefully useful docstrings with
``doctest``-based examples. The ``html/`` directory contains the
preprocessed epydoc_ output for reference.
+API Stability
+-------------
+
+API stability isn't guaranteed across versions, although frivolous
+changes won't be made.
+
+When ``pyisbn`` 1.0 is released the API will be frozen, and any
+changes which aren't backwards compatible will force a major version
+bump.
+
+Hacking
+-------
+
+Patches are most welcome, but I'd appreciate it if you could follow the
+guidelines below to make it easier to integrate your changes. These are
+guidelines however, and as such can be broken if the need arises or you
+just want to convince me that your style is better.
+
+ * `PEP 8`_, the style guide, should be followed where possible.
+ * While support for Python versions prior to v2.5 may be added in the
+ future if such a need were to arise, you are encouraged to use v2.5
+ features now.
+ * All new classes and methods should be accompanied by new
+ ``doctest`` examples, and epydoc_'s epytext formatted descriptions if
+ at all possible.
+ * Tests *must not* span network boundaries, see ``test.mock`` for
+ workarounds.
+ * ``doctest`` tests in modules are only for unit testing in general,
+ and should not rely on any modules that aren't in Python's standard
+ library.
+ * Functional tests should be in the ``doc`` directory in
+ reStructuredText_ formatted files, with actual tests in ``doctest``
+ blocks. Functional tests can depend on external modules, but they
+ must be Open Source.
+
+New examples for the ``doc`` directory are as appreciated as code
+changes.
+
Bugs
----
View
44 __pkg_data__.py
@@ -0,0 +1,44 @@
+#
+# vim: set sw=4 sts=4 et tw=80 fileencoding=utf-8:
+#
+"""Per-package configuration data"""
+# Copyright (C) 2008 James Rowe
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import ISBN
+MODULE = ISBN
+
+SCRIPTS = []
+
+DESCRIPTION = ISBN.__doc__.splitlines()[0][:-1]
+LONG_DESCRIPTION = "\n\n".join(ISBN.__doc__.split("\n\n")[1:4])
+
+KEYWORDS = ['ISBN', 'ISBN-10', 'ISBN-13', 'SBN']
+CLASSIFIERS = [
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Other Audience',
+ 'License :: OSI Approved :: GNU General Public License (GPL)',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Other/Nonlisted Topic',
+ 'Topic :: Text Processing :: Indexing',
+]
+
+OBSOLETES = []
+
+GRAPH_TYPE = None
+
+TEST_EXTRAGLOBS = []
View
488 setup.py
@@ -1,8 +1,8 @@
#! /usr/bin/python -tt
# vim: set sw=4 sts=4 et tw=80 fileencoding=utf-8:
#
-"""ISBN - A module for working with 10- and 13-digit ISBNs"""
-# Copyright (C) 2007 James Rowe
+"""setup - Generic project setup.py"""
+# Copyright (C) 2007-2008 James Rowe
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,123 +22,197 @@
import os
import shutil
import sys
+import time
+
+try:
+ from setuptools import setup
+ from setuptools.command.sdist import (finders, sdist)
+ from setuptools import Command
+ from distutils.util import convert_path
+ SETUPTOOLS = True #: True if ``setuptools`` is installed
+except ImportError:
+ from distutils.core import setup
+ from distutils.command.sdist import sdist
+ from distutils.cmd import Command
+ SETUPTOOLS = False
from distutils.archive_util import make_archive
from distutils.command.clean import clean
-from distutils.command.sdist import sdist
-from distutils.cmd import Command
-from distutils.core import setup
from distutils.dep_util import newer
-from distutils.errors import DistutilsModuleError
+from distutils.errors import (DistutilsFileError, DistutilsModuleError)
from distutils.file_util import write_file
from distutils.util import execute
from email.utils import parseaddr
from glob import glob
-from re import sub
-from subprocess import check_call
-from time import strftime
+from subprocess import (check_call, PIPE, Popen)
try:
- from docutils.core import publish_cmdline, default_description
+ from docutils.core import publish_cmdline
from docutils import nodes
from docutils.parsers.rst import directives
- DOCUTILS = True
+ DOCUTILS = True #: True if ``docutils`` module is available
except ImportError:
DOCUTILS = False
try:
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
- PYGMENTS = True
+ PYGMENTS = True #: True if ``pygments`` module is available
except ImportError:
PYGMENTS = False
try:
from epydoc import cli
- EPYDOC = True
+ EPYDOC = True #: True if ``epydoc`` module is available
except ImportError:
EPYDOC = False
-try:
- from mercurial import hg
- MERCURIAL = True
-except ImportError:
- MERCURIAL = False
-import ISBN
-from test_isbns import test_isbns
+import __pkg_data__
+import test
-BASE_URL = "http://www.jnrowe.ukfsn.org/"
+BASE_URL = "http://www.jnrowe.ukfsn.org/" #: Base URL for links
+PROJECT_URL = "%sprojects/%s.html" % (BASE_URL, __pkg_data__.MODULE.__name__)
-from sys import version_info
-if version_info < (2, 5, 0, 'final'):
+if sys.version_info < (2, 5, 0, 'final'):
raise SystemError("Requires Python v2.5+")
+#{ Generated data file functions
+
def write_changelog(filename):
- """
- Generate a ChangeLog from Mercurial repo
+ """Generate a ChangeLog from Mercurial repo
+
+ :Parameters:
+ filename : `str`
+ Filename to write ChangeLog to
- @type filename: C{str}
- @param filename: Filename to write ChangeLog to
"""
if os.path.isdir(".hg"):
- check_call(["hg", "log", "--exclude", ".be/", "--no-merges",
- "--style", "changelog"],
- stdout=open(filename, "w"))
+ print('Building ChangeLog from Mercurial repository')
+ try:
+ call_hg(["log", "--exclude", ".be/", "--no-merges",
+ "--style", "changelog"],
+ stdout=open(filename, "w"))
+ finally:
+ # Remove the ChangeLog if call_hg() failed
+ if os.stat(filename).st_size == 0:
+ os.unlink(filename)
else:
print("Unable to build ChangeLog, dir is not a Mercurial clone")
return False
-def gen_desc(doc):
+def write_manifest(files):
+ """Generate a MANIFEST file
+
+ :Parameters:
+ files : `list`
+ Filenames to include in MANIFEST
+
"""
- Pull simple description from docstring
+ manifest = open("MANIFEST", "w")
+ manifest.write("\n".join(sorted(files)) + "\n")
+ manifest.close()
+
+#}
+
+#{ Implementation utilities
+
+def call_hg(options, *args, **kwargs):
+ """Call Mercurial command line tools
+
+ :Parameters:
+ options : `list`
+ Mercurial command options
+ *args : `list`
+ Positional arguments for ``subprocess.Popen``
+ **kwargs : `dict`
+ Keyword arguments for ``subprocess.Popen``
+ :rtype: `str`
+ :return: Mercurial command output
+ :raise OSError: `hg` command not found
- @type doc: C{str}
- @param doc: Docstring to manipulate
- @rtype: C{str}
- @return: description string suitable for C{Command} class's description
"""
- desc = doc.splitlines()[1].lstrip()
+ if not "stdout" in kwargs:
+ kwargs["stdout"] = PIPE
+ options.insert(0, "hg")
+ try:
+ process = Popen(options, *args, **kwargs)
+ except OSError, e:
+ print("Error calling `hg`, is Mercurial installed? [%s]" % e)
+ sys.exit(1)
+ process.wait()
+ if not process.returncode == 0:
+ print("`hg' completed with %i return code" % process.returncode)
+ sys.exit(process.returncode)
+ return process.communicate()[0]
+
+def gen_desc(doc):
+ """Pull simple description from docstring
+
+ :Parameters:
+ doc : `str`
+ Docstring to manipulate
+ :rtype: str
+ :return: Description string suitable for ``Command`` class's description
+
+ """
+ desc = doc.splitlines()[0].lstrip()
return desc[0].lower() + desc[1:]
+
class NoOptsCommand(Command):
+ """Abstract class for simple ``distutils`` command implementation"""
+
def initialize_options(self):
+ """Set default values for options"""
pass
def finalize_options(self):
+ """Finalize, and test validity, of options"""
pass
+#}
+
+
class BuildDoc(NoOptsCommand):
- """
- Build project documentation
+ """Build project documentation
+
+ :Ivariables:
+ force
+ Force documentation generation
- @ivar force: Force documentation generation
"""
description = gen_desc(__doc__)
user_options = [
('force', 'f',
- "Force documentation generation"),
- ]
- boolean_options = ['force']
+ "force documentation generation"),
+ ] #: `BuildDoc`'s option mapping
+ boolean_options = ['force'] #: `BuildDoc` class' boolean options
def initialize_options(self):
+ """Set default values for options"""
self.force = False
def run(self):
+ """Build the required documentation"""
if not DOCUTILS:
raise DistutilsModuleError("docutils import failed, "
- "skipping documentation generation")
+ "can't generate documentation")
if not PYGMENTS:
- raise DistutilsModuleError("docutils import failed, "
- "skipping documentation generation")
+ # This could be a warning with conditional support for users, but
+ # how would coloured output be guaranteed in releases?
+ raise DistutilsModuleError("pygments import failed, "
+ "can't generate documentation")
pygments_formatter = HtmlFormatter()
def pygments_directive(name, arguments, options, content, lineno,
content_offset, block_text, state,
state_machine):
+ """Code colourising directive for ``docutils``"""
try:
lexer = get_lexer_by_name(arguments[0])
except ValueError:
- # no lexer found - use the text one instead of an exception
+ # no lexer found - use the text one instead of raising an
+ # exception
lexer = get_lexer_by_name('text')
parsed = highlight(u'\n'.join(content), lexer, pygments_formatter)
return [nodes.raw('', parsed, format='html')]
@@ -146,7 +220,7 @@ def pygments_directive(name, arguments, options, content, lineno,
pygments_directive.content = 1
directives.register_directive('code-block', pygments_directive)
- for source in sorted(["NEWS", "README"]):
+ for source in sorted(["NEWS", "README"] + glob('doc/*.txt')):
dest = os.path.splitext(source)[0] + '.html'
if self.force or newer(source, dest):
print('Building file %s' % dest)
@@ -161,184 +235,306 @@ def pygments_directive(name, arguments, options, content, lineno,
if not EPYDOC:
raise DistutilsModuleError("epydoc import failed, "
"skipping API documentation generation")
- files = ["ISBN.py", ]
- if self.force or any(newer(file, "html/index.html") for file in files):
- print('Building API documentation %s' % dest)
+ files = [__pkg_data__.MODULE.__name__, ]
+ files.extend([os.path.basename(i.__file__)
+ for i in __pkg_data__.SCRIPTS])
+ if self.force \
+ or any(newer(filename, "html/index.html") for filename in files):
+ print("Building API documentation")
if not self.dry_run:
saved_args = sys.argv[1:]
- sys.argv[1:] = files
+ sys.argv[1:] = ["--name", __pkg_data__.MODULE.__name__,
+ "--url", PROJECT_URL,
+ "--docformat", "restructuredtext",
+ "--no-sourcecode"]
+ if __pkg_data__.GRAPH_TYPE:
+ sys.argv.append("--graph=%s" % __pkg_data__.GRAPH_TYPE)
+ sys.argv.extend(files)
cli.cli()
sys.argv[1:] = saved_args
if os.path.isdir(".hg"):
- if not MERCURIAL:
- raise DistutilsModuleError("Mercurial import failed")
if self.force or not os.path.isfile("ChangeLog"):
- print('Building ChangeLog from Mercurial repository')
execute(write_changelog, ("ChangeLog", ))
else:
+ output = call_hg(["tip", "--template", "'{date}'"])
+ tip_time = float(output[1:output.find("-")])
cl_time = os.stat("ChangeLog").st_mtime
- repo = hg.repository(None)
- tip_time = repo.changelog.read(repo.lookup("tip"))[2][0]
if tip_time > cl_time:
execute(write_changelog, ("ChangeLog", ))
else:
- print("Unable to build ChangeLog, dir is not a Mercurial clone")
+ print("Unable to build ChangeLog, this directory is not a "
+ "Mercurial clone")
-class HgSdist(sdist):
+ if hasattr(__pkg_data__, "BuildDoc_run"):
+ __pkg_data__.BuildDoc_run(self.dry_run, self.force)
+
+
+#{ Distribution utilities
+
+def hg_finder(dirname, none):
+ """Find files for distribution tarball
+
+ This is only used when ``setuptools`` is imported, simply to create a valid
+ list of files to distribute. Standard setuptools only works with CVS.
+ Without this it *appears* to work, but only distributes a very small subset
+ of the package.
+
+ :see: `MySdist.get_file_list`
+
+ :Parameters:
+ dirname : `str`
+ Base directory to search for files
+ none : any
+ Just for compatibility
"""
- Create a source distribution tarball
+ # setuptools documentation says this shouldn't be a hard fail, but we won't
+ # do that as it makes builds entirely unpredictable
+ output = call_hg(["locate", ])
+ # Include all but Bugs Everywhere data from repo in tarballs
+ distributed_files = filter(lambda s: not s.startswith(".be/"),
+ output.splitlines())
+ distributed_files.extend([".hg_version", "ChangeLog"])
+ distributed_files.extend(glob("*.html"))
+ distributed_files.extend(glob("doc/*.html"))
+ for path, directory, filenames in os.walk("html"):
+ for filename in filenames:
+ distributed_files.append(os.path.join(path, filename))
+ return distributed_files
+if SETUPTOOLS:
+ finders.append((convert_path('.hg/dirstate'), hg_finder))
+
+class HgSdist(sdist):
+ """Create a source distribution tarball
+
+ :see: `sdist`
+
+ :Ivariables:
+ repo
+ Mercurial repository object
- @see: C{sdist}
- @ivar repo: Mercurial repository object
"""
description = gen_desc(__doc__)
+ user_options = [
+ ('force-build', 'b', "force build with stale version numbere"),
+ ] + sdist.user_options #: `HgSdist`'s option mapping
+ boolean_options = ['force-build']
def initialize_options(self):
+ """Set default values for options"""
sdist.initialize_options(self)
- if not MERCURIAL:
- raise DistutilsModuleError("Mercurial import failed")
- self.repo = hg.repository(None)
+ self.force_build = False
+ output = call_hg(["status", "-mard"])
+ if not len(output) == 0:
+ raise DistutilsFileError("Uncommitted changes!")
def get_file_list(self):
- changeset = self.repo.changectx()
- self.filelist.extend(changeset.manifest().keys())
- self.filelist.extend([".hg_version", "ChangeLog"])
- self.filelist.extend(glob("*.html"))
- self.filelist.extend(glob("doc/*.html"))
- for path, dir, filenames in os.walk("html"):
- for file in filenames:
- self.filelist.append(os.path.join(path, file))
+ """Generate MANIFEST file contents from Mercurial tree"""
+ output = call_hg(["locate", ])
+ # Include all but Bugs Everywhere data from repo in tarballs
+ manifest_files = filter(lambda s: not s.startswith(".be/"),
+ output.splitlines())
+ manifest_files.extend([".hg_version", "ChangeLog"])
+ manifest_files.extend(glob("*.html"))
+ manifest_files.extend(glob("doc/*.html"))
+ for path, directory, filenames in os.walk("html"):
+ for filename in filenames:
+ manifest_files.append(os.path.join(path, filename))
+ execute(write_manifest, [manifest_files], "writing MANIFEST")
sdist.get_file_list(self)
- self.filelist.sort()
def make_distribution(self):
+ """Update versioning data and build distribution"""
+ news_format = "%s - " % __pkg_data__.MODULE.__version__
+ news_matches = filter(lambda s: s.startswith(news_format), open("NEWS"))
+ if not any(news_matches):
+ print("NEWS entry for `%s' missing"
+ % __pkg_data__.MODULE.__version__)
+ sys.exit(1)
+ news_time = time.mktime(time.strptime(news_matches[0].split()[-1],
+ "%Y-%m-%d"))
+ if time.time() - news_time > 86400 and not self.force_build:
+ print("NEWS entry is older than a day, version may not have been "
+ "updated")
+ sys.exit(1)
execute(self.write_version, ())
- execute(write_changelog, ("ChangeLog", ))
sdist.make_distribution(self)
def write_version(self):
- """
- Store the current Mercurial changeset in a file
- """
- repo_id = hg.short((self.repo.lookup("tip")))
+ """Store the current Mercurial changeset in a file"""
+ # This could use `hg identify' but that output other unused information
+ output = call_hg(["tip", "--template", "'{node|short}'"])
+ repo_id = output[1:-1]
write_file(".hg_version", ("%s tip\n" % repo_id, ))
+
+class Snapshot(NoOptsCommand):
+ """Build a daily snapshot tarball"""
+ description = gen_desc(__doc__)
+ user_options = [] #: `Snapshot`'s option mapping
+
+ def run(self):
+ """Prepare and create tarball"""
+ snapshot_name = "%s-%s" % (__pkg_data__.MODULE.__name__,
+ time.strftime("%Y-%m-%d"))
+ snapshot_location = "dist/%s" % snapshot_name
+ if os.path.isdir(snapshot_location):
+ execute(shutil.rmtree, (snapshot_location, ))
+ execute(self.generate_tree, (snapshot_location, ))
+ execute(write_changelog, ("%s/ChangeLog" % snapshot_location, ))
+ execute(make_archive, (snapshot_location, "bztar", "dist",
+ snapshot_name))
+ execute(shutil.rmtree, (snapshot_location, ))
+
+ @staticmethod
+ def generate_tree(snapshot_name):
+ """Generate a clean Mercurial clone"""
+ check_call(["hg", "archive", snapshot_name])
+ shutil.rmtree("%s/.be" % snapshot_name)
+
+#}
+
+
class MyClean(clean):
- """
- Clean built and temporary files
+ """Clean built and temporary files
+
+ :see: `clean`
- @see: C{clean}
"""
description = gen_desc(__doc__)
def run(self):
+ """Remove built and temporary files"""
clean.run(self)
if self.all:
- for file in [".hg_version", "ChangeLog", "MANIFEST"] \
+ for filename in [".hg_version", "ChangeLog", "MANIFEST"] \
+ glob("*.html") + glob("doc/*.html") \
- + glob("*.pyc"):
- os.path.exists(file) and os.unlink(file)
+ + glob("%s/*.pyc" % __pkg_data__.MODULE.__name__):
+ if os.path.exists(filename):
+ os.unlink(filename)
execute(shutil.rmtree, ("html", True))
+ if hasattr(__pkg_data__, "MyClean_run"):
+ __pkg_data__.MyClean_run(self.dry_run, self.force)
-class Snapshot(NoOptsCommand):
- """
- Build a daily snapshot tarball
- """
- description = gen_desc(__doc__)
- user_options = []
- def run(self):
- snapshot_name="pyisbn-%s" % strftime("%Y-%m-%d")
- execute(shutil.rmtree, ("dist/%s" % snapshot_name, True))
- execute(self.generate_tree, ("dist/%s" % snapshot_name, ))
- execute(write_changelog, ("dist/%s/ChangeLog" % snapshot_name, ))
- execute(make_archive, ("dist/%s" % snapshot_name, "bztar", "dist",
- snapshot_name))
- execute(shutil.rmtree, ("dist/%s" % snapshot_name, ))
-
- def generate_tree(self, snapshot_name):
- """
- Generate a clean Mercurial clone
- """
- check_call(["hg", "archive", snapshot_name])
+#{ Testing utilities
class MyTest(NoOptsCommand):
+ """Abstract class for test command implementations"""
user_options = [
('exit-on-fail', 'x',
- "Exit on first failure"),
- ]
+ "exit on first failure"),
+ ] #: `MyTest`'s option mapping
boolean_options = ['exit-on-fail']
def initialize_options(self):
+ """Set default values for options"""
self.exit_on_fail = False
self.doctest_opts = doctest.REPORT_UDIFF|doctest.NORMALIZE_WHITESPACE
- self.extraglobs = {"test_isbns": test_isbns}
+ self.extraglobs = {
+ "test_isbns": test.test_isbns
+ } #: Mock objects to include for test framework
+ if hasattr(__pkg_data__, "TEST_EXTRAGLOBS"):
+ for value in __pkg_data__.TEST_EXTRAGLOBS:
+ self.extraglobs[value] = getattr(test.mock, value)
+
class TestDoc(MyTest):
- """
- Test documentation's code examples
+ """Test documentation's code examples
+
+ :see: `MyTest`
- @see: C{MyTest}
"""
description = gen_desc(__doc__)
def run(self):
- for filename in sorted(['README', ]):
- print('Testing documentation file %s' % filename)
+ """Run the documentation code examples"""
+ tot_fails = 0
+ tot_tests = 0
+ for filename in sorted(['README'] + glob("doc/*.txt")):
+ print(' Testing documentation file %s' % filename)
fails, tests = doctest.testfile(filename,
optionflags=self.doctest_opts,
extraglobs=self.extraglobs)
+ print(" %i tests run, %i failed" % (tests, fails))
if self.exit_on_fail and not fails == 0:
sys.exit(1)
+ tot_fails += fails
+ tot_tests += tests
+ print("Total of %i tests run, %i failed" % (tot_tests, tot_fails))
+ if hasattr(__pkg_data__, "TestDoc_run"):
+ __pkg_data__.TestDoc_run(self.dry_run, self.force)
-class TestMod(MyTest):
- """
- Test module's doctest examples
- @see: C{MyTest}
+class TestCode(MyTest):
+ """Test script and module's ``doctest`` examples
+
+ :see: `MyTest`
+
"""
description = gen_desc(__doc__)
def run(self):
- for filename in sorted(["ISBN.py", ]):
- print('Testing module file %s' % filename)
+ """Run the source's docstring code examples"""
+ files = [__pkg_data__.MODULE.__name__, ]
+ files.extend([os.path.basename(i.__file__)
+ for i in __pkg_data__.SCRIPTS])
+ files = glob("%s/*.py" % __pkg_data__.MODULE.__name__)
+ files.extend(["%s.py" % i.__name__ for i in __pkg_data__.SCRIPTS])
+ tot_fails = 0
+ tot_tests = 0
+ for filename in sorted(files):
+ print(' Testing python file %s' % filename)
module = os.path.splitext(filename)[0].replace("/", ".")
if module.endswith("__init__"):
module = module[:-9]
fails, tests = doctest.testmod(sys.modules[module],
optionflags=self.doctest_opts,
extraglobs=self.extraglobs)
+ print(" %i tests run, %i failed" % (tests, fails))
if self.exit_on_fail and not fails == 0:
sys.exit(1)
+ tot_fails += fails
+ tot_tests += tests
+ print("Total of %i tests run, %i failed" % (tot_tests, tot_fails))
+ if hasattr(__pkg_data__, "TestCode_run"):
+ __pkg_data__.TestCode_run(self.dry_run, self.force)
+
+#}
+
+def main():
+ # Force tests to be run, and documentation to be built, before creating a
+ # release.
+ if "sdist" in sys.argv:
+ for test in ("test_code", "test_doc"):
+ if not test in sys.argv:
+ sys.argv = [sys.argv[0], ] + [test, "-x"] + sys.argv[1:]
+ if not "build_doc" in sys.argv:
+ sys.argv.insert(1, "build_doc")
-if __name__ == "__main__":
setup(
- name = "pyisbn",
- version = ISBN.__version__,
- description = ISBN.__doc__.splitlines()[1],
- long_description = sub("C{([^}]*)}", r"``\1``",
- ISBN.__doc__[:ISBN.__doc__.rfind('\n\n')]),
- author = parseaddr(ISBN.__author__)[0],
- author_email = parseaddr(ISBN.__author__)[1],
- url = BASE_URL + "projects/pyisbn.html",
- download_url = "%sdata/pyisbn-%s.tar.bz2" \
- % (BASE_URL, ISBN.__version__),
- py_modules = ['ISBN'],
- license = ISBN.__license__,
- keywords = ['ISBN', 'ISBN-10', 'ISBN-13', 'SBN'],
- classifiers = [
- 'Development Status :: 4 - Beta',
- 'Intended Audience :: Other Audience',
- 'License :: OSI Approved :: GNU General Public License (GPL)',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Topic :: Other/Nonlisted Topic',
- 'Topic :: Text Processing :: Indexing',
- ],
- options = {'sdist': {'formats': 'bztar'}},
- cmdclass = {
+ name=__pkg_data__.MODULE.__name__,
+ version=__pkg_data__.MODULE.__version__,
+ description=__pkg_data__.DESCRIPTION,
+ long_description=__pkg_data__.LONG_DESCRIPTION,
+ author=parseaddr(__pkg_data__.MODULE.__author__)[0],
+ author_email=parseaddr(__pkg_data__.MODULE.__author__)[1],
+ url=PROJECT_URL,
+ download_url="%sdata/%s-%s.tar.bz2" \
+ % (BASE_URL, __pkg_data__.MODULE.__name__,
+ __pkg_data__.MODULE.__version__),
+ packages=[__pkg_data__.MODULE.__name__],
+ scripts=[os.path.basename(i.__file__) for i in __pkg_data__.SCRIPTS],
+ license=__pkg_data__.MODULE.__license__,
+ keywords=__pkg_data__.KEYWORDS,
+ classifiers=__pkg_data__.CLASSIFIERS,
+ obsoletes=__pkg_data__.OBSOLETES,
+ options={'sdist': {'formats': 'bztar'}},
+ cmdclass={
'build_doc': BuildDoc, 'clean': MyClean, 'sdist': HgSdist,
- 'snapshot': Snapshot, 'test_doc': TestDoc, 'test_mod': TestMod,
+ 'snapshot': Snapshot, 'test_doc': TestDoc, 'test_code': TestCode,
},
-)
+ )
+
+if __name__ == "__main__":
+ main()
View
10 test_isbns.py → test.py
@@ -1,9 +1,7 @@
#! /usr/bin/python -tt
# vim: set sw=4 sts=4 et tw=80 fileencoding=utf-8:
#
-"""
-test_isbns - ISBNs for use in tests
-"""
+"""test_isbns - ISBNs for use in tests"""
# Copyright (C) 2007 James Rowe
#
# This program is free software: you can redistribute it and/or modify
@@ -203,6 +201,7 @@
"The HP Way: How Bill Hewlett and I Built Our Company": "0060845791",
"The Hunt for Red October": "0006172768",
"The Invisible Man": "0460876287",
+ "The International Standard Book Number System": "3880531013",
"The Meaning of It All (Allen Lane History S.)": "0140276351",
"The Mythical Man Month and Other Essays on Software Engineering": "0201835959",
"The Principia: Mathematical Principles of Natural Philosophy": "0520088174",
@@ -219,7 +218,7 @@
"Thermodynamics of Natural Systems": "0521847729",
"Time's Alteration: Calendar Reform in Early Modern England": "1857286227",
"Turbo Codes: Principles and Applications (Kluwer International Series in Engineering & Computer Science)": "0792378687",
- "Turbulence and Structures: Chaos, Fluctuations and Helical Self-organizaton in Nature and Laboratory (A Volume in the INTERNATIONAL GEOPHYSICS Series)": "0121257401",
+ "Turbulence and Structures: Chaos, Fluctuations and Helical Self-organization in Nature and Laboratory (A Volume in the INTERNATIONAL GEOPHYSICS Series)": "0121257401",
"Understanding Energy: Energy, Entropy and Thermodynamics for Everyman": "9810206798",
"Understanding the Linux Kernel": "0596005652",
"User Interface Design for Programmers": "1893115941",
@@ -228,4 +227,5 @@
"What Are the Chances?: Voodoo Deaths, Office Gossip and Other Adventures in Probability": "0801869412",
"Without Remorse": "0006476414",
"Wizard: Life and Times of Nikola Tesla": "0806519606",
-}
+} #: Sample book data for use in tests
+
Please sign in to comment.
Something went wrong with that request. Please try again.