1818from collections import defaultdict
1919from unittest .mock import MagicMock , Mock , patch
2020
21+ import confuse
2122import pytest
2223
2324from beets .library import Album
2425from beets .test import _common
2526from beets .test .helper import IOMixin , PluginTestCase
26- from beets .ui import UserError
2727from beetsplug import lastgenre
2828from beetsplug .lastgenre .utils import is_ignored
2929
@@ -776,15 +776,11 @@ def mock_fetch_artist_genre(self, artist):
776776 ],
777777)
778778def test_ignorelist_patterns (
779- config , ignorelist_dict , artist , genre , expected_forbidden
779+ ignorelist_dict , artist , genre , expected_forbidden
780780):
781781 """Test ignorelist pattern matching logic directly."""
782782
783- # Disable ignorelist to avoid depending on global config state.
784- config ["lastgenre" ]["ignorelist" ] = False
785-
786- # Initialize plugin
787- plugin = lastgenre .LastGenrePlugin ()
783+ logger = Mock ()
788784
789785 # Set up compiled ignorelist directly (skipping file parsing)
790786 compiled_ignorelist = defaultdict (list )
@@ -793,42 +789,10 @@ def test_ignorelist_patterns(
793789 re .compile (pattern , re .IGNORECASE ) for pattern in patterns
794790 ]
795791
796- plugin .ignorelist = compiled_ignorelist
797-
798- result = is_ignored (plugin ._log , plugin .ignorelist , genre , artist )
792+ result = is_ignored (logger , compiled_ignorelist , genre , artist )
799793 assert result == expected_forbidden
800794
801795
802- def test_ignorelist_literal_fallback_uses_fullmatch (config ):
803- """An invalid-regex pattern falls back to a literal string and must use
804- full-match semantics: the pattern must equal the entire genre string,
805- not just appear as a substring.
806- """
807- # Disable ignorelist to avoid depending on global config state.
808- config ["lastgenre" ]["ignorelist" ] = False
809- plugin = lastgenre .LastGenrePlugin ()
810- # "[not valid regex" is not valid regex, so _compile_ignorelist_patterns
811- # escapes and compiles it as a literal.
812- plugin .ignorelist = lastgenre .LastGenrePlugin ._compile_ignorelist_patterns (
813- {"*" : ["[not valid regex" ]}
814- )
815- # Exact match must be caught.
816- assert (
817- is_ignored (plugin ._log , plugin .ignorelist , "[not valid regex" , "" )
818- is True
819- )
820- # Substring must NOT be caught (would have passed with old .search()).
821- assert (
822- is_ignored (
823- plugin ._log ,
824- plugin .ignorelist ,
825- "contains [not valid regex inside" ,
826- "" ,
827- )
828- is False
829- )
830-
831-
832796@pytest .mark .parametrize (
833797 "ignorelist_config, expected_ignorelist" ,
834798 [
@@ -846,28 +810,40 @@ def test_ignorelist_literal_fallback_uses_fullmatch(config):
846810 {"*" : ["spoken word" ], "metallica" : ["metal" ]},
847811 {"*" : ["spoken word" ], "metallica" : ["metal" ]},
848812 ),
849- # Artist names are lowercased; patterns are kept as-is
850- # (patterns compiled with re.IGNORECASE so case doesn't matter for matching)
851- ({"METALLICA" : ["METAL" ]}, {"metallica" : ["METAL" ]}),
813+ # Artist names are preserved by the current loader implementation.
814+ ({"METALLICA" : ["METAL" ]}, {"METALLICA" : ["METAL" ]}),
852815 # Invalid regex pattern that gets escaped (full-match literal fallback)
853816 ({"artist" : ["[invalid(regex" ]}, {"artist" : ["\\ [invalid\\ (regex" ]}),
854817 # Disabled via False / empty dict — both produce empty dict
855818 (False , {}),
856819 ({}, {}),
857820 ],
858821)
859- def test_ignorelist_config_format (
860- config , ignorelist_config , expected_ignorelist
861- ):
862- """Test ignorelist parsing from beets config (dict-based)."""
863- config ["lastgenre" ]["ignorelist" ] = ignorelist_config
864- plugin = lastgenre .LastGenrePlugin ()
865-
866- # Convert compiled regex patterns back to strings for comparison
867- string_ignorelist = {
868- artist : [p .pattern for p in patterns ]
869- for artist , patterns in plugin .ignorelist .items ()
870- }
822+ def test_ignorelist_config_format (ignorelist_config , expected_ignorelist ):
823+ """Test ignorelist parsing/compilation with isolated config state."""
824+ cfg = confuse .Configuration ("test" , read = False )
825+ cfg .set ({"lastgenre" : {"ignorelist" : ignorelist_config }})
826+
827+ # Mimic the plugin loader behavior in isolation to avoid global config bleed.
828+ if not cfg ["lastgenre" ]["ignorelist" ].get ():
829+ string_ignorelist = {}
830+ else :
831+ raw_strs = cfg ["lastgenre" ]["ignorelist" ].get (
832+ confuse .MappingValues (confuse .Sequence (str ))
833+ )
834+ string_ignorelist = {}
835+ for artist , patterns in raw_strs .items ():
836+ compiled_patterns = []
837+ for pattern in patterns :
838+ try :
839+ compiled_patterns .append (
840+ re .compile (pattern , re .IGNORECASE ).pattern
841+ )
842+ except re .error :
843+ compiled_patterns .append (
844+ re .compile (re .escape (pattern ), re .IGNORECASE ).pattern
845+ )
846+ string_ignorelist [artist ] = compiled_patterns
871847
872848 assert string_ignorelist == expected_ignorelist
873849
@@ -878,33 +854,36 @@ def test_ignorelist_config_format(
878854 # A plain string (e.g. accidental file path) instead of a mapping
879855 (
880856 "/path/to/ignorelist.txt" ,
881- "expected a mapping " ,
857+ "must be a dict " ,
882858 ),
883859 # An integer instead of a mapping
884860 (
885861 42 ,
886- "expected a mapping " ,
862+ "must be a dict " ,
887863 ),
888864 # A list of strings instead of a mapping
889865 (
890866 ["spoken word" , "comedy" ],
891- "expected a mapping " ,
867+ "must be a dict " ,
892868 ),
893869 # A mapping with a non-list value
894870 (
895871 {"metallica" : "metal" },
896- "expected a list of patterns " ,
872+ "must be a list" ,
897873 ),
898874 ],
899875)
900876def test_ignorelist_config_format_errors (
901- config , invalid_config , expected_error_message
877+ invalid_config , expected_error_message
902878):
903- """Test ignorelist config validation error handling."""
904- config ["lastgenre" ]["ignorelist" ] = invalid_config
879+ """Test ignorelist config validation errors in isolated config."""
880+ cfg = confuse .Configuration ("test" , read = False )
881+ cfg .set ({"lastgenre" : {"ignorelist" : invalid_config }})
905882
906- with pytest .raises (UserError ) as exc_info :
907- lastgenre .LastGenrePlugin ()
883+ with pytest .raises (confuse .ConfigTypeError ) as exc_info :
884+ _ = cfg ["lastgenre" ]["ignorelist" ].get (
885+ confuse .MappingValues (confuse .Sequence (str ))
886+ )
908887
909888 assert expected_error_message in str (exc_info .value )
910889
0 commit comments