[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