Skip to content

Commit

Permalink
Minimally working attribution generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
pombredanne committed Nov 2, 2014
1 parent cc5a45e commit 58d5127
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 157 deletions.
109 changes: 87 additions & 22 deletions about_code_tool/attrib.py
Expand Up @@ -22,37 +22,102 @@
import jinja2


def generate_and_save(abouts, output_location, template=None,
inventory_location=None):
rendered = generate(abouts, template=None, inventory_location=None)
if rendered:
with codecs.open(output_location, 'wb', encoding='utf-8') as of:
of.write(rendered)
def by_license(abouts):
"""
Return an ordered dict sorted by key of About objects grouped by license
"""
pass


def by_component_name(abouts):
"""
Return an ordered dict sorted by key of About objects grouped by component
name.
"""
pass


def generate(abouts, template=None, inventory_location=None):
def unique(abouts):
"""
Generate an attribution file from the current list of ABOUT objects.
The optional `limit_to` parameter allows to restrict the generated
attribution to a specific list of component names.
Return a list of unique About objects.
"""
pass

if not template:
template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'templates/default2.html')

# FIXME: the template dir should be outside the code tree
template_dir = os.path.dirname(template)
template_file_name = os.path.basename(template)
loader = jinja2.FileSystemLoader(template_dir)
jinja_env = jinja2.Environment(loader=loader)
def prepare_licenses(abouts):
"""
Return a ordered dictionary of repeated licenses sorted by key and update
a list of new about objects with updated license references for repeated
licenses.
"""
pass


def generate(abouts, template_string=None):
"""
Generate and return attribution text from a list of ABOUT objects and a
template string.
The returned rendered text may contain template processing error messages.
"""
syntax_error = check_template(template_string)
if syntax_error:
return 'Template validation error at line: %r: %r' % (syntax_error)
template = jinja2.Template(template_string)
return template.render(abouts=abouts)

try:
rendered = template.render(abouts=abouts)
except Exception, e:
line = getattr(e, 'lineno', None)
ln_msg = ' at line: %r' % line if line else ''
err = getattr(e, 'message', '')
return 'Template processing error%(ln_msg)s: %(err)r' % locals()
return rendered


def check_template(template_string):
"""
Check the syntax of a template. Return an error tuple (line number,
message) if the template is invalid or None if it is valid.
"""
try:
template = jinja_env.get_template(template_file_name)
except jinja2.TemplateNotFound:
jinja2.Template(template_string)
return
except (jinja2.TemplateSyntaxError, jinja2.TemplateAssertionError,), e:
return e.lineno, e.message


# FIXME: the template dir should be outside the code tree
# FIXME: use posix paths
default_template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'templates', 'default2.html')

def generate_from_file(abouts, template_loc=None):
"""
Generate and return attribution string from a list of ABOUT objects and a
template location.
"""
if not template_loc:
template_loc = default_template
with codecs.open(template_loc, 'rb', encoding='utf-8') as tplf:
tpls = tplf.read()
return generate(abouts, template_string=tpls)


def generate_and_save(abouts, output_location, template_loc=None,
inventory_location=None):
"""
Generate attribution using template and save at output_location.
Filter the list of about object based on the inventory CSV at
inventory_location.
"""

# TODO: Filter abouts based on CSV at inventory_location.
if inventory_location:
pass

rendered = template.render(abouts=abouts)
return rendered
rendered = generate_from_file(abouts, template_loc=template_loc)

if rendered:
with codecs.open(output_location, 'wb', encoding='utf-8') as of:
of.write(rendered)
10 changes: 5 additions & 5 deletions about_code_tool/cmd.py
Expand Up @@ -148,13 +148,14 @@ def fetch(location):
def attrib(location, output, template=None, inventory_location=None,):
"""
Generate attribution document at output location using the directory of
ABOUT files at location, the template file (or a default), an inventory_location CSV
file containing a list of ABOUT files to generate attribution for. Only
include components code when attribute=yes
ABOUT files at location, the template file (or a default), an
inventory_location CSV file containing a list of ABOUT files to generate
attribution for. Only include components code when attribute=yes
"""
click.echo('Generate attribution documentation')
errors, abouts = about_code_tool.model.collect_inventory(location)
about_code_tool.attrib.generate_and_save(abouts, output, template, inventory_location)
about_code_tool.attrib.generate_and_save(abouts, output, template,
inventory_location)
log_errors(errors)


Expand Down Expand Up @@ -185,4 +186,3 @@ def log_errors(errors, level=NOTSET):

if __name__ == '__main__':
cli()

17 changes: 14 additions & 3 deletions about_code_tool/model.py
Expand Up @@ -55,19 +55,23 @@ def __init__(self, name=None, value=None, required=False, present=False):
self.original_value = value

# can become a string, list or OrderedDict() after validation
self.value = value
self.value = value or self.default_value()

self.required = required
# True if the field is present in an About object
self.present = present
self.errors = []

def default_value(self):
return ''

def validate(self, *args, **kwargs):
"""
Validate and normalize thyself. Return a list of errors.
"""
errors = []
name = self.name
self.value = self.default_value()
if not self.present:
# required fields must be present
if self.required:
Expand All @@ -80,7 +84,6 @@ def validate(self, *args, **kwargs):
else:
# present fields should have content ...
if not self.has_content:
self.value = None
# ... especially if required
if self.required:
msg = u'Field %(name)s is required and empty'
Expand Down Expand Up @@ -149,7 +152,6 @@ def __eq__(self, other):
"""
Equality based on string content value, ignoring spaces
"""

return (isinstance(other, self.__class__)
and self.name == other.name
and self.value == other.value)
Expand All @@ -160,6 +162,7 @@ class StringField(Field):
A field containing a string value possibly on multiple lines.
The validated value is a string.
"""

def _validate(self, *args, **kwargs):
errors = super(StringField, self)._validate(*args, ** kwargs)
return errors
Expand Down Expand Up @@ -215,6 +218,9 @@ class ListField(StringField):
A field containing a list of string values, one per line. The validated
value is a list.
"""
def default_value(self):
return []

def _validate(self, *args, **kwargs):
errors = super(ListField, self)._validate(*args, ** kwargs)

Expand Down Expand Up @@ -298,6 +304,8 @@ class PathField(ListField):
The validated value is an ordered mapping of path->location or None.
The paths can also be resolved
"""
def default_value(self):
return {}

def _validate(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -424,6 +432,9 @@ class BooleanField(SingleLineField):
"""
An flag field with a boolean value. Validated value is False, True or None.
"""
def default_value(self):
return None

flags = {'yes': True, 'y': True, 'no': False, 'n': False, }

flag_values = ', '.join(flags)
Expand Down
82 changes: 71 additions & 11 deletions about_code_tool/templates/default2.html
@@ -1,16 +1,76 @@
<!doctype html>
<html>
<head>
<title>Open Source Software Information</title>
</head>
<head>
<style type="text/css">
div.additional-license-text-list {display:block}
body {font-family: Helvetica, Arial, sans-serif;}
pre {white-space: pre-wrap;}
</style>
<title>Open Source Software Information</title>
</head>

<body>
<h1>OPEN SOURCE SOFTWARE INFORMATION</h1>
<body>
<h1>OPEN SOURCE SOFTWARE INFORMATION</h1>
<div>
<p>For instructions on how to obtain a copy of any source code
being made publicly available by Starship Technology related to
software used in this product you may send your request in
writing to:</p>

<div class="oss-table-of-contents">
{% for about in abouts %}
<p>name: {{ about.name.value }} version: {{ about.version.value }}</p>
{% endfor %}
</div>
</body>
<p>
Starship Technology LLC.<br/>
OSS Management<br/>
7829 Montague Expressway<br/>
Santa Clara, CA 95031<br/>
USA<br/>
</p>

<p>The Starship Technology website www.dejacode.com also
contains information regarding Starship Technology's use of open
source. Starship Technology has created the <a href=
"http://www.dejacode.org">www.dejacode.org</a> to
serve as a portal for interaction with the software
community-at-large.</p>

<p>This document contains additional information regarding
licenses, acknowledgments and required copyright notices for
open source packages used in this Starship Technology product.
This Starship Technology product contains the following open
source software components</p>
</div>

<div class="oss-table-of-contents">
{% for about in abouts %}
<p>name: {{ about.name.value }} version: {{ about.version.value }}</p>
<p><a href="#component_{{ loop.index0 }}">{{ about.name.value }} {{ about.version.value }}</a></p>
{% endfor %}
</div>
<hr/>
{% for about in abouts %}
<div class="oss-component" id="component_{{ loop.index0 }}">
<h3 class="component-name">{{ about.name.value }} {{ about.version.value }}</h3>
{% if about.license_name.value %}
<p>This component is licensed under {{about.license_name.value }}.
{% endif -%}
{% if about.copyright.value %}
<pre>{{about.copyright.value}}</pre>
{% endif %}
{% for pth, text in about.notice_file.value %}
<pre class="component-notice">{{ text }}</pre>
{% endfor %}
{% if about.license_name in [] %}
<p>Full text of <a class="{{ about.license_name.value }}" href="#component-license-{{ about.license_name.value }}"> {{ about.license_name.value }} </a> is available at the end of this document.</p>
{% else %}
{% for pth, text in about.license_file.value %}
<pre class="component-license">{{ text | e}}</pre>
{% endfor %}
{% endif %}

</div>
{% endfor %}
<hr/>

<h3>Common Licenses Used in This Product</h3>
<h3><a id="End">End</a></h3>
</body>
</html>
62 changes: 26 additions & 36 deletions about_code_tool/tests/test_attrib.py
Expand Up @@ -16,49 +16,39 @@

from __future__ import print_function

import os
import unittest

from about_code_tool import genattrib
from about_code_tool.tests import get_test_loc

from about_code_tool import attrib, model

TESTS_DIR = os.path.abspath(os.path.dirname(__file__))
TESTDATA_DIR = os.path.join(TESTS_DIR, 'testdata')
GEN_LOCATION = os.path.join(TESTDATA_DIR, 'test_files_for_genabout')

class AttribTest(unittest.TestCase):

class GenAttribTest(unittest.TestCase):
def test_convert_dict_key_to_lower_case(self):
test = [{'Directory': '/test/', 'file_name': 'test.c'}]
expected = [{'directory': '/test/', 'file_name': 'test.c'}]
result = genattrib.convert_dict_key_to_lower_case(test)
self.assertEqual(expected, result)

def test_check_no_about_file_existence(self):
test = [{'Directory': '/test/', 'file_name': '/test.c'}]
result = genattrib.check_about_file_existence_and_format(test)
self.assertFalse(result)
def test_check_template(self):
assert attrib.check_template('template_string') == None
assert attrib.check_template('{{template_string') == (1,
"unexpected end of template, expected 'end of print statement'.",)
template = open(get_test_loc('attrib_gen/test.template')).read()
assert attrib.check_template(template) == None

def test_check_have_about_file_existence(self):
test = [{'Directory': '/test/', 'about_file': '/test.ABOUT'}]
result = genattrib.check_about_file_existence_and_format(test)
self.assertEqual(test, result)
def test_check_template_default_is_valid(self):
template = open(attrib.default_template).read()
assert attrib.check_template(template) == None

def test_check_no_about_file_not_start_with_slash(self):
test = [{'Directory': '/test/', 'file_name': 'test.c'}]
result = genattrib.check_about_file_existence_and_format(test)
self.assertFalse(result)

def test_check_have_about_file_not_start_with_slash(self):
test = [{'Directory': '/test/', 'about_file': 'test.ABOUT'}]
expected = [{'Directory': '/test/', 'about_file': '/test.ABOUT'}]
result = genattrib.check_about_file_existence_and_format(test)
def test_generate(self):
expected = (u'Apache HTTP Server: 2.4.3\n'
u'resource: httpd-2.4.3.tar.gz\n')
test_file = get_test_loc('attrib_gen/attrib.ABOUT')
template = open(get_test_loc('attrib_gen/test.template')).read()
_errors, abouts = model.collect_inventory(test_file)
result = attrib.generate(abouts, template)
self.assertEqual(expected, result)

def test_update_path_to_about(self):
test = ['/test/test1.ABOUT', '/test/test2/', 'test/test3.c']
expected = ['/test/test1.ABOUT',
'/test/test2/test2.ABOUT',
'test/test3.c.ABOUT']
result = genattrib.update_path_to_about(test)
self.assertEqual(expected, result)
def test_generate_from_file_with_default_template(self):
test_file = get_test_loc('attrib_gen/attrib.ABOUT')
_errors, abouts = model.collect_inventory(test_file)
result = attrib.generate_from_file(abouts)
expected = open(get_test_loc('attrib_gen/expected_default_attrib.html')).read()
self.assertEqual([x.rstrip() for x in expected.splitlines()],
[x.rstrip() for x in result.splitlines()])

0 comments on commit 58d5127

Please sign in to comment.