Skip to content

Commit 80455d0

Browse files
committed
merge the fixes from #349
Signed-off-by: Chin Yeung Li <[email protected]>
2 parents fb720eb + ec02aad commit 80455d0

File tree

9 files changed

+148
-50
lines changed

9 files changed

+148
-50
lines changed

docs/CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Release x.x.x
44

55
* Update the list of common license keys
6+
* New UrlListField introduced for list of urls
7+
* The UrlField is now only taking single URL value
8+
* The owner is now a StringField instead of ListField
9+
* Format the ordering of the genreated ABOUT file (See https://github.com/nexB/aboutcode-toolkit/issues/349#issuecomment-438871444)
610

711
2018-10-23
812

src/attributecode/gen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def generate(location, base_dir, license_notice_text_location=None,
302302
about.license_name.present = True
303303

304304
# Write the ABOUT files
305-
about.dump(dump_loc, with_empty=with_empty, with_absent=with_absent)
305+
about.dump(dump_loc, use_mapping=use_mapping, mapping_file=mapping_file, with_empty=with_empty, with_absent=with_absent)
306306
for e in not_exist_errors:
307307
errors.append(Error(ERROR, e))
308308
except Exception as e:

src/attributecode/model.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,20 +344,19 @@ def __eq__(self, other):
344344
if sval == oval:
345345
return True
346346

347-
348-
class UrlField(ListField):
347+
class UrlListField(ListField):
349348
"""
350349
A URL field. The validated value is a list of URLs.
351350
"""
352351
def _validate(self, *args, **kwargs):
353352
"""
354353
Check that URLs are valid. Return a list of errors.
355354
"""
356-
errors = super(UrlField, self)._validate(*args, ** kwargs)
357-
for url in self.value:
355+
errors = super(UrlListField, self)._validate(*args, ** kwargs)
356+
name = self.name
357+
val = self.value
358+
for url in val:
358359
if not self.is_valid_url(url):
359-
name = self.name
360-
val = self.value
361360
msg = (u'Field %(name)s: Invalid URL: %(val)s' % locals())
362361
errors.append(Error(WARNING, msg))
363362
return errors
@@ -372,6 +371,32 @@ def is_valid_url(url):
372371
return valid
373372

374373

374+
class UrlField(StringField):
375+
"""
376+
A URL field. The validated value is a URL.
377+
"""
378+
def _validate(self, *args, **kwargs):
379+
"""
380+
Check that URL is valid. Return a list of errors.
381+
"""
382+
errors = super(UrlField, self)._validate(*args, ** kwargs)
383+
name = self.name
384+
val = self.value
385+
if not self.is_valid_url(val):
386+
msg = (u'Field %(name)s: Invalid URL: %(val)s' % locals())
387+
errors.append(Error(WARNING, msg))
388+
return errors
389+
390+
@staticmethod
391+
def is_valid_url(url):
392+
"""
393+
Return True if a URL is valid.
394+
"""
395+
scheme, netloc, _path, _p, _q, _frg = urlparse(url)
396+
valid = scheme in ('http', 'https', 'ftp') and netloc
397+
return valid
398+
399+
375400
class PathField(ListField):
376401
"""
377402
A field pointing to one or more paths relative to the ABOUT file location.
@@ -692,7 +717,7 @@ def create_fields(self):
692717
('license_key', ListField()),
693718
('license_name', ListField()),
694719
('license_file', FileTextField()),
695-
('license_url', UrlField()),
720+
('license_url', UrlListField()),
696721
('copyright', StringField()),
697722
('notice_file', FileTextField()),
698723
('notice_url', UrlField()),
@@ -704,7 +729,7 @@ def create_fields(self):
704729

705730
('changelog_file', FileTextField()),
706731

707-
('owner', ListField()),
732+
('owner', StringField()),
708733
('owner_url', UrlField()),
709734
('contact', ListField()),
710735
('author', ListField()),
@@ -1066,7 +1091,7 @@ def load_dict(self, fields_dict, base_dir, running_inventory=False,
10661091
return errors
10671092

10681093

1069-
def dumps(self, with_absent=False, with_empty=True):
1094+
def dumps(self, use_mapping=False, mapping_file=False, with_absent=False, with_empty=True):
10701095
"""
10711096
Return self as a formatted ABOUT string.
10721097
If with_absent, include absent (not present) fields.
@@ -1113,9 +1138,10 @@ def dumps(self, with_absent=False, with_empty=True):
11131138
if lic_group[3]:
11141139
lic_dict['url'] = lic_group[3]
11151140
about_data.setdefault('licenses', []).append(lic_dict)
1116-
return saneyaml.dump(about_data)
1141+
formatted_about_data = util.format_output(about_data, use_mapping, mapping_file)
1142+
return saneyaml.dump(formatted_about_data)
11171143

1118-
def dump(self, location, with_absent=False, with_empty=True):
1144+
def dump(self, location, use_mapping=False, mapping_file=False, with_absent=False, with_empty=True):
11191145
"""
11201146
Write formatted ABOUT representation of self to location.
11211147
If with_absent, include absent (not present) fields.
@@ -1135,7 +1161,7 @@ def dump(self, location, with_absent=False, with_empty=True):
11351161
if on_windows:
11361162
about_file_path = add_unc(about_file_path)
11371163
with codecs.open(about_file_path, mode='wb', encoding='utf-8') as dumped:
1138-
dumped.write(self.dumps(with_absent, with_empty))
1164+
dumped.write(self.dumps(use_mapping, mapping_file, with_absent, with_empty))
11391165

11401166
def dump_lic(self, location, license_dict):
11411167
"""

src/attributecode/util.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def get_mapping(location=None):
330330
if not os.path.exists(location):
331331
return {}
332332

333-
mapping = {}
333+
mapping = collections.OrderedDict()
334334
try:
335335
with open(location) as mapping_file:
336336
for line in mapping_file:
@@ -412,6 +412,52 @@ def apply_mapping(abouts, alternate_mapping=None):
412412
mapped_abouts.append(mapped_about)
413413
return mapped_abouts
414414

415+
def get_mapping_key_order(mapping_file):
416+
"""
417+
Get the mapping key order and return as a list
418+
"""
419+
if mapping_file:
420+
mapping = get_mapping(mapping_file)
421+
else:
422+
mapping = get_mapping()
423+
return mapping.keys()
424+
425+
def format_output(about_data, use_mapping, mapping_file):
426+
"""
427+
Convert the about_data dictionary to an ordered dictionary for saneyaml.dump()
428+
The ordering should be:
429+
430+
about_resource
431+
name
432+
version <-- if any
433+
and the rest is the order from the mapping.config file (if any); otherwise alphabetical order.
434+
"""
435+
mapping_key_order = []
436+
if use_mapping or mapping_file:
437+
mapping_key_order = get_mapping_key_order(mapping_file)
438+
priority_keys = [u'about_resource', u'name', u'version']
439+
about_data_keys = []
440+
order_dict = collections.OrderedDict()
441+
for key in about_data:
442+
about_data_keys.append(key)
443+
if u'about_resource' in about_data_keys:
444+
order_dict['about_resource'] = about_data['about_resource']
445+
if u'name' in about_data_keys:
446+
order_dict['name'] = about_data['name']
447+
if u'version' in about_data_keys:
448+
order_dict['version'] = about_data['version']
449+
if not mapping_key_order:
450+
for other_key in sorted(about_data_keys):
451+
if not other_key in priority_keys:
452+
order_dict[other_key] = about_data[other_key]
453+
else:
454+
for key in mapping_key_order:
455+
if not key in priority_keys and key in about_data_keys:
456+
order_dict[key] = about_data[key]
457+
for other_key in sorted(about_data_keys):
458+
if not other_key in priority_keys and not other_key in mapping_key_order:
459+
order_dict[other_key] = about_data[other_key]
460+
return order_dict
415461

416462
def get_about_file_path(location, use_mapping=False, mapping_file=None):
417463
"""

tests/test_gen.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ def test_load_inventory(self):
6262
assert expected_errors == errors
6363

6464
expected = [u'about_resource: .\n'
65+
u'name: AboutCode\n'
66+
u'version: 0.11.0\n'
6567
u'description: |-\n'
6668
u' multi\n'
67-
u' line\n'
68-
u'name: AboutCode\n'
69-
u'version: 0.11.0\n']
70-
result = [a.dumps(with_absent=False, with_empty=False)
69+
u' line\n']
70+
result = [a.dumps(use_mapping=False, mapping_file=False, with_absent=False, with_empty=False)
7171
for a in abouts]
7272
assert expected == result
7373

@@ -86,15 +86,15 @@ def test_load_inventory_with_mapping(self):
8686
assert sorted(expected_errors) == sorted(errors)
8787

8888
expected = [u'about_resource: .\n'
89+
u'name: AboutCode\n'
90+
u'version: 0.11.0\n'
8991
u'copyright: Copyright (c) nexB, Inc.\n'
92+
u'resource: this.ABOUT\n'
9093
u'description: |-\n'
9194
u' multi\n'
9295
u' line\n'
93-
u'name: AboutCode\n'
94-
u'resource: this.ABOUT\n'
95-
u'version: 0.11.0\n'
9696
]
97-
result = [a.dumps(with_absent=False, with_empty=False)
97+
result = [a.dumps(use_mapping, mapping_file=False, with_absent=False, with_empty=False)
9898
for a in abouts]
9999
assert expected == result
100100

@@ -148,15 +148,15 @@ def test_generate(self):
148148
assert msg1 in errors[0].message
149149
assert msg2 in errors[1].message
150150

151-
in_mem_result = [a.dumps(with_absent=False, with_empty=False)
151+
in_mem_result = [a.dumps(use_mapping=False, mapping_file=False, with_absent=False, with_empty=False)
152152
for a in abouts][0]
153153
expected = (u'about_resource: .\n'
154+
u'name: AboutCode\n'
155+
u'version: 0.11.0\n'
154156
u'about_resource_path: .\n'
155157
u'description: |-\n'
156158
u' multi\n'
157-
u' line\n'
158-
u'name: AboutCode\n'
159-
u'version: 0.11.0\n')
159+
u' line\n')
160160
assert expected == in_mem_result
161161

162162
def test_generate_not_overwrite_original_license_file(self):
@@ -167,14 +167,14 @@ def test_generate_not_overwrite_original_license_file(self):
167167

168168
errors, abouts = gen.generate(location, base_dir, license_notice_text_location, fetch_license)
169169

170-
in_mem_result = [a.dumps(with_absent=False, with_empty=False)
170+
in_mem_result = [a.dumps(use_mapping=False, mapping_file=False, with_absent=False, with_empty=False)
171171
for a in abouts][0]
172172
expected = (u'about_resource: .\n'
173+
u'name: AboutCode\n'
174+
u'version: 0.11.0\n'
173175
u'about_resource_path: .\n'
174176
u'licenses:\n'
175-
u' - file: this.LICENSE\n'
176-
u'name: AboutCode\n'
177-
u'version: 0.11.0\n')
177+
u' - file: this.LICENSE\n')
178178
assert expected == in_mem_result
179179

180180
def test_deduplicate(self):

tests/test_model.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,15 @@ def test_PathField_contains_dict_after_validate(self):
193193
]
194194
self.check_validate(field_class, value, expected, expected_errors)
195195

196+
"""
197+
UrlField no longer become a list.
198+
If a list is wanted, use UrlListField instead.
196199
def test_UrlField_contains_list_after_validate(self):
197200
value = 'http://some.com/url'
198201
field_class = model.UrlField
199202
expected = [value]
200203
self.check_validate(field_class, value, expected, expected_errors=[])
201-
204+
"""
202205
def test_SingleLineField_has_errors_if_multiline(self):
203206
value = '''line1
204207
line2'''
@@ -704,31 +707,29 @@ def test_About_dumps(self):
704707
assert [] == a.errors
705708

706709
expected = u'''about_resource: .
710+
name: AboutCode
711+
version: 0.11.0
712+
copyright: Copyright (c) 2013-2014 nexB Inc.
713+
license_expression: apache-2.0
707714
author:
708715
- Jillian Daguil
709716
- Chin Yeung Li
710717
- Philippe Ombredanne
711718
- Thomas Druez
712-
copyright: Copyright (c) 2013-2014 nexB Inc.
713719
description: |-
714720
AboutCode is a tool
715721
to process ABOUT files.
716722
An ABOUT file is a file.
717-
homepage_url:
718-
- http://dejacode.org
719-
license_expression: apache-2.0
723+
homepage_url: http://dejacode.org
720724
licenses:
721725
- file: apache-2.0.LICENSE
722726
key: apache-2.0
723-
name: AboutCode
724727
notice_file: NOTICE
725-
owner:
726-
- nexB Inc.
728+
owner: nexB Inc.
727729
vcs_repository: https://github.com/dejacode/about-code-tool.git
728730
vcs_tool: git
729-
version: 0.11.0
730731
'''
731-
result = a.dumps()
732+
result = a.dumps(use_mapping=True)
732733
assert expected == result
733734

734735
# We do not support with_absent and with_empty staring in version 3.2.0.
@@ -810,7 +811,7 @@ def test_About_dumps_with_different_boolean_value(self):
810811
modified: yes
811812
'''
812813

813-
result = a.dumps()
814+
result = a.dumps(use_mapping=False, mapping_file=False)
814815
assert set(expected) == set(result)
815816

816817

@@ -826,7 +827,7 @@ def test_About_dumps_does_not_dump_present__empty_with_absent_False(self):
826827
name: AboutCode
827828
version: 0.11.0
828829
'''
829-
result = a.dumps(with_absent=False, with_empty=False)
830+
result = a.dumps(use_mapping=False, mapping_file=False, with_absent=False, with_empty=False)
830831
assert expected == result
831832

832833
def test_About_as_dict_contains_special_paths(self):
@@ -927,7 +928,7 @@ def test_About_as_dict_with_nothing(self):
927928
'license_key': [u'apache-2.0'],
928929
'license_expression': u'apache-2.0',
929930
'name': u'AboutCode',
930-
'owner': [u'nexB Inc.']}
931+
'owner': u'nexB Inc.'}
931932
result = a.as_dict(with_paths=False,
932933
with_empty=False,
933934
with_absent=False)
@@ -939,7 +940,7 @@ def test_load_dump_is_idempotent(self):
939940
a = model.About()
940941
a.load(test_file)
941942
dumped_file = get_temp_file('that.ABOUT')
942-
a.dump(dumped_file, with_absent=False, with_empty=False)
943+
a.dump(dumped_file, use_mapping=False, mapping_file=False, with_absent=False, with_empty=False)
943944

944945
expected = get_unicode_content(test_file).splitlines()
945946
result = get_unicode_content(dumped_file).splitlines()
@@ -987,7 +988,7 @@ def test_as_dict_load_dict_is_idempotent(self):
987988
'description': u'AboutCode is a tool\nfor files.',
988989
'license_expression': u'apache-2.0',
989990
'name': u'AboutCode',
990-
'owner': [u'nexB Inc.']}
991+
'owner': u'nexB Inc.'}
991992

992993
a = model.About()
993994
base_dir = 'some_dir'
@@ -1002,10 +1003,10 @@ def test_load_dict_handles_field_validation_correctly(self):
10021003
u'author': [u'Jillian Daguil, Chin Yeung Li, Philippe Ombredanne, Thomas Druez'],
10031004
u'copyright': u'Copyright (c) 2013-2014 nexB Inc.',
10041005
u'description': u'AboutCode is a tool to process ABOUT files. An ABOUT file is a file.',
1005-
u'homepage_url': [u'http://dejacode.org'],
1006+
u'homepage_url': u'http://dejacode.org',
10061007
u'license_expression': u'apache-2.0',
10071008
u'name': u'AboutCode',
1008-
u'owner': [u'nexB Inc.'],
1009+
u'owner': u'nexB Inc.',
10091010
u'vcs_repository': u'https://github.com/dejacode/about-code-tool.git',
10101011
u'vcs_tool': u'git',
10111012
u'version': u'0.11.0'}

tests/test_util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,17 @@ def test_get_relative_path_with_same_path_twice(self):
266266
result = util.get_relative_path(loc, loc)
267267
assert expected == result
268268

269+
def test_get_mapping_key_order(self):
270+
expected = ['about_file_path', 'name', 'version', 'copyright', 'license_expression', 'resource']
271+
result = util.get_mapping_key_order(mapping_file=False)
272+
assert expected == result
273+
274+
def test_get_mapping_key_order_with_mapping_file(self):
275+
expected = ['about_file_path', 'name', 'version', 'description', 'license_expression', 'copyright']
276+
test_mapping_file = get_test_loc('mapping_config/mapping.config')
277+
result = util.get_mapping_key_order(test_mapping_file)
278+
assert expected == result
279+
269280
def test_load_csv_without_mapping(self):
270281
test_file = get_test_loc('util/about.csv')
271282
expected = [OrderedDict(

0 commit comments

Comments
 (0)