Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions lyricsgenius/types/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any

from ..utils import safe_unicode, sanitize_filename
Expand All @@ -26,7 +26,10 @@ def save_lyrics(

Args:
filename (:obj:`str`, optional): Output filename, a string.
If not specified, the result is returned as a string.
May include a full or relative directory path (e.g.
``"output/my_song"``). The directory will be created
automatically if it does not exist.
If not specified, a default name is used.
extension (:obj:`str`, optional): Format of the file (`json` or `txt`).
overwrite (:obj:`bool`, optional): Overwrites preexisting file if `True`.
Otherwise prompts user for input.
Expand All @@ -45,17 +48,21 @@ def save_lyrics(
msg = "extension must be JSON or TXT"
assert (extension == "json") or (extension == "txt"), msg

# Standardize the extension
filename, _ = os.path.splitext(filename)
filename += "." + extension
filename = sanitize_filename(filename) if sanitize else filename
# Separate parent directory from stem so we sanitize only the filename
# portion, then reconstruct the full path.
p = Path(filename)
stem = sanitize_filename(p.stem) if sanitize else p.stem
p = p.with_name(stem + "." + extension)

# Create parent directory if needed (no-op when parent is cwd)
p.parent.mkdir(parents=True, exist_ok=True)

# Check if file already exists
write_file = False
if overwrite or not os.path.isfile(filename):
if overwrite or not p.is_file():
write_file = True
elif verbose:
msg = f"{filename} already exists. Overwrite?\n(y/n): "
msg = f"{p} already exists. Overwrite?\n(y/n): "
if input(msg).lower() == "y":
write_file = True

Expand All @@ -67,12 +74,12 @@ def save_lyrics(

# Save the lyrics to a file
if extension == "json":
self.to_json(filename, ensure_ascii=ensure_ascii, sanitize=sanitize)
self.to_json(str(p), ensure_ascii=ensure_ascii, sanitize=False)
else:
self.to_text(filename, sanitize=sanitize)
self.to_text(str(p), sanitize=False)

if verbose:
print(f"Wrote {safe_unicode(filename)}.")
print(f"Wrote {safe_unicode(str(p))}.")

return None

Expand Down Expand Up @@ -116,9 +123,10 @@ def to_json(
return json.dumps(data, indent=1, ensure_ascii=ensure_ascii)

# Save Song object to a json file
filename = sanitize_filename(filename) if sanitize else filename
with open(filename, "w", encoding="utf-8") as ff:
json.dump(data, ff, indent=4, ensure_ascii=ensure_ascii)
p = Path(sanitize_filename(filename) if sanitize else filename)
p.write_text(
json.dumps(data, indent=4, ensure_ascii=ensure_ascii), encoding="utf-8"
)
return None

@property
Expand Down Expand Up @@ -156,9 +164,8 @@ def to_text(self, filename: str | None = None, sanitize: bool = True) -> str | N
return self._text_data

# Save song lyrics to a text file
filename = sanitize_filename(filename) if sanitize else filename
with open(filename, "w", encoding="utf-8") as ff:
ff.write(self._text_data)
p = Path(sanitize_filename(filename) if sanitize else filename)
p.write_text(self._text_data, encoding="utf-8")
return None

def __repr__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "lyricsgenius"
version = "3.8.0"
version = "3.9.0"
dependencies = ["beautifulsoup4>=4.12.3", "requests>=2.27.1"]
requires-python = ">=3.11"
authors = [{ name = "John W. R. Miller", email = "john.w.millr+lg@gmail.com" }]
Expand Down
48 changes: 48 additions & 0 deletions tests/test_song.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,51 @@ def test_save_lyrics_txt(song_object: Song, tmp_path: Path) -> None:
# Check that the file was written correctly
assert filename.is_file(), filename
assert filename.read_text() == song_object.lyrics, filename


def test_save_lyrics_with_path_json(song_object: Song, tmp_path: Path) -> None:
"""Test that save_lyrics creates the directory when a path is included in filename."""
output_dir = tmp_path / "output" / "nested"
song_object.save_lyrics(
filename=str(output_dir / "test_song"),
extension="json",
overwrite=True,
verbose=False,
)

saved_file = output_dir / "test_song.json"
assert saved_file.is_file(), f"Expected file at {saved_file}"
content = saved_file.read_text()
assert '"title": "Mocking the Tests"' in content, content
assert '"artist": "Py Testerson"' in content, content


def test_save_lyrics_with_path_txt(song_object: Song, tmp_path: Path) -> None:
"""Test that save_lyrics creates the directory when a path is included in filename."""
output_dir = tmp_path / "lyrics_output"
song_object.save_lyrics(
filename=str(output_dir / "test_song"),
extension="txt",
overwrite=True,
verbose=False,
)

saved_file = output_dir / "test_song.txt"
assert saved_file.is_file(), f"Expected file at {saved_file}"
assert saved_file.read_text() == song_object.lyrics


def test_save_lyrics_path_creates_directory(song_object: Song, tmp_path: Path) -> None:
"""Test that save_lyrics creates the parent directory if it doesn't exist."""
new_dir = tmp_path / "brand_new_dir"
assert not new_dir.exists()

song_object.save_lyrics(
filename=str(new_dir / "out"),
extension="json",
overwrite=True,
verbose=False,
)

assert new_dir.is_dir()
assert (new_dir / "out.json").is_file()
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.