Skip to content

Commit fab8a78

Browse files
committed
Correctly merge the 22_collect_redistributable_src branch
1 parent 0ca5344 commit fab8a78

File tree

4 files changed

+364
-34
lines changed

4 files changed

+364
-34
lines changed

src/attributecode/cmd.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
from attributecode.attrib import check_template
3939
from attributecode.attrib import DEFAULT_TEMPLATE_FILE
4040
from attributecode.attrib import generate_and_save as generate_attribution_doc
41-
from attributecode.gen import generate as generate_about_files
42-
from attributecode.model import collect_inventory
41+
from attributecode.gen import generate as generate_about_files, load_inventory
42+
from attributecode.model import collect_inventory, get_copy_list
43+
from attributecode.model import copy_redist_src
4344
from attributecode.model import write_output
4445
from attributecode.util import extract_zip
4546
from attributecode.util import filter_errors
47+
from attributecode.util import get_temp_dir
4648

4749

4850
__copyright__ = """
@@ -366,6 +368,97 @@ def attrib(location, output, template, vartext, quiet, verbose):
366368
sys.exit(errors_count)
367369

368370

371+
######################################################################
372+
# collect_redist_src subcommand
373+
######################################################################
374+
375+
@about.command(cls=AboutCommand,
376+
short_help='Collect redistributable sources.')
377+
378+
@click.argument('location',
379+
required=True,
380+
metavar='LOCATION',
381+
type=click.Path(
382+
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True))
383+
384+
@click.argument('output',
385+
required=True,
386+
metavar='OUTPUT')
387+
388+
@click.option('--from-inventory',
389+
metavar='FILE',
390+
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
391+
help='Path to an inventory CSV/JSON file as the base list for files/directories '
392+
'that need to be copied which have the \'redistribute\' flagged.')
393+
394+
@click.option('--with-structures',
395+
is_flag=True,
396+
help='Copy sources with directory structure.')
397+
398+
@click.option('--zip',
399+
is_flag=True,
400+
help='Zip the copied sources to the output location.')
401+
402+
@click.option('-q', '--quiet',
403+
is_flag=True,
404+
help='Do not print error or warning messages.')
405+
406+
@click.option('--verbose',
407+
is_flag=True,
408+
help='Show all error and warning messages.')
409+
410+
@click.help_option('-h', '--help')
411+
412+
def collect_redist_src(location, output, from_inventory, with_structures, zip, quiet, verbose):
413+
"""
414+
Collect sources that have 'redistribute' flagged in .ABOUT files or inventory
415+
to the output location.
416+
417+
LOCATION: Path to a directory containing sources that need to be copied
418+
(and containing ABOUT files if `inventory` is not provided)
419+
420+
OUTPUT: Path to a directory or a zip file where sources will be copied to.
421+
"""
422+
if zip:
423+
if not output.endswith('.zip'):
424+
click.echo('The output needs to be a zip file.')
425+
sys.exit()
426+
427+
if not quiet:
428+
print_version()
429+
click.echo('Collecting inventory from ABOUT files...')
430+
431+
if location.lower().endswith('.zip'):
432+
# accept zipped ABOUT files as input
433+
location = extract_zip(location)
434+
435+
if from_inventory:
436+
errors, abouts = load_inventory(from_inventory, location)
437+
else:
438+
errors, abouts = collect_inventory(location)
439+
440+
if zip:
441+
# Copy to a temp location and the zip to the ouput location
442+
output_location = get_temp_dir()
443+
else:
444+
output_location = output
445+
446+
copy_list, copy_list_errors = get_copy_list(abouts, location)
447+
copy_errors = copy_redist_src(copy_list, location, output_location, with_structures)
448+
449+
if zip:
450+
import shutil
451+
shutil.make_archive(output, 'zip', output_location)
452+
453+
errors.extend(copy_list_errors)
454+
errors.extend(copy_errors)
455+
errors_count = report_errors(errors, quiet, verbose, log_file_loc=output + '-error.log')
456+
if not quiet:
457+
msg = 'Redistributed sources are copied to {output}.'.format(**locals())
458+
click.echo(msg)
459+
sys.exit(errors_count)
460+
461+
369462
######################################################################
370463
# check subcommand
371464
######################################################################

src/attributecode/model.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import posixpath
3737
import traceback
3838

39-
from attributecode.util import python2
39+
from attributecode.util import python2, to_posix
4040

4141
if python2: # pragma: nocover
4242
from itertools import izip_longest as zip_longest # NOQA
@@ -63,11 +63,13 @@
6363
from attributecode.util import add_unc
6464
from attributecode.util import boolean_fields
6565
from attributecode.util import copy_license_notice_files
66+
from attributecode.util import copy_file
6667
from attributecode.util import csv
6768
from attributecode.util import file_fields
6869
from attributecode.util import filter_errors
6970
from attributecode.util import is_valid_name
7071
from attributecode.util import on_windows
72+
from attributecode.util import norm
7173
from attributecode.util import replace_tab_with_spaces
7274
from attributecode.util import wrap_boolean_value
7375
from attributecode.util import UNC_PREFIX
@@ -876,7 +878,7 @@ def hydrate(self, fields):
876878
# this is a special attribute, skip entirely
877879
continue
878880

879-
# A field that has been alredy processed ... and has a value
881+
# A field that has been already processed ... and has a value
880882
previous_value = seen_fields.get(name)
881883
if previous_value:
882884
if value != previous_value:
@@ -942,8 +944,9 @@ def process(self, fields, about_file_path, running_inventory=False,
942944
errors = self.hydrate(fields)
943945
# We want to copy the license_files before the validation
944946
if reference_dir:
945-
copy_license_notice_files(
947+
copy_err = copy_license_notice_files(
946948
fields, base_dir, reference_dir, afp)
949+
errors.extend(copy_err)
947950

948951
# TODO: why? we validate all fields, not only these hydrated
949952
validation_errors = validate_fields(
@@ -1289,6 +1292,113 @@ def get_field_names(abouts):
12891292
return fields
12901293

12911294

1295+
def copy_redist_src(copy_list, location, output, with_structure):
1296+
"""
1297+
Given a list of files/directories and copy to the destination
1298+
"""
1299+
errors = []
1300+
for from_path in copy_list:
1301+
norm_from_path = norm(from_path)
1302+
relative_from_path = norm_from_path.partition(util.norm(location))[2]
1303+
# Need to strip the '/' to use the join
1304+
if relative_from_path.startswith('/'):
1305+
relative_from_path = relative_from_path.partition('/')[2]
1306+
# Get the directory name of the output path
1307+
if with_structure:
1308+
output_dir = os.path.dirname(os.path.join(output, util.norm(relative_from_path)))
1309+
else:
1310+
output_dir = output
1311+
err = copy_file(from_path, output_dir)
1312+
if err:
1313+
errors.extend(err)
1314+
return errors
1315+
1316+
1317+
def get_copy_list(abouts, location):
1318+
"""
1319+
Return a list of files/directories that need to be copied (and error if any)
1320+
This is a summary list in a sense that if a directory is already in the list,
1321+
its children directories/files will not be included in the list regardless if
1322+
they have 'redistribute' flagged. The reason for this is we want to capture
1323+
the error/warning if existence files/directories already exist. However, if
1324+
we don't have this "summarized" list, and we've copied a file (with directory structure)
1325+
and then later on this file's parent directory also need to be copied, then
1326+
it will prompt warning as the directory that need to be copied is already exist.
1327+
Technically, this is correct, but it leads to confusion. Therefore, we want to
1328+
create a summarized list to avoid this kind of confusion.
1329+
"""
1330+
errors = []
1331+
copy_list = []
1332+
dir_list = []
1333+
file_list = []
1334+
for about in abouts:
1335+
if about.redistribute.value:
1336+
file_exist = True
1337+
for e in about.errors:
1338+
if 'Field about_resource' in e.message and 'not found' in e.message:
1339+
msg = e.message + u' and cannot be copied.'
1340+
errors.append(Error(CRITICAL, msg))
1341+
file_exist = False
1342+
continue
1343+
if file_exist:
1344+
for k in about.about_resource.value:
1345+
from_path = about.about_resource.value.get(k)
1346+
if on_windows:
1347+
norm_from_path = norm(from_path)
1348+
else:
1349+
norm_from_path = os.path.normpath(from_path)
1350+
# Get the relative path
1351+
relative_from_path = norm_from_path.partition(util.norm(location))[2]
1352+
if os.path.isdir(from_path):
1353+
if not dir_list:
1354+
dir_list.append(relative_from_path)
1355+
else:
1356+
handled = False
1357+
for dir in dir_list:
1358+
# The dir is a parent of the relative_from_path
1359+
if dir in relative_from_path:
1360+
handled = True
1361+
continue
1362+
# The relative_from_path is the parent of the dir
1363+
# We need to update the dir_list
1364+
if relative_from_path in dir:
1365+
dir_list.remove(dir)
1366+
dir_list.append(relative_from_path)
1367+
handled = True
1368+
continue
1369+
if not handled:
1370+
dir_list.append(relative_from_path)
1371+
else:
1372+
# Check if the file is from "root"
1373+
# If the file is at root level, it'll add to the copy_list
1374+
if not os.path.dirname(relative_from_path) == '/':
1375+
file_list.append(relative_from_path)
1376+
else:
1377+
copy_list.append(from_path)
1378+
1379+
for dir in dir_list:
1380+
for f in file_list:
1381+
# The file is already in one of copied directories
1382+
if dir in f:
1383+
file_list.remove(f)
1384+
continue
1385+
if dir.startswith('/'):
1386+
dir = dir.partition('/')[2]
1387+
absolute_path = os.path.join(location, dir)
1388+
if on_windows:
1389+
absolute_path = add_unc(absolute_path)
1390+
copy_list.append(absolute_path)
1391+
1392+
for f in file_list:
1393+
if f.startswith('/'):
1394+
f = f.partition('/')[2]
1395+
absolute_path = os.path.join(location, f)
1396+
if on_windows:
1397+
absolute_path = add_unc(absolute_path)
1398+
copy_list.append(absolute_path)
1399+
1400+
return copy_list, errors
1401+
12921402
def about_object_to_list_of_dictionary(abouts):
12931403
"""
12941404
Convert About objects to a list of dictionaries

0 commit comments

Comments
 (0)