[PATCH 05/12] copyright: Add a License class.

John Wright jsw at debian.org
Sun Aug 31 21:26:11 UTC 2014


From: John Wright <jsw at google.com>

License is an immutable (synopsis, text) tuple.  Later, we can add
methods to attempt to parse the synopsis as an expression of
alternatvies - for now it just keeps the data and provides an easy way
to generate data to shove into a License field from free-form text.

This change also makes the license field of Header actually parse to and
from the License class.
---
 lib/debian/copyright.py |  94 +++++++++++++++++++++++++++++-
 tests/test_copyright.py | 148 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 240 insertions(+), 2 deletions(-)

diff --git a/lib/debian/copyright.py b/lib/debian/copyright.py
index 8315efc..941d7fa 100644
--- a/lib/debian/copyright.py
+++ b/lib/debian/copyright.py
@@ -27,6 +27,7 @@ TODO(jsw): Add example usage.
 from __future__ import unicode_literals
 
 import collections
+import itertools
 import re
 import string
 import warnings
@@ -166,6 +167,96 @@ class _SpaceSeparated(object):
         return ' '.join(tmp)
 
 
+# TODO(jsw): Move multiline formatting/parsing elsewhere?
+
+def format_multiline(s):
+    """Formats multiline text for insertion in a Deb822 field.
+
+    Each line except for the first one is prefixed with a single space.  Lines
+    that are blank or only whitespace are replaced with ' .'
+    """
+    if s is None:
+        return None
+    return format_multiline_lines(s.splitlines())
+
+
+def format_multiline_lines(lines):
+    """Same as format_multline, but taking input pre-split into lines."""
+    out_lines = []
+    for i, line in enumerate(lines):
+        if i != 0:
+            if not line.strip():
+                line = '.'
+            line = ' ' + line
+        out_lines.append(line)
+    return '\n'.join(out_lines)
+
+
+def parse_multiline(s):
+    """Inverse of format_multiline.
+
+    Technically it can't be a perfect inverse, since format_multline must
+    replace all-whitespace lines with ' .'.  Specifically, this function:
+      - Does nothing to the first line
+      - Removes first character (which must be ' ') from each proceeding line.
+      - Replaces any line that is '.' with an empty line.
+    """
+    if s is None:
+        return None
+    return '\n'.join(parse_multiline_as_lines(s))
+
+
+def parse_multiline_as_lines(s):
+    """Same as parse_multiline, but returns a list of lines.
+
+    (This is the inverse of format_multiline_lines.)
+    """
+    lines = s.splitlines()
+    for i, line in enumerate(lines):
+        if i == 0:
+            continue
+        if line.startswith(' '):
+            line = line[1:]
+        else:
+            raise ValueError('continued line must begin with " "')
+        if line == '.':
+            line = ''
+        lines[i] = line
+    return lines
+
+
+class License(collections.namedtuple('License', 'synopsis text')):
+    """Represents the contents of a License field.  Immutable."""
+
+    def __new__(cls, synopsis, text=''):
+        """Creates a new License object.
+
+        :param synopsis: The short name of the license, or an expression giving
+            alternatives.  (The first line of a License field.)
+        :param text: The full text of the license, if any (may be None).  The
+            lines should not be mangled for "deb822"-style wrapping - i.e. they
+            should not have whitespace prefixes or single '.' for empty lines.
+        """
+        return super(License, cls).__new__(
+            cls, synopsis=_single_line(synopsis), text=(text or ''))
+
+    @classmethod
+    def from_str(cls, s):
+        if s is None:
+            return None
+
+        lines = parse_multiline_as_lines(s)
+        if not lines:
+            return cls('')
+        return cls(lines[0], text='\n'.join(itertools.islice(lines, 1, None)))
+
+    def to_str(self):
+        return format_multiline_lines([self.synopsis] + self.text.splitlines())
+
+    # TODO(jsw): Parse the synopsis?
+    # TODO(jsw): Provide methods to look up license text for known licenses?
+
+
 class Header(deb822.RestrictedWrapper):
     """Represents the header paragraph of a debian/copyright file.
 
@@ -215,8 +306,7 @@ class Header(deb822.RestrictedWrapper):
 
     comment = deb822.RestrictedField('Comment')
 
-    # TODO(jsw): Parse this.
     license = deb822.RestrictedField(
-        'License', to_str=lambda _: None, from_str=lambda _: None)
+        'License', from_str=License.from_str, to_str=License.to_str)
 
     copyright = deb822.RestrictedField('Copyright')
diff --git a/tests/test_copyright.py b/tests/test_copyright.py
index 129d57e..8555089 100755
--- a/tests/test_copyright.py
+++ b/tests/test_copyright.py
@@ -63,6 +63,28 @@ License: GPL-2+
  [LICENSE TEXT]
 """
 
+GPL_TWO_PLUS_TEXT = """\
+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 2 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 package; if not, write to the Free
+Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+Boston, MA  02110-1301 USA
+
+On Debian systems, the full text of the GNU General Public
+License version 2 can be found in the file
+`/usr/share/common-licenses/GPL-2'."""
+
 FORMAT = 'http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/'
 
 
@@ -209,6 +231,120 @@ class CopyrightTest(unittest.TestCase):
         self.assertIsNone(c.header.license)
 
 
+class MultlineTest(unittest.TestCase):
+    """Test cases for format_multiline{,_lines} and parse_multline{,_as_lines}.
+    """
+
+    def setUp(self):
+        paragraphs = list(deb822.Deb822.iter_paragraphs(SIMPLE.splitlines()))
+        self.formatted = paragraphs[1]['License']
+        self.parsed = 'GPL-2+\n' + GPL_TWO_PLUS_TEXT
+        self.parsed_lines = self.parsed.splitlines()
+
+    def test_format_multiline(self):
+        self.assertEqual(None, copyright.format_multiline(None))
+        self.assertEqual('Foo', copyright.format_multiline('Foo'))
+        self.assertEqual(
+            'Foo\n Bar baz\n .\n Quux.',
+            copyright.format_multiline('Foo\nBar baz\n\nQuux.'))
+        self.assertEqual(
+            self.formatted, copyright.format_multiline(self.parsed))
+
+    def test_parse_multiline(self):
+        self.assertEqual(None, copyright.parse_multiline(None))
+        self.assertEqual('Foo', copyright.parse_multiline('Foo'))
+        self.assertEqual(
+            'Foo\nBar baz\n\nQuux.',
+            copyright.parse_multiline('Foo\n Bar baz\n .\n Quux.'))
+        self.assertEqual(
+            self.parsed, copyright.parse_multiline(self.formatted))
+
+    def test_format_multiline_lines(self):
+        self.assertEqual('', copyright.format_multiline_lines([]))
+        self.assertEqual('Foo', copyright.format_multiline_lines(['Foo']))
+        self.assertEqual(
+            'Foo\n Bar baz\n .\n Quux.',
+            copyright.format_multiline_lines(
+                ['Foo', 'Bar baz', '', 'Quux.']))
+        self.assertEqual(
+            self.formatted,
+            copyright.format_multiline_lines(self.parsed_lines))
+
+    def test_parse_multiline_as_lines(self):
+        self.assertEqual([], copyright.parse_multiline_as_lines(''))
+        self.assertEqual(['Foo'], copyright.parse_multiline_as_lines('Foo'))
+        self.assertEqual(
+            ['Foo', 'Bar baz', '', 'Quux.'],
+            copyright.parse_multiline_as_lines(
+                'Foo\n Bar baz\n .\n Quux.'))
+        self.assertEqual(
+            self.parsed_lines,
+            copyright.parse_multiline_as_lines(self.formatted))
+
+    def test_parse_format_inverses(self):
+        self.assertEqual(
+            self.formatted,
+            copyright.format_multiline(
+                copyright.parse_multiline(self.formatted)))
+
+        self.assertEqual(
+            self.formatted,
+            copyright.format_multiline_lines(
+                copyright.parse_multiline_as_lines(self.formatted)))
+
+        self.assertEqual(
+            self.parsed,
+            copyright.parse_multiline(
+                copyright.format_multiline(self.parsed)))
+
+        self.assertEqual(
+            self.parsed_lines,
+            copyright.parse_multiline_as_lines(
+                copyright.format_multiline_lines(self.parsed_lines)))
+
+
+class LicenseTest(unittest.TestCase):
+
+    def test_empty_text(self):
+        l = copyright.License('GPL-2+')
+        self.assertEqual('GPL-2+', l.synopsis)
+        self.assertEqual('', l.text)
+        self.assertEqual('GPL-2+', l.to_str())
+
+    def test_newline_in_synopsis(self):
+        with self.assertRaises(ValueError) as cm:
+            copyright.License('foo\n bar')
+        self.assertEqual(('must be single line',), cm.exception.args)
+
+    def test_nonempty_text(self):
+        text = (
+            'Foo bar.\n'
+            '\n'
+            'Baz.\n'
+            'Quux\n'
+            '\n'
+            'Bang and such.')
+        l = copyright.License('GPL-2+', text=text)
+        self.assertEqual(text, l.text)
+        self.assertEqual(
+            ('GPL-2+\n'
+             ' Foo bar.\n'
+             ' .\n'
+             ' Baz.\n'
+             ' Quux\n'
+             ' .\n'
+             ' Bang and such.'),
+            l.to_str())
+
+    def test_typical(self):
+        paragraphs = list(deb822.Deb822.iter_paragraphs(SIMPLE.splitlines()))
+        p = paragraphs[1]
+        l = copyright.License.from_str(p['license'])
+        self.assertEqual('GPL-2+', l.synopsis)
+        self.assertEqual(GPL_TWO_PLUS_TEXT, l.text)
+        self.assertEqual(p['license'], l.to_str())
+
+
 class HeaderTest(unittest.TestCase):
 
     def test_format_not_none(self):
@@ -268,6 +404,18 @@ class HeaderTest(unittest.TestCase):
             '\n Foo Bar <foo at bar.com>\n http://bar.com/foo',
             h['upstream-contact'])
 
+    def test_license(self):
+        h = copyright.Header()
+        self.assertIsNone(h.license)
+        l = copyright.License('GPL-2+')
+        h.license = l
+        self.assertEqual(l, h.license)
+        self.assertEqual('GPL-2+', h['license'])
+
+        h.license = None
+        self.assertIsNone(h.license)
+        self.assertFalse('license' in h)
+
 
 if __name__ == '__main__':
     unittest.main()
-- 
2.1.0




More information about the pkg-python-debian-maint mailing list