Skip to content

Commit 3d7b1a6

Browse files
Hotfix: Restore __init__ method; more robust initialization for singleton locks (#338)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c64787f commit 3d7b1a6

File tree

2 files changed

+53
-23
lines changed

2 files changed

+53
-23
lines changed

src/filelock/_api.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,39 +85,32 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
8585
def __new__( # noqa: PLR0913
8686
cls,
8787
lock_file: str | os.PathLike[str],
88-
timeout: float = -1,
89-
mode: int = 0o644,
90-
thread_local: bool = True, # noqa: FBT001, FBT002
88+
timeout: float = -1, # noqa: ARG003
89+
mode: int = 0o644, # noqa: ARG003
90+
thread_local: bool = True, # noqa: FBT001, FBT002, ARG003
9191
*,
92-
blocking: bool = True,
92+
blocking: bool = True, # noqa: ARG003
9393
is_singleton: bool = False,
9494
**kwargs: Any, # capture remaining kwargs for subclasses # noqa: ARG003, ANN401
9595
) -> Self:
9696
"""Create a new lock object or if specified return the singleton instance for the lock file."""
9797
if not is_singleton:
98-
self = super().__new__(cls)
99-
self._initialize(lock_file, timeout, mode, thread_local, blocking=blocking, is_singleton=is_singleton)
100-
return self
98+
return super().__new__(cls)
10199

102100
instance = cls._instances.get(str(lock_file))
103101
if not instance:
104102
self = super().__new__(cls)
105-
self._initialize(lock_file, timeout, mode, thread_local, blocking=blocking, is_singleton=is_singleton)
106103
cls._instances[str(lock_file)] = self
107104
return self
108105

109-
if timeout != instance.timeout or mode != instance.mode:
110-
msg = "Singleton lock instances cannot be initialized with differing arguments"
111-
raise ValueError(msg)
112-
113106
return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322
114107

115108
def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
116109
"""Setup unique state for lock subclasses."""
117110
super().__init_subclass__(**kwargs)
118111
cls._instances = WeakValueDictionary()
119112

120-
def _initialize( # noqa: PLR0913
113+
def __init__( # noqa: PLR0913
121114
self,
122115
lock_file: str | os.PathLike[str],
123116
timeout: float = -1,
@@ -143,6 +136,34 @@ def _initialize( # noqa: PLR0913
143136
to pass the same object around.
144137
145138
"""
139+
if is_singleton and hasattr(self, "_context"):
140+
# test whether other parameters match existing instance.
141+
if not self.is_singleton:
142+
msg = "__init__ should only be called on initialized object if it is a singleton"
143+
raise RuntimeError(msg)
144+
145+
params_to_check = {
146+
"thread_local": (thread_local, self.is_thread_local()),
147+
"timeout": (timeout, self.timeout),
148+
"mode": (mode, self.mode),
149+
"blocking": (blocking, self.blocking),
150+
}
151+
152+
non_matching_params = {
153+
name: (passed_param, set_param)
154+
for name, (passed_param, set_param) in params_to_check.items()
155+
if passed_param != set_param
156+
}
157+
if not non_matching_params:
158+
return # bypass initialization because object is already initialized
159+
160+
# parameters do not match; raise error
161+
msg = "Singleton lock instances cannot be initialized with differing arguments"
162+
msg += "\nNon-matching arguments: "
163+
for param_name, (passed_param, set_param) in non_matching_params.items():
164+
msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)"
165+
raise ValueError(msg)
166+
146167
self._is_thread_local = thread_local
147168
self._is_singleton = is_singleton
148169

tests/test_filelock.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -687,9 +687,10 @@ def __init__( # noqa: PLR0913 Too many arguments to function call (6 > 5)
687687
mode: int = 0o644,
688688
thread_local: bool = True,
689689
my_param: int = 0,
690-
**kwargs: dict[str, Any],
690+
**kwargs: dict[str, Any], # noqa: ARG002
691691
) -> None:
692-
pass
692+
super().__init__(lock_file, timeout, mode, thread_local, blocking=True, is_singleton=True)
693+
self.my_param = my_param
693694

694695
lock_path = tmp_path / "a"
695696
MyFileLock(str(lock_path), my_param=1)
@@ -702,9 +703,10 @@ def __init__( # noqa: PLR0913 Too many arguments to function call (6 > 5)
702703
mode: int = 0o644,
703704
thread_local: bool = True,
704705
my_param: int = 0,
705-
**kwargs: dict[str, Any],
706+
**kwargs: dict[str, Any], # noqa: ARG002
706707
) -> None:
707-
pass
708+
super().__init__(lock_file, timeout, mode, thread_local, blocking=True, is_singleton=True)
709+
self.my_param = my_param
708710

709711
MySoftFileLock(str(lock_path), my_param=1)
710712

@@ -742,12 +744,19 @@ def test_singleton_locks_are_distinct_per_lock_file(lock_type: type[BaseFileLock
742744
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
743745
def test_singleton_locks_must_be_initialized_with_the_same_args(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
744746
lock_path = tmp_path / "a"
745-
lock = lock_type(str(lock_path), is_singleton=True) # noqa: F841
746-
747-
with pytest.raises(ValueError, match="Singleton lock instances cannot be initialized with differing arguments"):
748-
lock_type(str(lock_path), timeout=10, is_singleton=True)
749-
with pytest.raises(ValueError, match="Singleton lock instances cannot be initialized with differing arguments"):
750-
lock_type(str(lock_path), mode=0, is_singleton=True)
747+
args: dict[str, Any] = {"timeout": -1, "mode": 0o644, "thread_local": True, "blocking": True}
748+
alternate_args: dict[str, Any] = {"timeout": 10, "mode": 0, "thread_local": False, "blocking": False}
749+
750+
lock = lock_type(str(lock_path), is_singleton=True, **args)
751+
752+
for arg_name in args:
753+
general_msg = "Singleton lock instances cannot be initialized with differing arguments"
754+
altered_args = args.copy()
755+
altered_args[arg_name] = alternate_args[arg_name]
756+
with pytest.raises(ValueError, match=general_msg) as exc_info:
757+
lock_type(str(lock_path), is_singleton=True, **altered_args)
758+
exc_info.match(arg_name) # ensure specific non-matching argument is included in exception text
759+
del lock, exc_info
751760

752761

753762
@pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy")

0 commit comments

Comments
 (0)