Skip to content

Commit d14731d

Browse files
committed
Use context to share music_dir
1 parent 6c98253 commit d14731d

6 files changed

Lines changed: 38 additions & 22 deletions

File tree

beets/context.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from contextvars import ContextVar
2+
3+
# Holds the music dir context
4+
_music_dir_var: ContextVar[bytes] = ContextVar("music_dir", default=b"")
5+
6+
7+
def get_music_dir() -> bytes:
8+
"""Get the current music directory context."""
9+
return _music_dir_var.get()
10+
11+
12+
def set_music_dir(value: bytes) -> None:
13+
"""Get the current music directory context."""
14+
_music_dir_var.set(value)

beets/library/library.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import platformdirs
66

77
import beets
8-
from beets import dbcore
8+
from beets import context, dbcore
99
from beets.util import normpath
1010

1111
from . import migrations
@@ -36,6 +36,7 @@ def __init__(
3636
super().__init__(path, timeout=timeout)
3737

3838
self.directory = normpath(directory or platformdirs.user_music_path())
39+
context.set_music_dir(self.directory)
3940

4041
self.path_formats = path_formats
4142
self.replacements = replacements

beets/library/models.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
import sys
66
import time
77
import unicodedata
8-
from contextlib import suppress
98
from functools import cached_property
109
from pathlib import Path
1110
from typing import TYPE_CHECKING, Any, ClassVar
1211

1312
from mediafile import MediaFile, UnreadableFileError
1413

1514
import beets
16-
from beets import dbcore, logging, plugins, util
15+
from beets import context, dbcore, logging, plugins, util
1716
from beets.dbcore import types
1817
from beets.util import (
1918
MoveOperation,
@@ -93,22 +92,16 @@ def _setitem(self, key: str, value: Any):
9392
# Store paths relative to the music directory
9493
# Check for absolute path because item may be initialised with
9594
# a relative path already
96-
if os.path.isabs(value):
97-
# Suppress these errors since tests may initialise an Item
98-
# without the db attribute
99-
with suppress(ValueError, AttributeError):
100-
value = os.path.relpath(value, self.db.directory)
95+
if os.path.isabs(value) and (music_dir := context.get_music_dir()):
96+
value = os.path.relpath(value, music_dir)
10197

10298
return super()._setitem(key, value)
10399

104100
def __getitem__(self, key: str):
105101
value = super().__getitem__(key)
106102
if key == "path" and value:
107103
# Return absolute paths.
108-
# Suppress these errors since tests may initialise an Item
109-
# without the db attribute
110-
with suppress(ValueError, AttributeError):
111-
value = os.path.join(self.db.directory, value)
104+
value = normpath(os.path.join(context.get_music_dir(), value))
112105

113106
return value
114107

beets/util/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import contextvars
1920
import errno
2021
import fnmatch
2122
import os
@@ -1048,17 +1049,15 @@ def asciify_path(path: str, sep_replace: str) -> str:
10481049

10491050

10501051
def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
1051-
"""Apply the function `transform` to all the elements in the
1052-
iterable `items`, like `map(transform, items)` but with no return
1053-
value.
1052+
"""Apply a transformation to each item concurrently using a thread pool.
10541053
1055-
The parallelism uses threads (not processes), so this is only useful
1056-
for IO-bound `transform`s.
1054+
Propagates the calling thread's context variables into each worker,
1055+
ensuring that context-dependent state is available during parallel
1056+
execution.
10571057
"""
1058-
pool = ThreadPool()
1059-
pool.map(transform, items)
1060-
pool.close()
1061-
pool.join()
1058+
ctx = contextvars.copy_context() # snapshot parent context at call time
1059+
with ThreadPool() as pool:
1060+
pool.map(lambda item: ctx.run(transform, item), items)
10621061

10631062

10641063
class cached_classproperty(Generic[T]):

test/plugins/test_ipfs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def test_stored_hashes(self):
3737
try:
3838
if check_item.get("ipfs", with_album=False):
3939
ipfs_item = os.fsdecode(os.path.basename(want_item.path))
40-
want_path = f"/ipfs/{test_album.ipfs}/{ipfs_item}"
40+
want_path = os.path.normpath(
41+
f"/ipfs/{test_album.ipfs}/{ipfs_item}"
42+
)
4143
want_path = bytestring_path(want_path)
4244
assert check_item.path == want_path
4345
assert (

test/test_query.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ def test_explicit(self, monkeypatch, lib, q, expected_titles):
310310

311311
assert {i.title for i in lib.items(q)} == set(expected_titles)
312312

313+
def test_absolute(self, lib, helper):
314+
q = f"path::{helper.lib_path / '/aaa/bb/c.mp3'}"
315+
316+
assert {i.title for i in lib.items(q)} == {"path item"}
317+
item = lib.items(q)[0]
318+
assert item._values_fixed["path"] == str(helper.lib_path / "/aaa/bb/c.mp3").encode()
319+
313320
@pytest.mark.skipif(sys.platform == "win32", reason=WIN32_NO_IMPLICIT_PATHS)
314321
@pytest.mark.parametrize(
315322
"q, expected_titles",

0 commit comments

Comments
 (0)