diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d8ff4..d0d94fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## CHANGELOG +### 1.15.0 + +* Implement new option `--format=csv` + ### 1.14.0 * Implement new option `--from=mixed` as a mixed mode diff --git a/README.md b/README.md index bcdf12b..2ee4a6b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Dump the software license list of Python packages installed with pip. * [Confluence](#confluence) * [HTML](#html) * [JSON](#json) + * [CSV](#csv) * [Deprecated options](#deprecated-options) * [Option: summary](#option-summary) * [More Information](#more-information) @@ -291,6 +292,17 @@ When executed with the `--format=json` option, you can output list in JSON forma ``` +#### CSV + +When executed with the `--format=csv` option, you can output list in quoted CSV format. Useful when you want to copy/paste the output to an excel sheet. + +```bash +(venv) $ pip-licenses --format=csv +"Name","Version","License" +"Django","2.0.2","BSD" +"pytz","2017.3","MIT" +``` + #### Deprecated options The following options will be deprecated in version 2.0.0. Please migrate to `--format` option. diff --git a/dev-requirements.txt b/dev-requirements.txt index a12b6df..db4d72b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile -U dev-requirements.in +# pip-compile dev-requirements.in # apipkg==1.5 # via execnet atomicwrites==1.3.0 # via pytest @@ -42,4 +42,4 @@ tqdm==4.31.1 # via twine twine==1.13.0 urllib3==1.24.3 # via requests webencodings==0.5.1 # via bleach -wheel==0.33.2 +wheel==0.33.3 diff --git a/piplicenses.py b/piplicenses.py index 071c6a4..0c5d509 100644 --- a/piplicenses.py +++ b/piplicenses.py @@ -45,7 +45,7 @@ HEADER as RULE_HEADER, NONE as RULE_NONE) __pkgname__ = 'pip-licenses' -__version__ = '1.14.0' +__version__ = '1.15.0' __author__ = 'raimon' __license__ = 'MIT License' __summary__ = ('Dump the software license list of ' @@ -253,6 +253,39 @@ def get_string(self, **kwargs): return json.dumps(lines, indent=2, sort_keys=True) +class CSVPrettyTable(PrettyTable): + """PrettyTable-like class exporting to CSV""" + + def get_string(self, **kwargs): + + def esc_quotes(val): + """ + Meta-escaping double quotes + https://tools.ietf.org/html/rfc4180 + """ + try: + return val.replace('"', '""') + except UnicodeDecodeError: # pragma: no cover + return val.decode('utf-8').replace('"', '""') + except UnicodeEncodeError: # pragma: no cover + return val.encode('unicode_escape').replace('"', '""') + + options = self._get_options(kwargs) + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows, options) + + lines = [] + formatted_header = ','.join(['"%s"' % (esc_quotes(val), ) + for val in self._field_names]) + lines.append(formatted_header) + for row in formatted_rows: + formatted_row = ','.join(['"%s"' % (esc_quotes(val), ) + for val in row]) + lines.append(formatted_row) + + return '\n'.join(lines) + + def factory_styled_table_with_args(args, output_fields=DEFAULT_OUTPUT_FIELDS): table = PrettyTable() table.field_names = output_fields @@ -272,6 +305,8 @@ def factory_styled_table_with_args(args, output_fields=DEFAULT_OUTPUT_FIELDS): table.hrules = RULE_NONE elif args.format == 'json': table = JsonPrettyTable(table.field_names) + elif args.format == 'csv': + table = CSVPrettyTable(table.field_names) return table @@ -444,6 +479,9 @@ def _compatible_format_args(self, args): if format_input in ('json', 'j'): args.format = 'json' + if format_input in ('csv', ): + args.format = 'csv' + if args.from_classifier: setattr(args, 'from', 'classifier') @@ -512,7 +550,7 @@ def create_parser(): default='plain', metavar='STYLE', help=('dump as set format style\n' '"plain", "markdown", "rst", "confluence",\n' - '"html", "json"\n' + '"html", "json", "csv"\n' 'default: --format=plain')) parser.add_argument('-m', '--format-markdown', action='store_true', diff --git a/test_piplicenses.py b/test_piplicenses.py index 4dbbf22..c4f2bfc 100644 --- a/test_piplicenses.py +++ b/test_piplicenses.py @@ -319,6 +319,15 @@ def test_format_json(self): self.assertIn('"Author":', output_string) self.assertNotIn('"URL":', output_string) + def test_format_csv(self): + format_csv_args = ['--format=csv', '--with-authors'] + args = self.parser.parse_args(format_csv_args) + output_string = create_output_string(args) + + obtained_header = output_string.split('\n', 1)[0] + expected_header = '"Name","Version","License","Author"' + self.assertEqual(obtained_header, expected_header) + def test_from_compatibility(self): from_old_style_args = ['--from-classifier'] args = self.parser.parse_args(from_old_style_args)