Skip to content

Commit 897847e

Browse files
committed
Accept non-standard package data in CSV #1398
Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent dc6685d commit 897847e

2 files changed

Lines changed: 97 additions & 75 deletions

File tree

src/formattedcode/output_csv.py

Lines changed: 90 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,37 @@
3737
from scancode import FileOptionType
3838
from scancode import OUTPUT_GROUP
3939

40+
# Python 2 and 3 support
41+
try:
42+
# Python 2
43+
unicode
44+
str = unicode # NOQA
45+
except NameError:
46+
# Python 3
47+
unicode = str # NOQA
48+
basestring = str # NOQA
49+
50+
51+
# Tracing flags
52+
TRACE = True
53+
54+
55+
def logger_debug(*args):
56+
pass
57+
58+
59+
if TRACE:
60+
import sys
61+
import logging
62+
63+
logger = logging.getLogger(__name__)
64+
logging.basicConfig(stream=sys.stdout)
65+
logger.setLevel(logging.DEBUG)
66+
67+
def logger_debug(*args):
68+
return logger.debug(' '.join(isinstance(a, unicode)
69+
and a or repr(a) for a in args))
70+
4071

4172
@output_impl
4273
class CsvOutput(OutputPlugin):
@@ -199,114 +230,98 @@ def collect_keys(mapping, key_group):
199230
yield flat
200231

201232

202-
def flatten_package(_package, path, prefix='package__'):
233+
def get_package_columns(_columns=set()):
234+
"""
235+
Return (and cache in_columns) a set of package column names included in the
236+
CSV output.
237+
Some columsn are excluded for now such as lists of mappings: these do not
238+
serialize well to CSV
239+
"""
240+
if _columns:
241+
return _columns
242+
243+
from packagedcode.models import Package
203244

204245
# exclude some columns for now that contain list of items
205-
excluded_package_columns = {
246+
excluded_columns = {
206247
# list of strings
207-
'download_checksums',
208248
'keywords',
209249
# list of dicts
210250
'parties',
211251
'dependencies',
252+
'source_packages',
253+
}
212254

213-
# comming from a match
255+
# some extra columns for components
256+
extra_columns = [
257+
'components',
258+
'owner_name',
259+
'reference_notes',
260+
'description',
261+
'notice_filename',
262+
'notice_url',
263+
]
214264

215-
# we have license_expression we do not need more
216-
'licenses_summary',
217-
'licenses',
218-
'license_choices_expression',
219-
'license_choices',
265+
fields = Package.fields() + extra_columns
266+
_columns = set(f for f in fields if f not in excluded_columns)
267+
return _columns
220268

221-
'api_url',
222-
'uuid',
223-
'sha1',
224-
'md5',
225-
'owner',
226269

227-
'reference_notes',
228-
'project',
229-
'codescan_identifier',
230-
'is_license_notice',
231-
'is_copyright_notice',
232-
'is_notice_in_codebase',
233-
# 'notice_filename',
234-
# 'notice_url',
235-
'website_terms_of_use',
236-
'is_active',
237-
'curation_level',
238-
'completion_level',
239-
'guidance',
240-
'admin_notes',
241-
'ip_sensitivity_approved',
242-
'affiliate_obligations',
243-
'affiliate_obligation_triggers',
244-
'concluded_license',
245-
'legal_comments',
246-
'legal_reviewed',
247-
'approval_reference',
248-
'distribution_formats_allowed',
249-
'acceptable_linkage',
250-
'export_restrictions',
251-
'approved_download_location',
252-
'approved_community_interaction',
253-
'urn',
254-
'created_date',
255-
'last_modified_date',
256-
'dataspace',
257-
'external_references',
258-
'display_name',
259-
'notes',
260-
'origin_date',
261-
'sublicense_allowed',
262-
}
270+
def flatten_package(_package, path, prefix='package__'):
271+
272+
# known package columns
273+
274+
package_columns = get_package_columns()
263275

264276
pack = OrderedDict(Resource=path)
265277
for k, val in _package.items():
266-
# FIXME: we only keep for now some of the value collections
267-
if k in excluded_package_columns:
278+
if k not in package_columns:
268279
continue
280+
269281
# prefix columns with "package__"
270282
nk = prefix + k
271283

272284
if k == 'version':
273-
if val:
285+
val = val or ''
286+
if val and not val.lower().startswith('v'):
274287
# prefix versions with a v to avoid spreadsheet tools to mistake
275-
# a version for a number or date.
288+
# a version for a number or date when reading CSVs (common with
289+
# Excel and LibreOffice).
276290
val = 'v ' + val
277-
pack[nk] = val
278-
else:
279-
pack[nk] = ''
291+
pack[nk] = val
280292
continue
281293

282-
# these may come from a match
283-
294+
# these may come from a component matched
284295
if k == 'components' and val and isinstance(val, list):
285-
for compo in val:
286-
for compo_key, compo_val in compo.items():
287-
if compo_key in excluded_package_columns:
296+
for component in val:
297+
for component_key, component_val in component.items():
298+
if component_key not in package_columns:
288299
continue
289300

290-
compo_nk = nk + '__' + compo_key
301+
component_new_key = nk + '__' + component_key
291302

292-
if compo_val is None:
293-
pack[compo_nk] = ''
303+
if component_val is None:
304+
pack[component_new_key] = ''
294305
continue
295306

296-
if not isinstance(compo_val, basestring):
297-
compo_val = repr(compo_val)
298-
existing = pack.get(compo_nk) or []
299-
pack[compo_nk] = ' \n'.join(existing + [compo_val])
300-
continue
307+
if isinstance(component_val, list):
308+
component_val = '\n'.join(component_val)
309+
310+
if not isinstance(component_val, str):
311+
component_val = repr(component_val)
312+
313+
existing = pack.get(component_new_key) or []
314+
if not isinstance(existing, list):
315+
existing = [existing]
316+
317+
if TRACE:
318+
logger_debug('component_new_key:', component_new_key, 'existing:', type(existing), repr(existing))
319+
logger_debug('component_key:', component_key, 'component_val:', type(component_val), repr(component_val))
301320

302-
if k == 'match':
303-
for mk, mval in val.items():
304-
match_nk = nk + '__' + mk
305-
pack[match_nk] = mval
321+
pack[component_new_key] = ' \n'.join(existing + [component_val])
306322
continue
307323

308324
# everything else
309-
# collect all the keys
310325

311326
pack[nk] = ''
312327

src/packagedcode/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ def create(cls, ignore_unknown=True, **kwargs):
125125
kwargs = {k: v for k, v in kwargs.items() if k in known_attr}
126126
return cls(**kwargs)
127127

128+
@classmethod
129+
def fields(cls):
130+
"""
131+
Return a list of field names defined on this model.
132+
"""
133+
return [a.name for a in attr.fields(cls)]
134+
128135

129136
party_person = 'person'
130137
# often loosely defined

0 commit comments

Comments
 (0)