Skip to content
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

File-like objects support #45

Merged
merged 7 commits into from
Aug 17, 2021
Merged

File-like objects support #45

merged 7 commits into from
Aug 17, 2021

Conversation

JuniorJPDJ
Copy link
Contributor

and simple and useful MediaFile.as_dict() function

Resolves #35

@JuniorJPDJ JuniorJPDJ force-pushed the filething branch 2 times, most recently from 724c135 to bb72d66 Compare May 12, 2021 01:46
@sampsyo
Copy link
Member

sampsyo commented May 12, 2021

Thanks for getting this started! If you're interested in changing path to filename, perhaps we should do that in a separate PR so it's easier to review the core of the "file-like object" feature separately? (In general, for me, I find it's easier to fully understand one change at a time.)

@JuniorJPDJ
Copy link
Contributor Author

It's connected as in case of FileThing it doesn't have to be path already but it will always be some sort of file name.
I can split it to separate commits for you if it's gonna make review easier ;D

@sampsyo
Copy link
Member

sampsyo commented May 12, 2021

That would be awesome, if you don't mind!

@JuniorJPDJ
Copy link
Contributor Author

Done!
If possible - I'd like to get your review and merge on #42 first tho.

`MediaFile.as_dict() converts tags to dict usable in for example `MediaFile.update()` or just printing purposes.
@JuniorJPDJ JuniorJPDJ marked this pull request as ready for review May 18, 2021 00:14
@JuniorJPDJ
Copy link
Contributor Author

Rebased on top of master.

Copy link
Member

@sampsyo sampsyo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few miscellaneous comments within!

@@ -436,7 +436,6 @@ def test_write(self):
shutil.copy(src, path)
mf = mediafile.MediaFile(path)
mf.read_only_field = "something terrible"
mf.path = os.path.join(self.temp_dir, b'test.flac')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite see why this needs to be deleted.

Copy link
Contributor Author

@JuniorJPDJ JuniorJPDJ May 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    def test_write(self):
        src = os.path.join(_common.RSRC, b'empty.flac')
        path = os.path.join(self.temp_dir, b'test.flac')
        shutil.copy(src, path)
        mf = mediafile.MediaFile(path)
        mf.read_only_field = "something terrible"
        mf.path = os.path.join(self.temp_dir, b'test.flac')
        mf.save()
        self.assertNotIn(self.key, mf.mgfile.tags)

This is a whole function.
path is already set to exactly this path and then handled to mediafile in line:

 mf = mediafile.MediaFile(path)

So it was setting mf.path to mf.path ;D
Sooo instead of fixing this code when renaming .path, I just removed it and it still works!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh; interesting! I wonder why this was here in the first place.

@@ -1518,6 +1549,19 @@ def __init__(self, path, id3v23=False):
# Set the ID3v2.3 flag only for MP3s.
self.id3v23 = id3v23 and self.type == 'mp3'

@property
def filesize(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could use a docstring?

Copy link
Contributor Author

@JuniorJPDJ JuniorJPDJ May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty obvious to me, but if you say it's needed.. ;D

@@ -1468,20 +1495,24 @@ class MediaFile(object):
"""Represents a multimedia file on disk and provides access to its
metadata.
"""
def __init__(self, path, id3v23=False):
@loadfile()
def __init__(self, filething, id3v23=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the docstring needs updating to describe what a filething is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I can say what exactly is it without scaring users ;/

mutagen itself just mentions that you can put string or filelike as first argument.
Problem is that technically this first argument is eaten by loadfile decorator and it's not filething from func definition. filething from func definition is namedtuple from mutagen._utils.

So TLDR:
I'm not sure how to document this.
Do you have any idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd just say that filething can either be a string or a file-like object.

# Utility.


def _update_filething(filething):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also perhaps needs a docstring? Some explanation could help explain why this is necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is I'm not exactly sure why and where exactly closing happens.
Debugging this issue was guessing game.
mutagen was closing my files (only in case of local files, wtf) after reading and I couldn't find any .close() call in whole mutagen code nor using debugger on own proxy file-like.
Anyway - this allows to reopen files in r+ when saving when opened as r on loading.
Proxy file-like weren't closed at all..

I'm not sure what could I write in this docstring what's not written in comment anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the reason!
mutagen._util._openfile closes files it opened just after function needing file finishes.

mediafile.py Outdated
Comment on lines 138 to 142
except UnreadableFileError:
# reraise our errors without changes
raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why this is necessary now (when it wasn't before)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__init__ function is now whole wrapped by loadfile decorator, which calls mutagen._util.loadfile through mutagen_call (original loadfile also raises exceptions) so we.. use whole __init__ function through mutagen_call and then call it inside, so exceptions are caught by it two times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now, after some time, considering removing mutagen_call from there and catching this exceptions myself in decorator.
It probably would be cleaner solution...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not cleaner solution as it finished almost as copy-pasta of mutagen_call.
I'll just add one more comment here and leave it as it was in previous version.

except Exception as exc:
# Isolate bugs in Mutagen.
log.debug(u'%s', traceback.format_exc())
log.error(u'uncaught Mutagen exception in %s: %s', action, exc)
raise MutagenError(path, exc)


def loadfile(method=True, writable=False, create=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last place where a docstring could be helpful… even just referencing the original loadfile decorator could help explain why this exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok ;D
Should be pretty easy.

fixes support for filelike objects
resolves beetbox#35

Incompatible changes:
- renames `path` argument to `MediaFile` to `filename` (compatibility with mutagen)

MediaFile can now be created using following scheme:
```
fileobj = io.BytesIO()
MediaFile(filename="myfile.mp3")
MediaFile(fileobj=myfileobj)
```
but old style
```
MediaFile("myfile.mp3")
MediaFile(myfileobj)
```
will also work.
This doesn't have to be path as we are using FileThing to access file - it doesn't even have to be any local file
anyway FileThing should have name inside, so we can use it as filename to provide user readable representation of this FileThing to user
@JuniorJPDJ
Copy link
Contributor Author

I think I fixed most of issues you asked about ;D

@JuniorJPDJ
Copy link
Contributor Author

JuniorJPDJ commented May 22, 2021

I re-added MediaFile.path as @property - I don't want to completely break backwards compatibility.

It should be compatible with most of existing software now, as far as someone don't set path in their code. I don't see a point of doing this anyway as afaik it didn't change anything - saving still worked on previous file.

For backwards compatibility
Also changes `MediaFile.filename` to `@property` - holding reference to string is pointless and it shouldn't be settable.
@JuniorJPDJ
Copy link
Contributor Author

.

@sampsyo
Copy link
Member

sampsyo commented Jun 27, 2021

Just returning to this PR after a long delay—is everything in order from your perspective, @JuniorJPDJ? Maybe we should add something to the documentation about this new capability so people know they can use it with open files?

mediafile.py Outdated
def __init__(self, filething, id3v23=False):
"""Constructs a new `MediaFile` reflecting the provided file.
May throw `UnreadableFileError`.
First argument can be a string (file path) or filelike object.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's something about it.
IMO it's enough.

@JuniorJPDJ
Copy link
Contributor Author

IMO it's enough. @up is line describing it
If you think it's not enough - tell me please what exactly would you like to see.

@JuniorJPDJ
Copy link
Contributor Author

bump

@sampsyo
Copy link
Member

sampsyo commented Aug 17, 2021

Thanks for your patience! This all looks great. I have updated some documentation and will merge now. ✨

@sampsyo sampsyo merged commit e88adb4 into beetbox:master Aug 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support filelike objects, not only pathes
2 participants