Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Annotation' class as a part of the SPDX 2.1 specification. #74

Merged
merged 3 commits into from
Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions data/SPDXRdfExample.rdf
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@
<reviewer>Person: Suzanne Reviewer</reviewer>
</Review>
</reviewed>
<annotation>
<Annotation rdf:about="https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-45">
<annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_review"/>
<rdfs:comment>This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses</rdfs:comment>
<annotationDate>2012-06-13T00:00:00Z</annotationDate>
<annotator>Person: Jim Reviewer</annotator>
</Annotation>
</annotation>
<referencesFile>
<File rdf:about="https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File2">
<copyrightText>Copyright 2010, 2011 Source Auditor Inc.</copyrightText>
Expand Down
7 changes: 7 additions & 0 deletions data/SPDXTagExample.tag
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Reviewer: Person: Suzanne Reviewer
ReviewDate: 2011-03-13T00:00:00Z
ReviewComment: <text>Another example reviewer.</text>

## Annotation Information
Annotator: Person: Jim Annotator
AnnotationType: REVIEW
AnnotationDate: 2012-03-11T00:00:00Z
AnnotationComment: <text>An example annotation comment.</text>
SPDXREF: SPDXRef-45

## Package Information
PackageName: SPDX Translator
PackageVersion: Version 0.9.2
Expand Down
7 changes: 7 additions & 0 deletions examples/parse_rdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
for lics in doc.extracted_licenses:
print '\tIdentifier: {0}'.format(lics.identifier)
print '\tName: {0}'.format(lics.full_name)
print 'Annotations:'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the examples to also work on Python 3, please import the future print function and use it.

for an in doc.annotations:
print '\tAnnotator: {0}'.format(an.annotator)
print '\tAnnotation Date: {0}'.format(an.annotation_date)
print '\tAnnotation Comment: {0}'.format(an.comment)
print '\tAnnotation Type: {0}'.format(an.annotation_type)
print '\tAnnotation SPDX Identifier: {0}'.format(an.spdx_id)

else:
print 'Errors while parsing'
125 changes: 125 additions & 0 deletions spdx/annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

# Copyright (c) 2018 Yash M. Nisar
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

from datetime import datetime
from functools import total_ordering

from spdx.utils import datetime_iso_format


@total_ordering
class Annotation(object):

"""
Document annotation information.
Fields:
- annotator: Person, Organization or tool that has commented on a file,
package, or the entire document. Conditional (Mandatory, one), if there
is an Annotation.
- annotation_date: To identify when the comment was made. Conditional
(Mandatory, one), if there is an Annotation. Type: datetime.
- comment: Annotation comment. Conditional (Mandatory, one), if there is
an Annotation. Type: str.
- annotation_type: Annotation type. Conditional (Mandatory, one), if there is an
Annotation. Type: str.
- spdx_id: Uniquely identify the element in an SPDX document which is being
referenced. Conditional (Mandatory, one), if there is an Annotation.
Type: str.
"""

def __init__(self, annotator=None, annotation_date=None, comment=None,
annotation_type=None, spdx_id=None):
self.annotator = annotator
self.annotation_date = annotation_date
self.comment = comment
self.annotation_type = annotation_type
self.spdx_id = spdx_id

def __eq__(self, other):
return (
isinstance(other, Annotation) and self.annotator == other.annotator
and self.annotation_date == other.annotation_date
and self.comment == other.comment
)

def __lt__(self, other):
return (
(self.annotator, self.annotation_date, self.comment) <
(other.annotator, other.annotation_date, other.comment,)
)

def set_annotation_date_now(self):
self.annotation_date = datetime.utcnow()

@property
def annotation_date_iso_format(self):
return datetime_iso_format(self.annotation_date)

@property
def has_comment(self):
return self.comment is not None

def validate(self, messages=None):
"""Returns True if all the fields are valid.
Appends any error messages to messages parameter.
"""
# FIXME: we should return messages instead
messages = messages if messages is not None else []

return (self.validate_annotator(messages)
and self.validate_annotation_date(messages)
and self.validate_annotation_type(messages)
and self.validate_spdx_id(messages))

def validate_annotator(self, messages=None):
# FIXME: we should return messages instead
messages = messages if messages is not None else []

if self.annotator is not None:
return True
else:
messages.append('Annotation missing annotator.')
return False

def validate_annotation_date(self, messages=None):
# FIXME: we should return messages instead
messages = messages if messages is not None else []

if self.annotation_date is not None:
return True
else:
messages.append('Annotation missing annotation date.')
return False

def validate_annotation_type(self, messages=None):
# FIXME: we should return messages instead
messages = messages if messages is not None else []

if self.annotation_type is not None:
return True
else:
messages.append('Annotation missing annotation type.')
return False

def validate_spdx_id(self, messages=None):
# FIXME: we should return messages instead
messages = messages if messages is not None else []

if self.spdx_id is not None:
return True
else:
messages.append('Annotation missing SPDX Identifier Reference.')
return False
15 changes: 15 additions & 0 deletions spdx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ class Document(object):
SPDX license list. Optional, many. Type: ExtractedLicense.
- reviews: SPDX document review information, Optional zero or more.
Type: Review.
- annotations: SPDX document annotation information, Optional zero or more.
Type: Annotation.
"""

def __init__(self, version=None, data_license=None, name=None, spdx_id=None,
Expand All @@ -291,10 +293,14 @@ def __init__(self, version=None, data_license=None, name=None, spdx_id=None,
self.package = package
self.extracted_licenses = []
self.reviews = []
self.annotations = []

def add_review(self, review):
self.reviews.append(review)

def add_annotation(self, annotation):
self.annotations.append(annotation)

def add_extr_lic(self, lic):
self.extracted_licenses.append(lic)

Expand Down Expand Up @@ -415,6 +421,15 @@ def validate_reviews(self, messages=None):
valid = review.validate(messages) and valid
return valid

def validate_annotations(self, messages=None):
# FIXME: messages should be returned
messages = messages if messages is not None else []

valid = True
for annotation in self.annotations:
valid = annotation.validate(messages) and valid
return valid

def validate_creation_info(self, messages=None):
# FIXME: messages should be returned
messages = messages if messages is not None else []
Expand Down
6 changes: 6 additions & 0 deletions spdx/parsers/lexers/tagvalue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class Lexer(object):
'Reviewer': 'REVIEWER',
'ReviewDate': 'REVIEW_DATE',
'ReviewComment': 'REVIEW_COMMENT',
# Annotation info
'Annotator': 'ANNOTATOR',
'AnnotationDate': 'ANNOTATION_DATE',
'AnnotationComment': 'ANNOTATION_COMMENT',
'AnnotationType': 'ANNOTATION_TYPE',
'SPDXREF': 'ANNOTATION_SPDX_ID',
# Package Fields
'PackageName': 'PKG_NAME',
'PackageVersion': 'PKG_VERSION',
Expand Down
90 changes: 89 additions & 1 deletion spdx/parsers/rdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
'FILE_SINGLE_LICS': 'File concluded license must be a license url or spdx:noassertion or spdx:none.',
'REVIEWER_VALUE' : 'Invalid reviewer value \'{0}\' must be Organization, Tool or Person.',
'REVIEW_DATE' : 'Invalid review date value \'{0}\' must be date in ISO 8601 format.',
'ANNOTATOR_VALUE': 'Invalid annotator value \'{0}\' must be Organization, Tool or Person.',
'ANNOTATION_DATE': 'Invalid annotation date value \'{0}\' must be date in ISO 8601 format.'
}


Expand Down Expand Up @@ -739,7 +741,90 @@ def get_reviewer(self, r_term):
self.value_error('REVIEWER_VALUE', reviewer_list[0][2])


class Parser(PackageParser, FileParser, ReviewParser):
class AnnotationParser(BaseParser):
"""
Helper class for parsing annotation information.
"""

def __init__(self, builder, logger):
super(AnnotationParser, self).__init__(builder, logger)

def parse_annotation(self, r_term):
annotator = self.get_annotator(r_term)
annotation_date = self.get_annotation_date(r_term)
if annotator is not None:
self.builder.add_annotator(self.doc, annotator)
if annotation_date is not None:
try:
self.builder.add_annotation_date(self.doc, annotation_date)
except SPDXValueError:
self.value_error('ANNOTATION_DATE', annotation_date)
comment = self.get_annotation_comment(r_term)
if comment is not None:
self.builder.add_annotation_comment(self.doc, comment)
annotation_type = self.get_annotation_type(r_term)
self.builder.add_annotation_type(self.doc, annotation_type)
try:
self.builder.set_annotation_spdx_id(self.doc, r_term)
except CardinalityError:
self.more_than_one_error('SPDX Identifier Reference')

def get_annotation_type(self, r_term):
"""Returns annotation type or None if found none or more than one.
Reports errors on failure."""
for _, _, typ in self.graph.triples((
r_term, self.spdx_namespace['annotationType'], None)):
if typ is not None:
return typ
else:
self.error = True
msg = 'Annotation must have exactly one annotation type.'
self.logger.log(msg)
return

def get_annotation_comment(self, r_term):
"""Returns annotation comment or None if found none or more than one.
Reports errors.
"""
comment_list = list(self.graph.triples((r_term, RDFS.comment, None)))
if len(comment_list) > 1:
self.error = True
msg = 'Annotation can have at most one comment.'
self.logger.log(msg)
return
else:
return six.text_type(comment_list[0][2])

def get_annotation_date(self, r_term):
"""Returns annotation date or None if not found.
Reports error on failure.
Note does not check value format.
"""
annotation_date_list = list(self.graph.triples((r_term, self.spdx_namespace['annotationDate'], None)))
if len(annotation_date_list) != 1:
self.error = True
msg = 'Annotation must have exactly one annotation date.'
self.logger.log(msg)
return
return six.text_type(annotation_date_list[0][2])

def get_annotator(self, r_term):
"""Returns annotator as creator object or None if failed.
Reports errors on failure.
"""
annotator_list = list(self.graph.triples((r_term, self.spdx_namespace['annotator'], None)))
if len(annotator_list) != 1:
self.error = True
msg = 'Annotation must have exactly one annotator'
self.logger.log(msg)
return
try:
return self.builder.create_entity(self.doc, six.text_type(annotator_list[0][2]))
except SPDXValueError:
self.value_error('ANNOTATOR_VALUE', annotator_list[0][2])


class Parser(PackageParser, FileParser, ReviewParser, AnnotationParser):
"""
RDF/XML file parser.
"""
Expand Down Expand Up @@ -774,6 +859,9 @@ def parse(self, fil):
for s, _p, o in self.graph.triples((None, self.spdx_namespace['reviewed'], None)):
self.parse_review(o)

for s, _p, o in self.graph.triples((None, self.spdx_namespace['annotation'], None)):
self.parse_annotation(o)

validation_messages = []
# Report extra errors if self.error is False otherwise there will be
# redundent messages
Expand Down
45 changes: 44 additions & 1 deletion spdx/parsers/rdfbuilders.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,50 @@ def add_review_comment(self, doc, comment):
raise OrderError('ReviewComment')


class AnnotationBuilder(tagvaluebuilders.AnnotationBuilder):

def __init__(self):
super(AnnotationBuilder, self).__init__()

def add_annotation_comment(self, doc, comment):
"""Sets the annotation comment. Raises CardinalityError if
already set. OrderError if no annotator defined before.
"""
if len(doc.annotations) != 0:
if not self.annotation_comment_set:
self.annotation_comment_set = True
doc.annotations[-1].comment = comment
return True
else:
raise CardinalityError('AnnotationComment')
else:
raise OrderError('AnnotationComment')

def add_annotation_type(self, doc, annotation_type):
"""Sets the annotation type. Raises CardinalityError if
already set. OrderError if no annotator defined before.
"""
if len(doc.annotations) != 0:
if not self.annotation_type_set:
if annotation_type.endswith('annotationType_other'):
self.annotation_type_set = True
doc.annotations[-1].annotation_type = 'OTHER'
return True
elif annotation_type.endswith('annotationType_review'):
self.annotation_type_set = True
doc.annotations[-1].annotation_type = 'REVIEW'
return True
else:
raise SPDXValueError('Annotation::AnnotationType')
else:
raise CardinalityError('Annotation::AnnotationType')
else:
raise OrderError('Annotation::AnnotationType')


class Builder(DocBuilder, EntityBuilder, CreationInfoBuilder, PackageBuilder,
FileBuilder, ReviewBuilder, ExternalDocumentRefBuilder):
FileBuilder, ReviewBuilder, ExternalDocumentRefBuilder,
AnnotationBuilder):

def __init__(self):
super(Builder, self).__init__()
Expand All @@ -402,3 +444,4 @@ def reset(self):
self.reset_package()
self.reset_file_stat()
self.reset_reviews()
self.reset_annotations()
Loading