From e5b43d4bf476ddf299e7ecfe4a0645b515f4b176 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Wed, 9 Oct 2019 00:52:49 -0700 Subject: [PATCH 01/27] Extended the file type export options to include not only JSON but also XML and CSV --- beetsplug/export.py | 130 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index d783f5b933..cd91fc3938 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -18,8 +18,10 @@ from __future__ import division, absolute_import, print_function import sys -import json import codecs +import json +import csv +import xml.etree.ElementTree as ET from datetime import datetime, date from beets.plugins import BeetsPlugin @@ -52,6 +54,24 @@ def __init__(self): 'sort_keys': True } }, + 'csv': { + # csv module formatting options + 'formatting': { + 'ensure_ascii': False, + 'indent': 0, + 'separators': (','), + 'sort_keys': True + } + }, + 'xml': { + # xml module formatting options + 'formatting': { + 'ensure_ascii': False, + 'indent': 4, + 'separators': (','), + 'sort_keys': True + } + } # TODO: Use something like the edit plugin # 'item_fields': [] }) @@ -78,17 +98,22 @@ def commands(self): u'-o', u'--output', help=u'path for the output file. If not given, will print the data' ) + cmd.parser.add_option( + u'-f', u'--format', default='json', + help=u'specify the format of the exported data. Your options are json (deafult), csv, and xml' + ) return [cmd] def run(self, lib, opts, args): file_path = opts.output - file_format = self.config['default_format'].get(str) file_mode = 'a' if opts.append else 'w' + file_format = opts.format format_options = self.config[file_format]['formatting'].get(dict) export_format = ExportFormat.factory( - file_format, **{ + file_type=file_format, + **{ 'file_path': file_path, 'file_mode': file_mode } @@ -108,44 +133,107 @@ def run(self, lib, opts, args): except (mediafile.UnreadableFileError, IOError) as ex: self._log.error(u'cannot read file: {0}', ex) continue - data = key_filter(data) items += [data] - export_format.export(items, **format_options) class ExportFormat(object): """The output format type""" + def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): + self.path = file_path + self.mode = file_mode + self.encoding = encoding @classmethod - def factory(cls, type, **kwargs): - if type == "json": - if kwargs['file_path']: - return JsonFileFormat(**kwargs) - else: - return JsonPrintFormat() - raise NotImplementedError() + def factory(cls, file_type, **kwargs): + if file_type == "json": + return JsonFormat(**kwargs) + elif file_type == "csv": + return CSVFormat(**kwargs) + elif file_type == "xml": + return XMLFormat(**kwargs) + else: + raise NotImplementedError() def export(self, data, **kwargs): raise NotImplementedError() -class JsonPrintFormat(ExportFormat): - """Outputs to the console""" +class JsonFormat(ExportFormat): + """Saves in a json file""" + def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): + super(JsonFormat, self).__init__(file_path, file_mode, encoding) + self.export = self.export_to_file if self.path else self.export_to_terminal - def export(self, data, **kwargs): + def export_to_terminal(self, data, **kwargs): json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) + def export_to_file(self, data, **kwargs): + with codecs.open(self.path, self.mode, self.encoding) as f: + json.dump(data, f, cls=ExportEncoder, **kwargs) -class JsonFileFormat(ExportFormat): - """Saves in a json file""" +class CSVFormat(ExportFormat): + """Saves in a csv file""" def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): - self.path = file_path - self.mode = file_mode - self.encoding = encoding + super(CSVFormat, self).__init__(file_path, file_mode, encoding) + self.header = [] def export(self, data, **kwargs): + if data and type(data) is list and len(data) > 0: + self.header = list(data[0].keys()) + if self.path: + self.export_to_file(data, **kwargs) + else: + self.export_to_terminal(data, **kwargs) + + def export_to_terminal(self, data, **kwargs): + writer = csv.DictWriter(sys.stdout, fieldnames=self.header) + writer.writeheader() + writer.writerows(data) + + def export_to_file(self, data, **kwargs): with codecs.open(self.path, self.mode, self.encoding) as f: - json.dump(data, f, cls=ExportEncoder, **kwargs) + writer = csv.DictWriter(f, fieldnames=self.header) + writer.writeheader() + writer.writerows(data) + + +class XMLFormat(ExportFormat): + """Saves in a xml file""" + def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): + super(XMLFormat, self).__init__(file_path, file_mode, encoding) + + def export(self, data, **kwargs): + # create the file structure + library = ET.Element('library') + tracks_key = ET.SubElement(library, 'key') + tracks_key.text = "Tracks" + tracks_dict = ET.SubElement(library, 'dict') + if data and type(data) is list \ + and len(data) > 0 and type(data[0]) is dict: + index = 1 + for item in data: + track_key = ET.SubElement(tracks_dict, 'key') + track_key.text = str(index) + track_dict = ET.SubElement(tracks_dict, 'dict') + track_details = ET.SubElement(track_dict, 'Track ID') + track_details.text = str(index) + index += 1 + for key, value in item.items(): + track_details = ET.SubElement(track_dict, key) + track_details.text = value + data = str(ET.tostring(library, encoding=self.encoding)) + #data = ET.dump(library) + if self.path: + self.export_to_file(data, **kwargs) + else: + self.export_to_terminal(data, **kwargs) + + def export_to_terminal(self, data, **kwargs): + print(data) + + def export_to_file(self, data, **kwargs): + with codecs.open(self.path, self.mode, self.encoding) as f: + f.write(data) \ No newline at end of file From 93e10264fb31776a7bbf552d3e11f1af2047f20c Mon Sep 17 00:00:00 2001 From: Austin Marino <32184751+austinmm@users.noreply.github.com> Date: Thu, 10 Oct 2019 16:26:20 -0700 Subject: [PATCH 02/27] Updated export.rst This update reflects the code changes I made to the export plugin --- docs/plugins/export.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index d712dfc8b3..4809df58b3 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -2,9 +2,11 @@ Export Plugin ============= The ``export`` plugin lets you get data from the items and export the content -as `JSON`_. +as `JSON`_, `CSV`_, or `XML`_. .. _JSON: https://www.json.org +.. _CSV: https://fileinfo.com/extension/csv +.. _XML: https://fileinfo.com/extension/xml Enable the ``export`` plugin (see :ref:`using-plugins` for help). Then, type ``beet export`` followed by a :doc:`query ` to get the data from your library. For example, run this:: @@ -36,11 +38,13 @@ The ``export`` command has these command-line options: * ``--append``: Appends the data to the file instead of writing. +* ``--format`` or ``-f``: Specifies the format of the exported data. If not informed, JSON will be used. + Configuration ------------- To configure the plugin, make a ``export:`` section in your configuration -file. Under the ``json`` key, these options are available: +file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available: - **ensure_ascii**: Escape non-ASCII characters with ``\uXXXX`` entities. @@ -63,3 +67,15 @@ The default options look like this:: indent: 4 separators: [',' , ': '] sort_keys: true + csv: + formatting: + ensure_ascii: False + indent: 0 + separators: [','] + sort_keys: true + xml: + formatting: + ensure_ascii: False + indent: 4 + separators: ['>'] + sort_keys: true From 4dfb6b9fae26fe1602def5fc4ac6f855dda0a93e Mon Sep 17 00:00:00 2001 From: Austin Marino <32184751+austinmm@users.noreply.github.com> Date: Thu, 10 Oct 2019 16:32:24 -0700 Subject: [PATCH 03/27] Added examples of new format option --- docs/plugins/export.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 4809df58b3..d548a5e66f 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -15,6 +15,7 @@ your library. For example, run this:: to print a JSON file containing information about your Beatles tracks. + Command-Line Options -------------------- @@ -38,7 +39,12 @@ The ``export`` command has these command-line options: * ``--append``: Appends the data to the file instead of writing. -* ``--format`` or ``-f``: Specifies the format of the exported data. If not informed, JSON will be used. +* ``--format`` or ``-f``: Specifies the format the data will be exported as. If not informed, JSON will be used by default. + For example:: + + $ beet export -f csv beatles + $ beet export -f json beatles + $ beet export -f xml beatles Configuration ------------- From a8a480a691c478a60e5733eeed70b8597433dbd1 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Thu, 10 Oct 2019 16:33:46 -0700 Subject: [PATCH 04/27] Updated config formats --- beetsplug/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index cd91fc3938..0666747b22 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -68,7 +68,7 @@ def __init__(self): 'formatting': { 'ensure_ascii': False, 'indent': 4, - 'separators': (','), + 'separators': (''), 'sort_keys': True } } From 8ff875bded5e8a9415b9c47389131d788aa98efd Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Thu, 10 Oct 2019 19:34:57 -0700 Subject: [PATCH 05/27] Addec Unit test for export plugin --- test/test_export.py | 98 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 test/test_export.py diff --git a/test/test_export.py b/test/test_export.py new file mode 100644 index 0000000000..ad00f83e78 --- /dev/null +++ b/test/test_export.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2019, Carl Suster +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Test the beets.export utilities associated with the export plugin. +""" + +from __future__ import division, absolute_import, print_function + +import unittest +from test import helper +from test.helper import TestHelper +#from beetsplug.export import ExportPlugin, ExportFormat, JSONFormat, CSVFormat, XMLFormat +#from collections import namedtuple + + +class ExportPluginTest(unittest.TestCase, TestHelper): + def setUp(self): + self.setup_beets() + self.load_plugins('export') + + def tearDown(self): + self.unload_plugins() + self.teardown_beets() + + def test_json_output(self): + item1, item2 = self.add_item_fixtures(count=2) + item1.album = 'talbum' + item1.artist = "tartist" + item1.track = "ttrack" + item1.write() + item1.store() + + out = self.run_with_output('export', '-f json -i "track,album" tartist') + self.assertIn('"track": "' + item1.track + '"', out) + self.assertIn('"album": "' + item1.album + '"', out) + + def test_csv_output(self): + item1, item2 = self.add_item_fixtures(count=2) + item1.album = 'talbum' + item1.artist = "tartist" + item1.track = "ttrack" + item1.write() + item1.store() + + out = self.run_with_output('export', '-f json -i "track,album" tartist') + self.assertIn(item1.track + ',' + item1.album, out) + + def test_xml_output(self): + item1, item2 = self.add_item_fixtures(count=2) + item1.album = 'talbum' + item1.artist = "tartist" + item1.track = "ttrack" + item1.write() + item1.store() + + out = self.run_with_output('export', '-f json -i "track,album" tartist') + self.assertIn("" + item1.track + "", out) + self.assertIn("" + item1.album + "", out) + + """ + def setUp(self): + Opts = namedtuple('Opts', 'output append included_keys library format') + self.args = None + self._export = ExportPlugin() + included_keys = ['title,artist,album'] + self.opts = Opts(None, False, included_keys, True, "json") + self.export_format_classes = {"json": ExportFormat, "csv": CSVFormat, "xml": XMLFormat} + + def test_run(self, _format="json"): + self.opts.format = _format + self._export.run(lib=self.lib, opts=self.opts, args=self.args) + # 1.) Test that the ExportFormat Factory class method invoked the correct class + self.assertEqual(type(self._export.export_format), self.export_format_classes[_format]) + # 2.) Test that the cmd parser options specified were processed in correctly + self.assertEqual(self._export.export_format.path, self.opts.output) + mode = 'a' if self.opts.append else 'w' + self.assertEqual(self._export.export_format.mode, mode) + """ + + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') \ No newline at end of file From c31b488e549a2efdf3819654c82f8c5d41ced5ae Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Thu, 10 Oct 2019 19:35:49 -0700 Subject: [PATCH 06/27] Updated class fields to allow for easier unit testing --- beetsplug/export.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 0666747b22..63939f7e49 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -42,6 +42,9 @@ class ExportPlugin(BeetsPlugin): def __init__(self): super(ExportPlugin, self).__init__() + # Used when testing export plugin + self.run_results = None + self.export_format = None self.config.add({ 'default_format': 'json', @@ -68,7 +71,7 @@ def __init__(self): 'formatting': { 'ensure_ascii': False, 'indent': 4, - 'separators': (''), + 'separators': ('>'), 'sort_keys': True } } @@ -105,13 +108,12 @@ def commands(self): return [cmd] def run(self, lib, opts, args): - file_path = opts.output file_mode = 'a' if opts.append else 'w' file_format = opts.format format_options = self.config[file_format]['formatting'].get(dict) - export_format = ExportFormat.factory( + self.export_format = ExportFormat.factory( file_type=file_format, **{ 'file_path': file_path, @@ -135,7 +137,9 @@ def run(self, lib, opts, args): continue data = key_filter(data) items += [data] - export_format.export(items, **format_options) + + self.run_results = items + self.export_format.export(self.run_results, **format_options) class ExportFormat(object): @@ -144,6 +148,8 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding + # Used for testing + self.results = None @classmethod def factory(cls, file_type, **kwargs): @@ -167,11 +173,13 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.export = self.export_to_file if self.path else self.export_to_terminal def export_to_terminal(self, data, **kwargs): - json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) + r = json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) + self.results = str(r) def export_to_file(self, data, **kwargs): with codecs.open(self.path, self.mode, self.encoding) as f: - json.dump(data, f, cls=ExportEncoder, **kwargs) + r = json.dump(data, f, cls=ExportEncoder, **kwargs) + self.results = str(r) class CSVFormat(ExportFormat): @@ -192,12 +200,15 @@ def export_to_terminal(self, data, **kwargs): writer = csv.DictWriter(sys.stdout, fieldnames=self.header) writer.writeheader() writer.writerows(data) + self.results = str(writer) + def export_to_file(self, data, **kwargs): with codecs.open(self.path, self.mode, self.encoding) as f: writer = csv.DictWriter(f, fieldnames=self.header) writer.writeheader() writer.writerows(data) + self.results = str(writer) class XMLFormat(ExportFormat): @@ -233,7 +244,9 @@ def export(self, data, **kwargs): def export_to_terminal(self, data, **kwargs): print(data) + self.results = str(data) def export_to_file(self, data, **kwargs): with codecs.open(self.path, self.mode, self.encoding) as f: - f.write(data) \ No newline at end of file + f.write(data) + self.results = str(data) \ No newline at end of file From 0e2c1e0d56c857437d6230b838c7365bc29c955c Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Sat, 12 Oct 2019 14:47:44 -0700 Subject: [PATCH 07/27] Made changes to reflect comments and suggestions made by sampsyo --- beetsplug/export.py | 79 +++++++++++------------------------------ docs/plugins/export.rst | 6 +--- 2 files changed, 21 insertions(+), 64 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 63939f7e49..0288e88c61 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -42,9 +42,6 @@ class ExportPlugin(BeetsPlugin): def __init__(self): super(ExportPlugin, self).__init__() - # Used when testing export plugin - self.run_results = None - self.export_format = None self.config.add({ 'default_format': 'json', @@ -60,19 +57,18 @@ def __init__(self): 'csv': { # csv module formatting options 'formatting': { - 'ensure_ascii': False, - 'indent': 0, - 'separators': (','), - 'sort_keys': True + 'delimiter': ',', # column seperator + 'dialect': 'excel', # the name of the dialect to use + 'quotechar': '|' } }, 'xml': { # xml module formatting options 'formatting': { - 'ensure_ascii': False, - 'indent': 4, - 'separators': ('>'), - 'sort_keys': True + 'encoding': 'unicode', # the output encoding + 'xml_declaration':'True', # controls if an XML declaration should be added to the file + 'method': 'xml', # either "xml", "html" or "text" (default is "xml") + 'short_empty_elements': 'True' # controls the formatting of elements that contain no content. } } # TODO: Use something like the edit plugin @@ -103,17 +99,17 @@ def commands(self): ) cmd.parser.add_option( u'-f', u'--format', default='json', - help=u'specify the format of the exported data. Your options are json (deafult), csv, and xml' + help=u"the output format: json (default), csv, or xml" ) return [cmd] def run(self, lib, opts, args): file_path = opts.output file_mode = 'a' if opts.append else 'w' - file_format = opts.format + file_format = opts.format if opts.format else self.config['default_format'].get(str) format_options = self.config[file_format]['formatting'].get(dict) - self.export_format = ExportFormat.factory( + export_format = ExportFormat.factory( file_type=file_format, **{ 'file_path': file_path, @@ -135,11 +131,11 @@ def run(self, lib, opts, args): except (mediafile.UnreadableFileError, IOError) as ex: self._log.error(u'cannot read file: {0}', ex) continue + data = key_filter(data) items += [data] - self.run_results = items - self.export_format.export(self.run_results, **format_options) + export_format.export(items, **format_options) class ExportFormat(object): @@ -148,8 +144,8 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding - # Used for testing - self.results = None + # out_stream is assigned sys.stdout (terminal output) or the file stream for the path specified + self.out_stream = codecs.open(self.path, self.mode, self.encoding) if self.path else sys.stdout @classmethod def factory(cls, file_type, **kwargs): @@ -170,16 +166,9 @@ class JsonFormat(ExportFormat): """Saves in a json file""" def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): super(JsonFormat, self).__init__(file_path, file_mode, encoding) - self.export = self.export_to_file if self.path else self.export_to_terminal - - def export_to_terminal(self, data, **kwargs): - r = json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) - self.results = str(r) - def export_to_file(self, data, **kwargs): - with codecs.open(self.path, self.mode, self.encoding) as f: - r = json.dump(data, f, cls=ExportEncoder, **kwargs) - self.results = str(r) + def export(self, data, **kwargs): + json.dump(data, self.out_stream, cls=ExportEncoder, **kwargs) class CSVFormat(ExportFormat): @@ -189,26 +178,11 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.header = [] def export(self, data, **kwargs): - if data and type(data) is list and len(data) > 0: + if data and len(data) > 0: self.header = list(data[0].keys()) - if self.path: - self.export_to_file(data, **kwargs) - else: - self.export_to_terminal(data, **kwargs) - - def export_to_terminal(self, data, **kwargs): - writer = csv.DictWriter(sys.stdout, fieldnames=self.header) + writer = csv.DictWriter(self.out_stream, fieldnames=self.header, **kwargs) writer.writeheader() writer.writerows(data) - self.results = str(writer) - - - def export_to_file(self, data, **kwargs): - with codecs.open(self.path, self.mode, self.encoding) as f: - writer = csv.DictWriter(f, fieldnames=self.header) - writer.writeheader() - writer.writerows(data) - self.results = str(writer) class XMLFormat(ExportFormat): @@ -235,18 +209,5 @@ def export(self, data, **kwargs): for key, value in item.items(): track_details = ET.SubElement(track_dict, key) track_details.text = value - data = str(ET.tostring(library, encoding=self.encoding)) - #data = ET.dump(library) - if self.path: - self.export_to_file(data, **kwargs) - else: - self.export_to_terminal(data, **kwargs) - - def export_to_terminal(self, data, **kwargs): - print(data) - self.results = str(data) - - def export_to_file(self, data, **kwargs): - with codecs.open(self.path, self.mode, self.encoding) as f: - f.write(data) - self.results = str(data) \ No newline at end of file + tree = ET.ElementTree(library) + tree.write(self.out_stream, **kwargs) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index d548a5e66f..1cd9b09d34 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -40,11 +40,7 @@ The ``export`` command has these command-line options: * ``--append``: Appends the data to the file instead of writing. * ``--format`` or ``-f``: Specifies the format the data will be exported as. If not informed, JSON will be used by default. - For example:: - - $ beet export -f csv beatles - $ beet export -f json beatles - $ beet export -f xml beatles +The format options include csv, json and xml. Configuration ------------- From ec705fae1e4ad2e665cdfb4b5f765b43034bf447 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Sat, 12 Oct 2019 15:41:06 -0700 Subject: [PATCH 08/27] Updated documents and comments to reflcet recent code changes. Cleaned up code to better follow PEP-8 conventions and just work more efficiently all around. --- beetsplug/export.py | 36 ++++++++++++++++-------------------- docs/plugins/export.rst | 16 +++++++--------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 0288e88c61..43417efead 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -46,7 +46,7 @@ def __init__(self): self.config.add({ 'default_format': 'json', 'json': { - # json module formatting options + # JSON module formatting options. 'formatting': { 'ensure_ascii': False, 'indent': 4, @@ -55,20 +55,19 @@ def __init__(self): } }, 'csv': { - # csv module formatting options + # CSV module formatting options. 'formatting': { - 'delimiter': ',', # column seperator - 'dialect': 'excel', # the name of the dialect to use - 'quotechar': '|' + 'delimiter': ',', # The delimiter used to seperate columns. + 'dialect': 'excel' # The type of dialect to use when formating the file output. } }, 'xml': { - # xml module formatting options + # XML module formatting options. 'formatting': { - 'encoding': 'unicode', # the output encoding - 'xml_declaration':'True', # controls if an XML declaration should be added to the file - 'method': 'xml', # either "xml", "html" or "text" (default is "xml") - 'short_empty_elements': 'True' # controls the formatting of elements that contain no content. + 'encoding': 'unicode', # The output encoding. + 'xml_declaration':True, # Controls if an XML declaration should be added to the file. + 'method': 'xml', # Can be either "xml", "html" or "text" (default is "xml"). + 'short_empty_elements': True # Controls the formatting of elements that contain no content. } } # TODO: Use something like the edit plugin @@ -123,6 +122,7 @@ def run(self, lib, opts, args): included_keys = [] for keys in opts.included_keys: included_keys.extend(keys.split(',')) + key_filter = make_key_filter(included_keys) for data_emitter in data_collector(lib, ui.decargs(args)): @@ -144,7 +144,7 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding - # out_stream is assigned sys.stdout (terminal output) or the file stream for the path specified + # Assigned sys.stdout (terminal output) or the file stream for the path specified. self.out_stream = codecs.open(self.path, self.mode, self.encoding) if self.path else sys.stdout @classmethod @@ -175,11 +175,9 @@ class CSVFormat(ExportFormat): """Saves in a csv file""" def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): super(CSVFormat, self).__init__(file_path, file_mode, encoding) - self.header = [] def export(self, data, **kwargs): - if data and len(data) > 0: - self.header = list(data[0].keys()) + header = list(data[0].keys()) if data else [] writer = csv.DictWriter(self.out_stream, fieldnames=self.header, **kwargs) writer.writeheader() writer.writerows(data) @@ -191,23 +189,21 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): super(XMLFormat, self).__init__(file_path, file_mode, encoding) def export(self, data, **kwargs): - # create the file structure + # Creates the XML file structure. library = ET.Element('library') tracks_key = ET.SubElement(library, 'key') tracks_key.text = "Tracks" tracks_dict = ET.SubElement(library, 'dict') - if data and type(data) is list \ - and len(data) > 0 and type(data[0]) is dict: - index = 1 - for item in data: + if data and isinstance(data[0], dict): + for index, item in enumerate(data): track_key = ET.SubElement(tracks_dict, 'key') track_key.text = str(index) track_dict = ET.SubElement(tracks_dict, 'dict') track_details = ET.SubElement(track_dict, 'Track ID') track_details.text = str(index) - index += 1 for key, value in item.items(): track_details = ET.SubElement(track_dict, key) track_details.text = value + tree = ET.ElementTree(library) tree.write(self.out_stream, **kwargs) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 1cd9b09d34..f7f9e02178 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -68,16 +68,14 @@ The default options look like this:: ensure_ascii: False indent: 4 separators: [',' , ': '] - sort_keys: true + sort_keys: True csv: formatting: - ensure_ascii: False - indent: 0 - separators: [','] - sort_keys: true + delimiter: ',' + dialect: 'excel' xml: formatting: - ensure_ascii: False - indent: 4 - separators: ['>'] - sort_keys: true + encoding: 'unicode', + xml_declaration: True, + method: 'xml' + short_empty_elements: True From c5ebbe0b783928f0461ea0b5adce6d413d7ff90a Mon Sep 17 00:00:00 2001 From: Austin Marino <32184751+austinmm@users.noreply.github.com> Date: Sat, 12 Oct 2019 15:56:16 -0700 Subject: [PATCH 09/27] Added Comments to formating configurations --- docs/plugins/export.rst | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index f7f9e02178..ba9ae28080 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -71,11 +71,34 @@ The default options look like this:: sort_keys: True csv: formatting: + /* + Used as the separating character between fields. The default value is a comma (,). + */ delimiter: ',' - dialect: 'excel' + /* + A dialect, in the context of reading and writing CSVs, + is a construct that allows you to create, store, + and re-use various formatting parameters for your data. + */ + dialect: 'excel' xml: formatting: + /* + Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). + */ encoding: 'unicode', + /* + Controls if an XML declaration should be added to the file. + Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). + */ xml_declaration: True, + /* + Can be either "xml", "html" or "text" (default is "xml") + */ method: 'xml' + /* + Controls the formatting of elements that contain no content. + If True (the default), they are emitted as a single self-closed tag, + otherwise they are emitted as a pair of start/end tags. + */ short_empty_elements: True From fa2c9ba2592c408949e6fa6ca00d5eebeee8aa45 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Sun, 13 Oct 2019 11:36:33 -0700 Subject: [PATCH 10/27] Aligned export related code with flake8 standards --- beetsplug/export.py | 33 ++++++++++++++++++++++----------- test/test_export.py | 42 +++++++++--------------------------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 43417efead..2f6af072ef 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -57,17 +57,23 @@ def __init__(self): 'csv': { # CSV module formatting options. 'formatting': { - 'delimiter': ',', # The delimiter used to seperate columns. - 'dialect': 'excel' # The type of dialect to use when formating the file output. + # The delimiter used to seperate columns. + 'delimiter': ',', + # The dialect to use when formating the file output. + 'dialect': 'excel' } }, 'xml': { # XML module formatting options. 'formatting': { - 'encoding': 'unicode', # The output encoding. - 'xml_declaration':True, # Controls if an XML declaration should be added to the file. - 'method': 'xml', # Can be either "xml", "html" or "text" (default is "xml"). - 'short_empty_elements': True # Controls the formatting of elements that contain no content. + # The output encoding. + 'encoding': 'unicode', + # Controls if XML declaration should be added to the file. + 'xml_declaration': True, + # Can be either "xml", "html" or "text" (default is "xml"). + 'method': 'xml', + # Controls formatting of elements that contain no content. + 'short_empty_elements': True } } # TODO: Use something like the edit plugin @@ -105,11 +111,12 @@ def commands(self): def run(self, lib, opts, args): file_path = opts.output file_mode = 'a' if opts.append else 'w' - file_format = opts.format if opts.format else self.config['default_format'].get(str) + file_format = opts.format if opts.format else \ + self.config['default_format'].get(str) format_options = self.config[file_format]['formatting'].get(dict) export_format = ExportFormat.factory( - file_type=file_format, + file_type=file_format, **{ 'file_path': file_path, 'file_mode': file_mode @@ -144,8 +151,12 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding - # Assigned sys.stdout (terminal output) or the file stream for the path specified. - self.out_stream = codecs.open(self.path, self.mode, self.encoding) if self.path else sys.stdout + """ self.out_stream = + sys.stdout if path doesn't exit + codecs.open(..) else + """ + self.out_stream = codecs.open(self.path, self.mode, self.encoding) \ + if self.path else sys.stdout @classmethod def factory(cls, file_type, **kwargs): @@ -178,7 +189,7 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): def export(self, data, **kwargs): header = list(data[0].keys()) if data else [] - writer = csv.DictWriter(self.out_stream, fieldnames=self.header, **kwargs) + writer = csv.DictWriter(self.out_stream, fieldnames=header, **kwargs) writer.writeheader() writer.writerows(data) diff --git a/test/test_export.py b/test/test_export.py index ad00f83e78..9d7c4a457b 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -19,10 +19,7 @@ from __future__ import division, absolute_import, print_function import unittest -from test import helper from test.helper import TestHelper -#from beetsplug.export import ExportPlugin, ExportFormat, JSONFormat, CSVFormat, XMLFormat -#from collections import namedtuple class ExportPluginTest(unittest.TestCase, TestHelper): @@ -41,11 +38,11 @@ def test_json_output(self): item1.track = "ttrack" item1.write() item1.store() - - out = self.run_with_output('export', '-f json -i "track,album" tartist') + options = '-f json -i "track,album" ' + item1.artist + out = self.run_with_output('export', options) self.assertIn('"track": "' + item1.track + '"', out) self.assertIn('"album": "' + item1.album + '"', out) - + def test_csv_output(self): item1, item2 = self.add_item_fixtures(count=2) item1.album = 'talbum' @@ -53,10 +50,10 @@ def test_csv_output(self): item1.track = "ttrack" item1.write() item1.store() - - out = self.run_with_output('export', '-f json -i "track,album" tartist') + options = '-f csv -i "track,album" ' + item1.artist + out = self.run_with_output('export', options) self.assertIn(item1.track + ',' + item1.album, out) - + def test_xml_output(self): item1, item2 = self.add_item_fixtures(count=2) item1.album = 'talbum' @@ -64,35 +61,14 @@ def test_xml_output(self): item1.track = "ttrack" item1.write() item1.store() - - out = self.run_with_output('export', '-f json -i "track,album" tartist') + options = '-f xml -i "track,album" ' + item1.artist + out = self.run_with_output('export', options) self.assertIn("" + item1.track + "", out) self.assertIn("" + item1.album + "", out) - """ - def setUp(self): - Opts = namedtuple('Opts', 'output append included_keys library format') - self.args = None - self._export = ExportPlugin() - included_keys = ['title,artist,album'] - self.opts = Opts(None, False, included_keys, True, "json") - self.export_format_classes = {"json": ExportFormat, "csv": CSVFormat, "xml": XMLFormat} - - def test_run(self, _format="json"): - self.opts.format = _format - self._export.run(lib=self.lib, opts=self.opts, args=self.args) - # 1.) Test that the ExportFormat Factory class method invoked the correct class - self.assertEqual(type(self._export.export_format), self.export_format_classes[_format]) - # 2.) Test that the cmd parser options specified were processed in correctly - self.assertEqual(self._export.export_format.path, self.opts.output) - mode = 'a' if self.opts.append else 'w' - self.assertEqual(self._export.export_format.mode, mode) - """ - - def suite(): return unittest.TestLoader().loadTestsFromName(__name__) if __name__ == '__main__': - unittest.main(defaultTest='suite') \ No newline at end of file + unittest.main(defaultTest='suite') From 294b3cdb8cf4ed63494f33cf00aa101c0dd7754e Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Sun, 13 Oct 2019 11:41:06 -0700 Subject: [PATCH 11/27] updated format descriptions --- docs/plugins/export.rst | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index ba9ae28080..ea2624094b 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -56,6 +56,18 @@ file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available - **sort_keys**: Sorts the keys in JSON dictionaries. +- **delimiter**: Used as the separating character between fields. The default value is a comma (,). + +- **dialect**: A dialect, in the context of reading and writing CSVs, is a construct that allows you to create, store, and re-use various formatting parameters for your data. + +- **encoding**: Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). + +- **xml_declaration**: Controls if an XML declaration should be added to the file. Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). + +- **method**: Can be either "xml", "html" or "text" (default is "xml") + +- **short_empty_elements**: Controls the formatting of elements that contain no content. If True (the default), they are emitted as a single self-closed tag, otherwise they are emitted as a pair of start/end tags. + These options match the options from the `Python json module`_. .. _Python json module: https://docs.python.org/2/library/json.html#basic-usage @@ -71,34 +83,11 @@ The default options look like this:: sort_keys: True csv: formatting: - /* - Used as the separating character between fields. The default value is a comma (,). - */ delimiter: ',' - /* - A dialect, in the context of reading and writing CSVs, - is a construct that allows you to create, store, - and re-use various formatting parameters for your data. - */ dialect: 'excel' xml: formatting: - /* - Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). - */ - encoding: 'unicode', - /* - Controls if an XML declaration should be added to the file. - Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). - */ - xml_declaration: True, - /* - Can be either "xml", "html" or "text" (default is "xml") - */ + encoding: 'unicode' + xml_declaration: True method: 'xml' - /* - Controls the formatting of elements that contain no content. - If True (the default), they are emitted as a single self-closed tag, - otherwise they are emitted as a pair of start/end tags. - */ short_empty_elements: True From 4f0a2b78a32242f50406fd26503e510ca015d1d5 Mon Sep 17 00:00:00 2001 From: Austin Marino <32184751+austinmm@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:45:33 -0700 Subject: [PATCH 12/27] Updated Format description layout --- docs/plugins/export.rst | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index ea2624094b..0a27c101bb 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -48,25 +48,28 @@ Configuration To configure the plugin, make a ``export:`` section in your configuration file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available: -- **ensure_ascii**: Escape non-ASCII characters with ``\uXXXX`` entities. +- **JSON Formatting** + - **ensure_ascii**: Escape non-ASCII characters with ``\uXXXX`` entities. -- **indent**: The number of spaces for indentation. + - **indent**: The number of spaces for indentation. -- **separators**: A ``[item_separator, dict_separator]`` tuple. + - **separators**: A ``[item_separator, dict_separator]`` tuple. -- **sort_keys**: Sorts the keys in JSON dictionaries. + - **sort_keys**: Sorts the keys in JSON dictionaries. -- **delimiter**: Used as the separating character between fields. The default value is a comma (,). +- **CSV Formatting** + - **delimiter**: Used as the separating character between fields. The default value is a comma (,). -- **dialect**: A dialect, in the context of reading and writing CSVs, is a construct that allows you to create, store, and re-use various formatting parameters for your data. + - **dialect**: A dialect, in the context of reading and writing CSVs, is a construct that allows you to create, store, and re-use various formatting parameters for your data. -- **encoding**: Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). +- **XML Formatting** + - **encoding**: Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). -- **xml_declaration**: Controls if an XML declaration should be added to the file. Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). + - **xml_declaration**: Controls if an XML declaration should be added to the file. Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). -- **method**: Can be either "xml", "html" or "text" (default is "xml") + - **method**: Can be either "xml", "html" or "text" (default is "xml") -- **short_empty_elements**: Controls the formatting of elements that contain no content. If True (the default), they are emitted as a single self-closed tag, otherwise they are emitted as a pair of start/end tags. + - **short_empty_elements**: Controls the formatting of elements that contain no content. If True (the default), they are emitted as a single self-closed tag, otherwise they are emitted as a pair of start/end tags. These options match the options from the `Python json module`_. From db5d21620bc5b98b8b5b401e1920a04cff6bed41 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Mon, 14 Oct 2019 13:57:32 -0700 Subject: [PATCH 13/27] Updated tests --- test/test_export.py | 54 +++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/test/test_export.py b/test/test_export.py index 9d7c4a457b..0fae57ca4d 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -21,7 +21,6 @@ import unittest from test.helper import TestHelper - class ExportPluginTest(unittest.TestCase, TestHelper): def setUp(self): self.setup_beets() @@ -31,40 +30,37 @@ def tearDown(self): self.unload_plugins() self.teardown_beets() - def test_json_output(self): - item1, item2 = self.add_item_fixtures(count=2) - item1.album = 'talbum' - item1.artist = "tartist" - item1.track = "ttrack" + def execute_command(self, format_type, artist): + options = ' -f %s -i "track,album" s'.format(format_type, artist) + actual = self.run_with_output('export', options) + return actual.replace(" ", "") + + def create_item(self, album='talbum', artist='tartist', track='ttrack'): + item1, = self.add_item_fixtures() + item1.album = album + item1.artist = artist + item1.track = track item1.write() item1.store() - options = '-f json -i "track,album" ' + item1.artist - out = self.run_with_output('export', options) - self.assertIn('"track": "' + item1.track + '"', out) - self.assertIn('"album": "' + item1.album + '"', out) + return item1 + + def test_json_output(self): + item1 = self.create_item() + actual = self.execute_command(format_type='json',artist=item1.artist) + expected = '[{"track":%s,"album":%s}]'.format(item1.track,item1.album) + self.assertIn(first=expected,second=actual,msg="export in JSON format failed") def test_csv_output(self): - item1, item2 = self.add_item_fixtures(count=2) - item1.album = 'talbum' - item1.artist = "tartist" - item1.track = "ttrack" - item1.write() - item1.store() - options = '-f csv -i "track,album" ' + item1.artist - out = self.run_with_output('export', options) - self.assertIn(item1.track + ',' + item1.album, out) + item1 = self.create_item() + actual = self.execute_command(format_type='json',artist=item1.artist) + expected = 'track,album\n%s,%s'.format(item1.track,item1.album) + self.assertIn(first=expected,second=actual,msg="export in CSV format failed") def test_xml_output(self): - item1, item2 = self.add_item_fixtures(count=2) - item1.album = 'talbum' - item1.artist = "tartist" - item1.track = "ttrack" - item1.write() - item1.store() - options = '-f xml -i "track,album" ' + item1.artist - out = self.run_with_output('export', options) - self.assertIn("" + item1.track + "", out) - self.assertIn("" + item1.album + "", out) + item1 = self.create_item() + actual = self.execute_command(format_type='json',artist=item1.artist) + expected = '%s%s'.format(item1.track,item1.album) + self.assertIn(first=expected,second=actual,msg="export in XML format failed") def suite(): From 0d818eced5cfc262cf63d939bdd55b1acef3f290 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Mon, 14 Oct 2019 17:02:39 -0700 Subject: [PATCH 14/27] Ran test to ensure it works --- beetsplug/export.py | 6 ++-- test/test_export.py | 69 +++++++++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 2f6af072ef..f0384ce555 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -67,13 +67,11 @@ def __init__(self): # XML module formatting options. 'formatting': { # The output encoding. - 'encoding': 'unicode', + 'encoding': 'utf-8', # Controls if XML declaration should be added to the file. 'xml_declaration': True, # Can be either "xml", "html" or "text" (default is "xml"). - 'method': 'xml', - # Controls formatting of elements that contain no content. - 'short_empty_elements': True + 'method': 'xml' } } # TODO: Use something like the edit plugin diff --git a/test/test_export.py b/test/test_export.py index 0fae57ca4d..35ad147184 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -20,6 +20,8 @@ import unittest from test.helper import TestHelper +import re + class ExportPluginTest(unittest.TestCase, TestHelper): def setUp(self): @@ -31,36 +33,61 @@ def tearDown(self): self.teardown_beets() def execute_command(self, format_type, artist): - options = ' -f %s -i "track,album" s'.format(format_type, artist) - actual = self.run_with_output('export', options) - return actual.replace(" ", "") - - def create_item(self, album='talbum', artist='tartist', track='ttrack'): - item1, = self.add_item_fixtures() - item1.album = album - item1.artist = artist - item1.track = track - item1.write() - item1.store() - return item1 + actual = self.run_with_output( + 'export', + '-f', format_type, + '-i', 'album,title', + artist + ) + return re.sub("\\s+", '', actual) + + def create_item(self): + item, = self.add_item_fixtures() + item.artist = 'xartist' + item.title = 'xtitle' + item.album = 'xalbum' + item.write() + item.store() + return item def test_json_output(self): item1 = self.create_item() - actual = self.execute_command(format_type='json',artist=item1.artist) - expected = '[{"track":%s,"album":%s}]'.format(item1.track,item1.album) - self.assertIn(first=expected,second=actual,msg="export in JSON format failed") + actual = self.execute_command( + format_type='json', + artist=item1.artist + ) + expected = u'[{"album":"%s","title":"%s"}]'\ + % (item1.album, item1.title) + self.assertIn( + expected, + actual + ) def test_csv_output(self): item1 = self.create_item() - actual = self.execute_command(format_type='json',artist=item1.artist) - expected = 'track,album\n%s,%s'.format(item1.track,item1.album) - self.assertIn(first=expected,second=actual,msg="export in CSV format failed") + actual = self.execute_command( + format_type='csv', + artist=item1.artist + ) + expected = u'album,title%s,%s'\ + % (item1.album, item1.title) + self.assertIn( + expected, + actual + ) def test_xml_output(self): item1 = self.create_item() - actual = self.execute_command(format_type='json',artist=item1.artist) - expected = '%s%s'.format(item1.track,item1.album) - self.assertIn(first=expected,second=actual,msg="export in XML format failed") + actual = self.execute_command( + format_type='xml', + artist=item1.artist + ) + expected = u'%s%s'\ + % (item1.album, item1.title) + self.assertIn( + expected, + actual + ) def suite(): From 5193f1b19acb8ed0df919223601c73795da63093 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Mon, 14 Oct 2019 17:31:59 -0700 Subject: [PATCH 15/27] Updated export doc --- docs/plugins/export.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 0a27c101bb..16f5e8ac18 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -69,8 +69,6 @@ file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available - **method**: Can be either "xml", "html" or "text" (default is "xml") - - **short_empty_elements**: Controls the formatting of elements that contain no content. If True (the default), they are emitted as a single self-closed tag, otherwise they are emitted as a pair of start/end tags. - These options match the options from the `Python json module`_. .. _Python json module: https://docs.python.org/2/library/json.html#basic-usage @@ -92,5 +90,4 @@ The default options look like this:: formatting: encoding: 'unicode' xml_declaration: True - method: 'xml' - short_empty_elements: True + method: 'xml' \ No newline at end of file From a9440ada2b47bb8902e030add91cf50daad78022 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Mon, 14 Oct 2019 18:17:12 -0700 Subject: [PATCH 16/27] Updated test structure for export --- test/test_export.py | 46 +++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/test/test_export.py b/test/test_export.py index 35ad147184..2ebc6cf95a 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -21,12 +21,16 @@ import unittest from test.helper import TestHelper import re +import beets +import beets.plugins class ExportPluginTest(unittest.TestCase, TestHelper): + def setUp(self): self.setup_beets() self.load_plugins('export') + self.test_values = {'title': 'xtitle', 'album': 'xalbum'} def tearDown(self): self.unload_plugins() @@ -44,8 +48,8 @@ def execute_command(self, format_type, artist): def create_item(self): item, = self.add_item_fixtures() item.artist = 'xartist' - item.title = 'xtitle' - item.album = 'xalbum' + item.title = self.test_values['title'] + item.album = self.test_values['album'] item.write() item.store() return item @@ -56,12 +60,13 @@ def test_json_output(self): format_type='json', artist=item1.artist ) - expected = u'[{"album":"%s","title":"%s"}]'\ - % (item1.album, item1.title) - self.assertIn( - expected, - actual - ) + for key, val in self.test_values.items(): + self.check_assertIn( + actual=actual, + str_format='"{0}":"{1}"', + key=key, + val=val + ) def test_csv_output(self): item1 = self.create_item() @@ -69,12 +74,13 @@ def test_csv_output(self): format_type='csv', artist=item1.artist ) - expected = u'album,title%s,%s'\ - % (item1.album, item1.title) - self.assertIn( - expected, - actual - ) + for key, val in self.test_values.items(): + self.check_assertIn( + actual=actual, + str_format='{0}{1}', + key='', + val=val + ) def test_xml_output(self): item1 = self.create_item() @@ -82,8 +88,16 @@ def test_xml_output(self): format_type='xml', artist=item1.artist ) - expected = u'%s%s'\ - % (item1.album, item1.title) + for key, val in self.test_values.items(): + self.check_assertIn( + actual=actual, + str_format='<{0}>{1}', + key=key, + val=val + ) + + def check_assertIn(self, actual, str_format, key, val): + expected = str_format.format(key, val) self.assertIn( expected, actual From 07138f86dc1095d3d16ac40d2598e754cba66622 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Mon, 14 Oct 2019 18:41:06 -0700 Subject: [PATCH 17/27] Fixed styling and test to pass cli --- docs/plugins/export.rst | 5 ++--- test/test_export.py | 11 ++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 16f5e8ac18..460d4d41c8 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -39,8 +39,7 @@ The ``export`` command has these command-line options: * ``--append``: Appends the data to the file instead of writing. -* ``--format`` or ``-f``: Specifies the format the data will be exported as. If not informed, JSON will be used by default. -The format options include csv, json and xml. +* ``--format`` or ``-f``: Specifies the format the data will be exported as. If not informed, JSON will be used by default. The format options include csv, json and xml. Configuration ------------- @@ -90,4 +89,4 @@ The default options look like this:: formatting: encoding: 'unicode' xml_declaration: True - method: 'xml' \ No newline at end of file + method: 'xml' diff --git a/test/test_export.py b/test/test_export.py index 2ebc6cf95a..c48a151f5c 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -21,12 +21,9 @@ import unittest from test.helper import TestHelper import re -import beets -import beets.plugins class ExportPluginTest(unittest.TestCase, TestHelper): - def setUp(self): self.setup_beets() self.load_plugins('export') @@ -61,7 +58,7 @@ def test_json_output(self): artist=item1.artist ) for key, val in self.test_values.items(): - self.check_assertIn( + self.check_assertin( actual=actual, str_format='"{0}":"{1}"', key=key, @@ -75,7 +72,7 @@ def test_csv_output(self): artist=item1.artist ) for key, val in self.test_values.items(): - self.check_assertIn( + self.check_assertin( actual=actual, str_format='{0}{1}', key='', @@ -89,14 +86,14 @@ def test_xml_output(self): artist=item1.artist ) for key, val in self.test_values.items(): - self.check_assertIn( + self.check_assertin( actual=actual, str_format='<{0}>{1}', key=key, val=val ) - def check_assertIn(self, actual, str_format, key, val): + def check_assertin(self, actual, str_format, key, val): expected = str_format.format(key, val) self.assertIn( expected, From 4251ff70dcb814c9158df684fedac269331970bf Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 00:15:45 -0700 Subject: [PATCH 18/27] updated the way in which xml is outputted --- beetsplug/export.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index f0384ce555..98b8f724b5 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -67,9 +67,9 @@ def __init__(self): # XML module formatting options. 'formatting': { # The output encoding. - 'encoding': 'utf-8', + 'encoding': 'unicode', # Controls if XML declaration should be added to the file. - 'xml_declaration': True, + # 'xml_declaration': True, # Can be either "xml", "html" or "text" (default is "xml"). 'method': 'xml' } @@ -199,20 +199,25 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): def export(self, data, **kwargs): # Creates the XML file structure. - library = ET.Element('library') - tracks_key = ET.SubElement(library, 'key') - tracks_key.text = "Tracks" - tracks_dict = ET.SubElement(library, 'dict') + library = ET.Element(u'library') + tracks_key = ET.SubElement(library, u'key') + tracks_key.text = u'tracks' + tracks_dict = ET.SubElement(library, u'dict') if data and isinstance(data[0], dict): for index, item in enumerate(data): - track_key = ET.SubElement(tracks_dict, 'key') + track_key = ET.SubElement(tracks_dict, u'key') track_key.text = str(index) - track_dict = ET.SubElement(tracks_dict, 'dict') - track_details = ET.SubElement(track_dict, 'Track ID') + track_dict = ET.SubElement(tracks_dict, u'dict') + track_details = ET.SubElement(track_dict, u'Track ID') track_details.text = str(index) for key, value in item.items(): track_details = ET.SubElement(track_dict, key) track_details.text = value - tree = ET.ElementTree(library) - tree.write(self.out_stream, **kwargs) + # tree = ET.ElementTree(element=library) + try: + data = ET.tostring(library, **kwargs) + except LookupError: + data = ET.tostring(library, encoding='utf-8', method='xml') + + self.out_stream.write(data) From eb6055eeca5671fae99387b07e29875a7706aedb Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 11:45:01 -0700 Subject: [PATCH 19/27] Cleaned up comments and code --- beetsplug/export.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 98b8f724b5..9a570c4fb4 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -66,10 +66,6 @@ def __init__(self): 'xml': { # XML module formatting options. 'formatting': { - # The output encoding. - 'encoding': 'unicode', - # Controls if XML declaration should be added to the file. - # 'xml_declaration': True, # Can be either "xml", "html" or "text" (default is "xml"). 'method': 'xml' } @@ -109,8 +105,7 @@ def commands(self): def run(self, lib, opts, args): file_path = opts.output file_mode = 'a' if opts.append else 'w' - file_format = opts.format if opts.format else \ - self.config['default_format'].get(str) + file_format = opts.format or self.config['default_format'].get(str) format_options = self.config[file_format]['formatting'].get(dict) export_format = ExportFormat.factory( @@ -149,10 +144,7 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding - """ self.out_stream = - sys.stdout if path doesn't exit - codecs.open(..) else - """ + # creates a file object to write/append or sets to stdout self.out_stream = codecs.open(self.path, self.mode, self.encoding) \ if self.path else sys.stdout @@ -200,24 +192,17 @@ def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): def export(self, data, **kwargs): # Creates the XML file structure. library = ET.Element(u'library') - tracks_key = ET.SubElement(library, u'key') - tracks_key.text = u'tracks' - tracks_dict = ET.SubElement(library, u'dict') + tracks = ET.SubElement(library, u'tracks') if data and isinstance(data[0], dict): for index, item in enumerate(data): - track_key = ET.SubElement(tracks_dict, u'key') - track_key.text = str(index) - track_dict = ET.SubElement(tracks_dict, u'dict') - track_details = ET.SubElement(track_dict, u'Track ID') - track_details.text = str(index) + track = ET.SubElement(tracks, u'track') for key, value in item.items(): - track_details = ET.SubElement(track_dict, key) + track_details = ET.SubElement(track, key) track_details.text = value - - # tree = ET.ElementTree(element=library) + # Depending on the version of python the encoding needs to change try: - data = ET.tostring(library, **kwargs) + data = ET.tostring(library, encoding='unicode', **kwargs) except LookupError: - data = ET.tostring(library, encoding='utf-8', method='xml') + data = ET.tostring(library, encoding='utf-8', **kwargs) self.out_stream.write(data) From 21d809180eb4f9034e8afbabac5d6a0c87c39213 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 11:45:38 -0700 Subject: [PATCH 20/27] Updated Test structure --- test/test_export.py | 97 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/test/test_export.py b/test/test_export.py index c48a151f5c..5112ce9c7a 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -20,7 +20,10 @@ import unittest from test.helper import TestHelper -import re +import re # used to test csv format +import json +from xml.etree.ElementTree import Element +import xml.etree.ElementTree as ET class ExportPluginTest(unittest.TestCase, TestHelper): @@ -34,10 +37,11 @@ def tearDown(self): self.teardown_beets() def execute_command(self, format_type, artist): + query = ','.format(list(self.test_values.keys())) actual = self.run_with_output( 'export', '-f', format_type, - '-i', 'album,title', + '-i', query, artist ) return re.sub("\\s+", '', actual) @@ -53,18 +57,64 @@ def create_item(self): def test_json_output(self): item1 = self.create_item() - actual = self.execute_command( - format_type='json', - artist=item1.artist + out = self.run_with_output( + 'export', + '-f', 'json', + '-i', 'album,title', + item1.artist ) + json_data = json.loads(out)[0] for key, val in self.test_values.items(): - self.check_assertin( - actual=actual, - str_format='"{0}":"{1}"', - key=key, - val=val - ) + self.assertTrue(key in json_data) + self.assertEqual(val, json_data[key]) + + def test_csv_output(self): + item1 = self.create_item() + out = self.run_with_output( + 'export', + '-f', 'csv', + '-i', 'album,title', + item1.artist + ) + csv_list = re.split('\r', re.sub('\n', '', out)) + head = re.split(',', csv_list[0]) + vals = re.split(',|\r', csv_list[1]) + for index, column in enumerate(head): + self.assertTrue(self.test_values.get(column, None) is not None) + self.assertEqual(vals[index], self.test_values[column]) + + def test_xml_output(self): + item1 = self.create_item() + out = self.run_with_output( + 'export', + '-f', 'xml', + '-i', 'album,title', + item1.artist + ) + library = ET.fromstring(out) + self.assertIsInstance(library, Element) + for track in library[0]: + for details in track: + tag = details.tag + txt = details.text + self.assertTrue(tag in self.test_values, msg=tag) + self.assertEqual(self.test_values[tag], txt, msg=txt) + + def check_assertin(self, actual, str_format, key, val): + expected = str_format.format(key, val) + self.assertIn( + expected, + actual + ) + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') + +""" def test_csv_output(self): item1 = self.create_item() actual = self.execute_command( @@ -93,16 +143,17 @@ def test_xml_output(self): val=val ) - def check_assertin(self, actual, str_format, key, val): - expected = str_format.format(key, val) - self.assertIn( - expected, - actual + def test_json_output(self): + item1 = self.create_item() + actual = self.execute_command( + format_type='json', + artist=item1.artist ) - - -def suite(): - return unittest.TestLoader().loadTestsFromName(__name__) - -if __name__ == '__main__': - unittest.main(defaultTest='suite') + for key, val in self.test_values.items(): + self.check_assertin( + actual=actual, + str_format='"{0}":"{1}"', + key=key, + val=val + ) +""" From 623f553c92d18faed0a94f339decb9278217dd49 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 11:51:45 -0700 Subject: [PATCH 21/27] Updated Test structure --- test/test_export.py | 81 +++++++-------------------------------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/test/test_export.py b/test/test_export.py index 5112ce9c7a..757212a382 100644 --- a/test/test_export.py +++ b/test/test_export.py @@ -37,14 +37,14 @@ def tearDown(self): self.teardown_beets() def execute_command(self, format_type, artist): - query = ','.format(list(self.test_values.keys())) - actual = self.run_with_output( + query = ','.join(self.test_values.keys()) + out = self.run_with_output( 'export', '-f', format_type, '-i', query, artist ) - return re.sub("\\s+", '', actual) + return out def create_item(self): item, = self.add_item_fixtures() @@ -57,11 +57,9 @@ def create_item(self): def test_json_output(self): item1 = self.create_item() - out = self.run_with_output( - 'export', - '-f', 'json', - '-i', 'album,title', - item1.artist + out = self.execute_command( + format_type='json', + artist=item1.artist ) json_data = json.loads(out)[0] for key, val in self.test_values.items(): @@ -70,11 +68,9 @@ def test_json_output(self): def test_csv_output(self): item1 = self.create_item() - out = self.run_with_output( - 'export', - '-f', 'csv', - '-i', 'album,title', - item1.artist + out = self.execute_command( + format_type='csv', + artist=item1.artist ) csv_list = re.split('\r', re.sub('\n', '', out)) head = re.split(',', csv_list[0]) @@ -85,11 +81,9 @@ def test_csv_output(self): def test_xml_output(self): item1 = self.create_item() - out = self.run_with_output( - 'export', - '-f', 'xml', - '-i', 'album,title', - item1.artist + out = self.execute_command( + format_type='xml', + artist=item1.artist ) library = ET.fromstring(out) self.assertIsInstance(library, Element) @@ -100,60 +94,9 @@ def test_xml_output(self): self.assertTrue(tag in self.test_values, msg=tag) self.assertEqual(self.test_values[tag], txt, msg=txt) - def check_assertin(self, actual, str_format, key, val): - expected = str_format.format(key, val) - self.assertIn( - expected, - actual - ) - def suite(): return unittest.TestLoader().loadTestsFromName(__name__) if __name__ == '__main__': unittest.main(defaultTest='suite') - -""" - def test_csv_output(self): - item1 = self.create_item() - actual = self.execute_command( - format_type='csv', - artist=item1.artist - ) - for key, val in self.test_values.items(): - self.check_assertin( - actual=actual, - str_format='{0}{1}', - key='', - val=val - ) - - def test_xml_output(self): - item1 = self.create_item() - actual = self.execute_command( - format_type='xml', - artist=item1.artist - ) - for key, val in self.test_values.items(): - self.check_assertin( - actual=actual, - str_format='<{0}>{1}', - key=key, - val=val - ) - - def test_json_output(self): - item1 = self.create_item() - actual = self.execute_command( - format_type='json', - artist=item1.artist - ) - for key, val in self.test_values.items(): - self.check_assertin( - actual=actual, - str_format='"{0}":"{1}"', - key=key, - val=val - ) -""" From d86e31d3706b0dcfdb4744c4701e37684c8eac55 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 13:10:47 -0700 Subject: [PATCH 22/27] Updated to reflect code changes and updated styling/format --- docs/plugins/export.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 460d4d41c8..8afc740cd2 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -56,21 +56,25 @@ file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available - **sort_keys**: Sorts the keys in JSON dictionaries. +These options match the options from the `Python json module`_. + +.. _Python json module: https://docs.python.org/2/library/json.html#basic-usage + - **CSV Formatting** - **delimiter**: Used as the separating character between fields. The default value is a comma (,). - - **dialect**: A dialect, in the context of reading and writing CSVs, is a construct that allows you to create, store, and re-use various formatting parameters for your data. + - **dialect**: A dialect is a construct that allows you to create, store, and re-use various formatting parameters for your data. -- **XML Formatting** - - **encoding**: Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). +These options match the options from the `Python csv module`_. - - **xml_declaration**: Controls if an XML declaration should be added to the file. Use False for never, True for always, None for only if not US-ASCII or UTF-8 or Unicode (default is None). +.. _Python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params +- **XML Formatting** - **method**: Can be either "xml", "html" or "text" (default is "xml") -These options match the options from the `Python json module`_. +These options match the options from the `Python xml module`_. -.. _Python json module: https://docs.python.org/2/library/json.html#basic-usage +.. _Python xml module: https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring The default options look like this:: @@ -87,6 +91,4 @@ The default options look like this:: dialect: 'excel' xml: formatting: - encoding: 'unicode' - xml_declaration: True method: 'xml' From 7f6630c006406a488dae4d60e8a14d07d2d48765 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 14:16:23 -0700 Subject: [PATCH 23/27] removed xml configs from doc and code --- beetsplug/export.py | 5 +---- docs/plugins/export.rst | 12 +----------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/beetsplug/export.py b/beetsplug/export.py index 9a570c4fb4..f7e84a5701 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -65,10 +65,7 @@ def __init__(self): }, 'xml': { # XML module formatting options. - 'formatting': { - # Can be either "xml", "html" or "text" (default is "xml"). - 'method': 'xml' - } + 'formatting': {} } # TODO: Use something like the edit plugin # 'item_fields': [] diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 8afc740cd2..a88925765e 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -69,13 +69,6 @@ These options match the options from the `Python csv module`_. .. _Python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params -- **XML Formatting** - - **method**: Can be either "xml", "html" or "text" (default is "xml") - -These options match the options from the `Python xml module`_. - -.. _Python xml module: https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring - The default options look like this:: export: @@ -88,7 +81,4 @@ The default options look like this:: csv: formatting: delimiter: ',' - dialect: 'excel' - xml: - formatting: - method: 'xml' + dialect: 'excel' From d7b0e9347afd148510415a9a3c72a632cf5b5a71 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 15:32:03 -0700 Subject: [PATCH 24/27] Updated changelog to reflect export plugin changes --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index cf14ae9745..2826c0d946 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,10 @@ Changelog New features: +* :doc:`/plugins/export`: Added new ``-f`` (``--format``) flag; + which allows for the ability to export in json, csv and xml. + Thanks to :user:`austinmm`. + :bug:`3402` * We now fetch information about `works`_ from MusicBrainz. MusicBrainz matches provide the fields ``work`` (the title), ``mb_workid`` (the MBID), and ``work_disambig`` (the disambiguation string). From 5d7c937d41cd425b37c3c44e2e80e2234d0a9673 Mon Sep 17 00:00:00 2001 From: Austin Marino Date: Tue, 15 Oct 2019 15:34:59 -0700 Subject: [PATCH 25/27] fixed conflicting files issues with changelog --- docs/changelog.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2826c0d946..6f4fdc6d31 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ New features: which allows for the ability to export in json, csv and xml. Thanks to :user:`austinmm`. :bug:`3402` +* :doc:`/plugins/unimported`: lets you find untracked files in your library directory. * We now fetch information about `works`_ from MusicBrainz. MusicBrainz matches provide the fields ``work`` (the title), ``mb_workid`` (the MBID), and ``work_disambig`` (the disambiguation string). @@ -74,6 +75,16 @@ New features: you can now match tracks and albums using the `Deezer`_ database. Thanks to :user:`rhlahuja`. :bug:`3355` +* :doc:`/plugins/beatport`: The plugin now gets the musical key, BPM and the + genre for each track. + :bug:`2080` +* :doc:`/plugins/beatport`: Fix default assignment of the musical key. + :bug:`3377` +* :doc:`/plugins/bpsync`: Add `bpsync` plugin to sync metadata changes + from the Beatport database. +* :doc:`/plugins/beatport`: Fix assignment of `genre` and rename `musical_key` + to `initial_key`. + :bug:`3387` Fixes: @@ -108,6 +119,13 @@ Fixes: * ``none_rec_action`` does not import automatically when ``timid`` is enabled. Thanks to :user:`RollingStar`. :bug:`3242` +* Fix a bug that caused a crash when tagging items with the beatport plugin. + :bug:`3374` +* ``beet update`` will now confirm that the user still wants to update if + their library folder cannot be found, preventing the user from accidentally + wiping out their beets database. + Thanks to :user:`logan-arens`. + :bug:`1934` For plugin developers: @@ -140,6 +158,8 @@ For plugin developers: APIs to provide metadata matches for the importer. Refer to the Spotify and Deezer plugins for examples of using this template class. :bug:`3355` +* The autotag hooks have been modified such that they now take 'bpm', + 'musical_key' and a per-track based 'genre' as attributes. For packagers: From d45b8bb03e17348b2220f90318ae32fa3bb42a12 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 16 Oct 2019 14:27:06 -0400 Subject: [PATCH 26/27] Docs fixes from my code review --- docs/plugins/export.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index a88925765e..6be20bec72 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -63,7 +63,7 @@ These options match the options from the `Python json module`_. - **CSV Formatting** - **delimiter**: Used as the separating character between fields. The default value is a comma (,). - - **dialect**: A dialect is a construct that allows you to create, store, and re-use various formatting parameters for your data. + - **dialect**: The kind of CSV file to produce. The default is `excel`. These options match the options from the `Python csv module`_. @@ -77,8 +77,8 @@ The default options look like this:: ensure_ascii: False indent: 4 separators: [',' , ': '] - sort_keys: True + sort_keys: true csv: formatting: delimiter: ',' - dialect: 'excel' + dialect: excel From 229177857565735dc2dfed471dff881765eca42e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 16 Oct 2019 14:29:32 -0400 Subject: [PATCH 27/27] Docs simplifications for #3400 --- docs/plugins/export.rst | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index 6be20bec72..f3756718ce 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -45,28 +45,24 @@ Configuration ------------- To configure the plugin, make a ``export:`` section in your configuration -file. Under the ``json``, ``csv``, and ``xml`` keys, these options are available: +file. +For JSON export, these options are available under the ``json`` key: -- **JSON Formatting** - - **ensure_ascii**: Escape non-ASCII characters with ``\uXXXX`` entities. +- **ensure_ascii**: Escape non-ASCII characters with ``\uXXXX`` entities. +- **indent**: The number of spaces for indentation. +- **separators**: A ``[item_separator, dict_separator]`` tuple. +- **sort_keys**: Sorts the keys in JSON dictionaries. - - **indent**: The number of spaces for indentation. +Those options match the options from the `Python json module`_. +Similarly, these options are available for the CSV format under the ``csv`` +key: - - **separators**: A ``[item_separator, dict_separator]`` tuple. - - - **sort_keys**: Sorts the keys in JSON dictionaries. - -These options match the options from the `Python json module`_. - -.. _Python json module: https://docs.python.org/2/library/json.html#basic-usage - -- **CSV Formatting** - - **delimiter**: Used as the separating character between fields. The default value is a comma (,). - - - **dialect**: The kind of CSV file to produce. The default is `excel`. +- **delimiter**: Used as the separating character between fields. The default value is a comma (,). +- **dialect**: The kind of CSV file to produce. The default is `excel`. These options match the options from the `Python csv module`_. +.. _Python json module: https://docs.python.org/2/library/json.html#basic-usage .. _Python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params The default options look like this:: @@ -74,7 +70,7 @@ The default options look like this:: export: json: formatting: - ensure_ascii: False + ensure_ascii: false indent: 4 separators: [',' , ': '] sort_keys: true