-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extended export plugin support #3400
Extended export plugin support #3400
Conversation
This update reflects the code changes I made to the export plugin
Cool; thanks for getting this started! Here's one question for context: while I can totally see the utility of a CSV export, I'm struggling to think of a use case for an XML export. Can you elaborate a little more on what you want to use CSV and XML data for? I think that would help calibrate our approach to these features. |
beetsplug/export.py
Outdated
@@ -40,6 +42,9 @@ class ExportPlugin(BeetsPlugin): | |||
|
|||
def __init__(self): | |||
super(ExportPlugin, self).__init__() | |||
# Used when testing export plugin | |||
self.run_results = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm; I don't think I see where this is actually used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to use that class field as a way of checking the output during the testing of the export plugin but I ended up going another route so I will go ahead and remove it.
beetsplug/export.py
Outdated
@@ -40,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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't quite see why this is a class field instead of just being a local variable in the run
method. Can we please revert to the old way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will revert it.
beetsplug/export.py
Outdated
'indent': 4, | ||
'separators': ('>'), | ||
'sort_keys': True | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think any of these config options are actually used in the CSV or XML emitters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just trying to follow the same format, but you are right they are not being used. I have updated them to now be format specific to json, csv and xml and updated the csv and xml outputs to use them...
'json': {
# json module formatting options
'formatting': {
'ensure_ascii': False,
'indent': 4,
'separators': (',', ': '),
'sort_keys': True
}
},
'csv': {
# csv module formatting options
'formatting': {
'delimiter': ',', # column seperator
'dialect': 'excel', # the name of the dialect to use
'quotechar':'|'
}
},
'xml': {
# xml module formatting options
'formatting': {
'encoding': 'UTF-8', # the output encoding
'xml_declaration':'True', # controls if an XML declaration should be added to the file
'default_namespace': 'None', # sets the default XML namespace (for “xmlns”)
'method': 'xml', # either "xml", "html" or "text" (default is "xml")
'short_empty_elements': 'True' # controls the formatting of elements that contain no content.
}
beetsplug/export.py
Outdated
@@ -78,17 +101,21 @@ 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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be shorter: perhaps, "the output format: json (default), csv, or xml".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
beetsplug/export.py
Outdated
file_mode = 'a' if opts.append else 'w' | ||
file_format = opts.format |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please preserve the effect of default_format
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you prefer I get rid of the default=json
in...
cmd.parser.add_option(
u'-f', u'--format', default='json',
help=u"the output format: json (default), csv, or xml"
)
and instead have something like...
file_format = opts.format if opts.format else self.config['default_format'].get(str)
beetsplug/export.py
Outdated
"""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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This technique where a method is defined conditionally is somewhat surprising. I now see how it works after some puzzling about it, but it could use an explanatory comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'll add some comments explaining its purpose
beetsplug/export.py
Outdated
|
||
def export_to_file(self, data, **kwargs): | ||
with codecs.open(self.path, self.mode, self.encoding) as f: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be really useful to factor out the file-opening part of the logic so it doesn't need to be re-implemented for every export format. In general, the output format and output destination (file or stdout) seem orthogonal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll explore that idea. Sounds like a better way
beetsplug/export.py
Outdated
|
||
def export(self, data, **kwargs): | ||
json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) | ||
if data and type(data) is list and len(data) > 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused by the list
type check here—when will the passed object not be a list? And what happens when it's not—so self.header
remains empty?
Also, this condition could be simplified to:
if data and isinstance(data, list):
because data
is only "true" if it is non-empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just trying to be extra cautious but I completely agree that it is a little overkill, this should work just fine instead; if data and len(data) > 0:
docs/plugins/export.rst
Outdated
|
||
$ beet export -f csv beatles | ||
$ beet export -f json beatles | ||
$ beet export -f xml beatles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can do without the full command-line examples here. Can we just list the options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
docs/plugins/export.rst
Outdated
ensure_ascii: False | ||
indent: 4 | ||
separators: ['>'] | ||
sort_keys: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above, I don't think these options are actually used for the new formats.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated them to work correctly now with the added formats CSV and XML
I am still working on all the changes but almost done. Thank you for all the feedback as it was very helpful and constructive. Also to address your question about the XML format. The reason I added the ability to export in the XML format was that this is the preferred export format used by Apple in Apple Music and simply figured that if Apple does it then there must exist a good reason. For CSV exporting, I imagined that some people, including myself, might want to use Excel tools to better analyze their music library. |
I have just pushed the changes that were discussed above. Please let me know if you recommend any other changes. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking pretty good so far!
Could you please look into the CI results above? We use a style checker to enforce PEP-8 conventions.
One other thing that the style checker won't catch: can you please try to use complete sentences in comments, complete with a capital letter and a period? It would be nice to be consistent with the way (most) other comments in beets are written.
beetsplug/export.py
Outdated
'formatting': { | ||
'delimiter': ',', # column seperator | ||
'dialect': 'excel', # the name of the dialect to use | ||
'quotechar': '|' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you probably want quotechar
to default to "? In fact, it doesn't seem like the most useful thing to want to override… maybe we can get away without this option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'll remove it
beetsplug/export.py
Outdated
# csv module formatting options | ||
'formatting': { | ||
'delimiter': ',', # column seperator | ||
'dialect': 'excel', # the name of the dialect to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This option seems useful enough! But maybe it's worth linking (in the documentation) to the place in the Python docs where dialects are described?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes I forgot to do that, thank you for reminding me
beetsplug/export.py
Outdated
|
||
def export(self, data, **kwargs): | ||
if data and len(data) > 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if data and len(data) > 0: | |
if data: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do
docs/plugins/export.rst
Outdated
ensure_ascii: False | ||
indent: 4 | ||
separators: ['>'] | ||
sort_keys: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these examples are now out of sync with the implementation. It would also be good to describe the options in English above.
…d up code to better follow PEP-8 conventions and just work more efficiently all around.
I have made the changes you suggested alongside some other just general code cleanup. I am not sure where I might be disobeying the PEP-8 conventions so I apologize if I am anywhere. I am still new to CIs and so would love any recommendations you might have so I can pass the two CI test. |
For the CI results, take a look at GitHub's "checks" interface on this page. It has "Details" links to the logs from Travis and CircleCI. For example, here's the log from the style checker: You can also consider installing flake8 and running it locally. Also, about the docs: while the text seems good, could you please move these from inline comments in the example YAML to the prose text above? There's already a section there with a bulleted list describing the options, before the page gets to the YAML example. That would be a nice place to add similar descriptions of the new options. |
…ustinmm/beets into Extended_Export_Plugin_Support Pulling updated exort.rst doc
Both the test_export.py and export.py pass the flake8 test so I'm not sure why there is still an issue with the CLI. How do I run the test_export.py on my computer because if I just run |
We have a wiki page about testing that might help! You might want to try using Tox or check out the section about test dependencies. You can also see the results of the tests directly on Travis: |
I was able to get all the tests to pass :). Please let me know if you want me to update anything else, but I believe it is production-ready. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall! I left a few more comments inline.
I also took a look at the exported XML format. It seems to look like this:
<library>
<key>tracks</key>
<dict>
<key>0</key>
<dict>
<Track ID>0</Track ID>
<artist>...</artist>
...
</dict>
<key>1</key>
...
</dict>
</library>
But I guess I would have expected there to be a tag called <tracks>
that contained individual <track>
elements with the attributes of each. That would be more XML-like—I'm not sure I see the utility of the extra level of "dict" indirection here.
Also, "Track ID" is not a valid XML tag name—they can't contain spaces.
beetsplug/export.py
Outdated
# Controls if XML declaration should be added to the file. | ||
# 'xml_declaration': True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is no longer used and can be deleted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed
beetsplug/export.py
Outdated
file_format = opts.format if opts.format else \ | ||
self.config['default_format'].get(str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should suffice: opts.format or self.config['default_format'].get(str)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good
beetsplug/export.py
Outdated
""" self.out_stream = | ||
sys.stdout if path doesn't exit | ||
codecs.open(..) else | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this is out of date? Or it can be rephrased as a natural-language comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the new comment...
# creates a file object to write/append or sets to stdout
beetsplug/export.py
Outdated
track_details = ET.SubElement(track_dict, key) | ||
track_details.text = value | ||
|
||
# tree = ET.ElementTree(element=library) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of date?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah
docs/plugins/export.rst
Outdated
|
||
- **separators**: A ``[item_separator, dict_separator]`` tuple. | ||
- **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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of duplicating the text from the Python CSV documentation, can we instead link to the explanation there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will link to an external description but I would imagine it is still good to have some description in the document itself. I just shortened it a little. Let me know if you want me to get rid of the description altogether.
docs/plugins/export.rst
Outdated
|
||
- **sort_keys**: Sorts the keys in JSON dictionaries. | ||
- **XML Formatting** | ||
- **encoding**: Use encoding="unicode" to generate a Unicode string (otherwise, a bytestring is generated). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm… how does this affect the usage of the plugin? Because we're writing to a file, it seems like the kind of Python string that gets generated is irrelevant. Perhaps there's an argument to be made that this should not be configurable in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it is not a config option now.
docs/plugins/export.rst
Outdated
- **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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I'm not sure if I know what an "XML declaration" is. Is this something that people will actually want to control?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its been removed
docs/plugins/export.rst
Outdated
|
||
- **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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's worth explaining what these options actually do? I'm not sure how setting method
to text
will affect the output, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user changes the config to use text or HTML then the XML structure built will reformat to be displayed as text or as HTML. It kind of adds two other export formats unintentionally...
XML:
<library><key>tracks</key><dict><key>0</key><dict><Track ID>0</Track ID><title>Catch Me</title><album>BRÅVES</album></dict><key>1</key><dict><Track ID>1</Track ID><title>A Toast</title><album>BRÅVES</album></dict><key>2</key><dict><Track ID>2</Track ID><title>Joan of Arc</title><album>BRÅVES</album></dict></dict></library>
Text:
tracks00Catch MeBRÅVES11A ToastBRÅVES22Joan of ArcBRÅVES
HTML:
<library><key>tracks</key><dict><key>0</key><dict><Track ID>0</Track ID><title>Catch Me</title><album>BRÅVES</album></dict><key>1</key><dict><Track ID>1</Track ID><title>A Toast</title><album>BRÅVES</album></dict><key>2</key><dict><Track ID>2</Track ID><title>Joan of Arc</title><album>BRÅVES</album></dict></dict></library>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm… correct me if I'm wrong, but isn't the HTML example here identical to the XML example? Is there a difference I'm missing?
Also, TBH, the text format doesn't look very useful—it's just all the values strung together, without spacing…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the HTML is the same but I don't think that is always the case. I agree the text is not very useful. Would you like me to remove this option?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so! Unless you can think of a compelling reason why someone would want to use it, let's take it out.
I believe I have successfully implemented all your recommended changes. Let me know if I need to make any other changes. Also, here is the updated XML format...
|
Cool! Thanks for continuing to make progress! I have only two more things in mind:
|
How do we create a changelog entry? |
There's a file in |
Let me know if I didn't create the changelog entry correctly or if you notice anything else that needs fixing :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks for your continued effort on this. I made some tiny comments which I will apply now before merging.
docs/plugins/export.rst
Outdated
- **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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- **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`. |
Keeping this focused on what matters to users of this plugin, not to Python programmers.
docs/plugins/export.rst
Outdated
@@ -62,4 +77,8 @@ The default options look like this:: | |||
ensure_ascii: False | |||
indent: 4 | |||
separators: [',' , ': '] | |||
sort_keys: true | |||
sort_keys: True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lower case is OK here—it’s YAML, not Python.
sort_keys: True | |
sort_keys: true |
docs/plugins/export.rst
Outdated
csv: | ||
formatting: | ||
delimiter: ',' | ||
dialect: 'excel' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dialect: 'excel' | |
dialect: excel |
No quotes are usually necessary in YAML.
Arg, turns out GitHub won’t let me commit my own suggestions on my phone. :( But I will wrap this up when I’m back at a normal computer. |
Your changes look great! Thank you for all the help you provided me with this. This was my first time contributing to an open-source project so all your assistance was very much appreciated. |
Wow! In that case, thank you for your diligent efforts, congratulations, and welcome to the world of open source. 😃 Nicely done! |
The original export plugin only exported as JSON so I added the ability to export in CSV or XML format. To do this I created a new option
-f
or--format
which specifies the export format, the default is still JSON. For example...$ beet export -f csv -i "title,album" ACDC
I also updated the plugin documentation to reflect the changes I've made to the plugin. Lastly, I also created a unit test for the export plugin; since one hadn't been created yet.