cdxcore 0.1.20__py3-none-any.whl → 0.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

cdxcore/__init__.py CHANGED
@@ -4,4 +4,4 @@ Created on June 2022
4
4
  @author: hansb
5
5
  """
6
6
 
7
- __version__ = "0.1.20" # auto-updated by setup.py
7
+ __version__ = "0.1.21" # auto-updated by setup.py
cdxcore/filelock.py ADDED
@@ -0,0 +1,821 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Simple file-based system-wide lock for both
4
+ `Linux <https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/>`__ and
5
+ `Windows <https://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html>`__.
6
+
7
+ Overview
8
+ --------
9
+
10
+ The most effective method of using ``filelock`` is calling :func:`cdxcore.filelock.AttemptLock`
11
+ in a context block::
12
+
13
+ from cdxcore.filelock import FileLock
14
+ from cdxcore.subdir import SubDir
15
+ lock_dir = SubDir("!/locks",ext="lck")
16
+ lock_name = lock_dir.full_file_name("lock1")
17
+
18
+ with AcquireLock( lock_name, timeout_second=2, timeout_retry=3 ):
19
+ # do locked activity
20
+
21
+ # locked section over
22
+
23
+ In above example the function :func:`cdxcore.filelock.AcquireLock` will attempt to acquire a file lock using the file ``lock_name`` in three attempts
24
+ with a timeout of two seconds between them. If acquiring a lock fails, a :class:`BlockingIOError` is raised.
25
+
26
+ If successful, the ``with`` construct ensures that the lock is released at the end of the block.
27
+
28
+ If we can handle a situation where a lock is not acquired safely,
29
+ the following pattern using :func:`cdxcore.filelock.AttemptLock` can bs used::
30
+
31
+ from cdxcore.filelock import FileLock
32
+ from cdxcore.subdir import SubDir
33
+ lock_dir = SubDir("!/locks",ext="lck")
34
+ lock_name = lock_dir.full_file_name("lock1")
35
+
36
+ with AttemptLock( lock_name, timeout_second=2, timeout_retry=3 ) as lock:
37
+ if lock.acquired:
38
+ # do locked activity
39
+ else:
40
+ # not locked activity
41
+
42
+ # locked section over
43
+
44
+ **Multiple Acquisitions**
45
+
46
+ Im both patterns above the lock was acquired only once. If the lock is to be acquired several times,
47
+ or to be passed to other functions, it is better to first create a :class:`cdxcore.filelock.Flock` object
48
+ and then use :func:`cdxcore.filelock.FLock.acquire` instead of :func:`cdxcore.filelock.AcquireLock`::
49
+
50
+ from cdxcore.filelock import FileLock
51
+ from cdxcore.subdir import SubDir
52
+
53
+ def subroutine( lock ):
54
+ with lock.aquire( timeout_second=2, timeout_retry=3 ):
55
+ # do locked activity
56
+
57
+ def mainroutine():
58
+ lock_dir = SubDir("!/locks",ext="lck")
59
+ lock_name = lock_dir.full_file_name("lock1")
60
+ lock = FileLock(lock_name)
61
+
62
+ with lock.aquire( timeout_second=2, timeout_retry=3 ):
63
+ # do locked activity
64
+
65
+ subroutine( lock )
66
+
67
+ In this case, :func:`cdxcore.filelock.FLock.attempt` can be used for conditional workflows
68
+ based on lock status::
69
+
70
+ def subroutine( lock ):
71
+ with lock.attempt( lock_name, timeout_second=2, timeout_retry=3 ) as lh:
72
+ if not lh.acquired:
73
+ return # job already done
74
+ # do locked activity
75
+
76
+ **Explicit State Management**
77
+
78
+ The use of ``with`` context blocks ensures that locks are released as soon
79
+ as the protected activity is finished. In some cases we may desired to finely
80
+ control such workflow.
81
+ In this case, use :func:`cdxcore.filelock.FLock.acquire` and :func:`cdxcore.filelock.FLock.release`
82
+ in pairs::
83
+
84
+ from cdxcore.filelock import FileLock
85
+ from cdxcore.subdir import SubDir
86
+
87
+ lock_dir = SubDir("!/locks",ext="lck")
88
+ lock_name = lock_dir.full_file_name("lock1")
89
+ lock = FileLock(lock_name)
90
+
91
+ def subroutine( lock ):
92
+ if not lock.acquire( timeout_second=2, timeout_retry=3 ):
93
+ return
94
+ # do protected work
95
+ lock.release()
96
+
97
+ try:
98
+ if lock.acquire( timeout_second=2, timeout_retry=3 ):
99
+ # do some protected work
100
+ lock.release()
101
+
102
+ ...
103
+
104
+ subroutine(lock)
105
+ ...
106
+
107
+ if lock.acquire( timeout_second=2, timeout_retry=3 ):
108
+ # do some protected work
109
+ lock.release()
110
+
111
+ finally:
112
+ lock.clear() # <- clears all acquisitions of the lock and stops further use.
113
+
114
+ **Garbage Collection**
115
+
116
+ By default locks will delete the underlying file using :meth:`cdxcore.filelock.FLock.clear`
117
+ upon garbage collection. This can be triggered with :func:`gc.collect`.
118
+
119
+ Import
120
+ ------
121
+ .. code-block:: python
122
+
123
+ from cdxcore.filelock import FileLock, AcquireLock
124
+
125
+ Documentation
126
+ -------------
127
+ """
128
+
129
+ from .err import verify
130
+ from .verbose import Context
131
+ from .util import datetime, fmt_datetime, fmt_seconds
132
+ from .subdir import SubDir
133
+
134
+ import os
135
+ import os.path
136
+ import time
137
+ import platform as platform
138
+ import threading as threading
139
+
140
+ _IS_WINDOWS = platform.system()[0] == "W"
141
+ _SYSTEM = "Windows" if _IS_WINDOWS else "Linux"
142
+
143
+ if _IS_WINDOWS:
144
+ # http://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html
145
+ # need to install pywin32
146
+ try:
147
+ import win32file as win32file
148
+ except Exception as e:
149
+ raise ModuleNotFoundError("pywin32") from e
150
+
151
+ import win32con
152
+ import pywintypes
153
+ import win32security
154
+ _WIN_HIGHBITS=0xffff0000 #high-order 32 bits of byte range to lock
155
+
156
+ else:
157
+ win32file = None
158
+
159
+ import os
160
+
161
+ class FLock:
162
+ pass
163
+
164
+ class LockContext(object):
165
+ """
166
+ A context handler returned by :meth:`cdxcore.filelock.Flock.acquire`,
167
+ :meth:`cdxcore.filelock.AcquireLock`, and :meth:`cdxcore.filelock.AttemptLock`.
168
+ """
169
+ def __init__(self, flock : FLock, acquired : bool):
170
+ self._flock = flock
171
+ self._acquired = acquired
172
+ def __str__(self) -> str:
173
+ return str(self._flock)
174
+ def __bool__(self) -> bool:
175
+ return self._acquired
176
+ @property
177
+ def acquired(self) -> bool:
178
+ """
179
+ Whether the underlying file lock was acquired by this context handler.
180
+ This is might be ``False`` for ``LockContext`` objects
181
+ returned by :meth:`cdxcore.filelock.AttemptLock`
182
+ """
183
+ return self._acquired
184
+ @property
185
+ def filename(self) -> str:
186
+ """ Underlying file lock name """
187
+ return self._flock.filename
188
+ def __enter__(self):
189
+ """
190
+ Enter a context block. This assumes that the lock was aquired; the corresponding ``__exit__``
191
+ will ``release()`` the lock.
192
+ """
193
+ return self
194
+ def __exit__(self, *kargs, **kwargs):
195
+ """ Release the lock """
196
+ if self._acquired:
197
+ self._flock.release()
198
+ return False # raise exceptions
199
+
200
+ class FLock(object):#NOQA
201
+ r"""
202
+ System-wide file lock.
203
+
204
+ Do not construct members of this class directly as it will not be able to create a second lock on the same
205
+ lock file within the same process. Use the "factory" function :func:`cdxcore.filelock.FileLock`
206
+ instead.
207
+ """
208
+
209
+ _CLASS_LOCK = threading.RLock()
210
+ _LOCKS = {}
211
+ _LOCK_CNT = 0
212
+
213
+ @staticmethod
214
+ def _create( filename : str, * ,
215
+ release_on_exit : bool = True,
216
+ verbose : Context|None = None ):
217
+ """
218
+ Creates a new ``FLock`` in a multi-thread-safe way.
219
+ """
220
+
221
+ with FLock._CLASS_LOCK:
222
+ flock = FLock._LOCKS.get(filename, None)
223
+ if flock is None:
224
+ flock = FLock( filename=filename, release_on_exit=release_on_exit, verbose=verbose )
225
+ FLock._LOCKS[filename] = flock
226
+ return flock
227
+
228
+ def __init__(self, filename : str, * ,
229
+ release_on_exit : bool = True,
230
+ verbose : Context|None = None ):
231
+ """
232
+ __init__
233
+ """
234
+ self._rlock = threading.RLock()
235
+ self._filename = SubDir.expandStandardRoot(filename)
236
+ self._fd = None
237
+ self._pid = os.getpid()
238
+ self._cnt = 0
239
+ self._verbose = verbose if not verbose is None else Context.quiet
240
+ self._release_on_exit = release_on_exit
241
+ nowstr = fmt_datetime(datetime.datetime.now())
242
+ self._lid = f"LOCK<{nowstr},...>: {filename}"
243
+ with self._CLASS_LOCK:
244
+ verify( not filename in self._LOCKS, lambda : f"Ther is already a lock for '{filename}' in place. Use 'FileLock()' to share locks accross threads", exception=FileExistsError)
245
+ my_cnt = self._LOCK_CNT
246
+ self._LOCK_CNT += 1
247
+ self._LOCKS[filename] = self
248
+ self._lid = f"LOCK<{nowstr},{my_cnt}>: {filename}"
249
+
250
+ def __del__(self):#NOQA
251
+ self.clear()
252
+
253
+ def clear(self):
254
+ """
255
+ Clears the current object and forces its release.
256
+ Will delete the underlying lock file if ``release_on_exit`` was used
257
+ when constructing the lock.
258
+ """
259
+ with self._rlock:
260
+ if self._filename is None:
261
+ return
262
+ if self._release_on_exit and not self._fd is None:
263
+ self._verbose.write("%s: deleting locked object", self._lid)
264
+ self.release( force=True )
265
+ with self._CLASS_LOCK:
266
+ del self._LOCKS[self._filename]
267
+ self._filename = None
268
+
269
+ def __str__(self) -> str:
270
+ """ Returns the current file name and the number of locks onbtained. """
271
+ assert not self._filename is None, ("Lock has been cleared", self._lid )
272
+ return f"{self._filename}:{self._cnt}"
273
+
274
+ def __bool__(self) -> bool:
275
+ """ Whether the lock is held """
276
+ assert not self._filename is None, ("Lock has been cleared", self._lid )
277
+ return self.locked
278
+ @property
279
+ def num_acquisitions(self) -> int:
280
+ """
281
+ Returns the net number of times the file was acquired using :meth:`cdxcore.filelock.FLock.acquire`.
282
+ Zero if the lock is not currently held.
283
+ """
284
+ assert not self._filename is None, ("Lock has been cleared", self._lid )
285
+ return self._cnt
286
+ @property
287
+ def locked(self) -> bool:
288
+ """ Whether the lock is active. """
289
+ assert not self._filename is None, ("Lock has been cleared", self._lid )
290
+ return self._cnt > 0
291
+ @property
292
+ def filename(self) -> str:
293
+ """ Return the filename of the lock. """
294
+ assert not self._filename is None, ("Lock has been cleared", self._lid )
295
+ return self._filename
296
+
297
+ def acquire(self, wait : bool = True,
298
+ *, timeout_seconds : int = 1,
299
+ timeout_retry : int = 5,
300
+ raise_on_fail : bool = True) -> LockContext|None:
301
+ """
302
+ Acquire lock.
303
+
304
+ If successful, this function returns a :class:`cdxcore.filelock.LockContext` which can
305
+ be used in a ``with`` statement as follows::
306
+
307
+ from cdxcore.filelock import FileLock
308
+ from cdxcore.subdir import SubDir
309
+
310
+ lock_dir = SubDir("!/locks",ext="lck")
311
+ lock_name = lock_dir.full_file_name("lock1")
312
+ lock = FileLock(lock_name)
313
+
314
+ with lock.aquire( timeout_second=2, timeout_retry=3 ):
315
+ # do locked activity
316
+ # no longer locked
317
+
318
+ In case ``acquire()`` fails to obtain the lock, by default it will raise an exception.
319
+
320
+ **One-Shot**
321
+
322
+ If you only acquire a lock once, it is more convenient to use :func:`cdxcore.filelock.AcquireLock`::
323
+
324
+ from cdxcore.filelock import FileLock
325
+ from cdxcore.subdir import SubDir
326
+
327
+ lock_dir = SubDir("!/locks",ext="lck")
328
+ lock_name = lock_dir.full_file_name("lock1")
329
+
330
+ with AcquireLock( lock_name, timeout_second=2, timeout_retry=3 ):
331
+ # do locked activity
332
+ # no longer locked
333
+
334
+
335
+ Parameters
336
+ ----------
337
+ wait : bool, default ``True``
338
+
339
+ * If ``False``, return immediately if the lock cannot be acquired.
340
+ * If ``True``, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
341
+
342
+ timeout_seconds : int | None, default ``None``
343
+ Number of seconds to wait before retrying.
344
+ Set to ``0``` to fail immediately.
345
+ If set to ``None``, then behaviour will depend on ``wait``:
346
+
347
+ * If wait is ``True``, then ``timeout_seconds==1``.
348
+ * If wait is ``False``, then ``timeout_seconds==0``.
349
+
350
+ timeout_retry : int | None, default ``None``
351
+ How many times to retry before timing out.
352
+ Set to ``None`` to retry indefinitely.
353
+
354
+ raise_on_fail : bool, default ``True``
355
+ By default, if the constructor fails to obtain the lock, raise an exception.
356
+ This will be either of type
357
+
358
+ * :class:`TimeoutError` if ``timeout_seconds > 0`` and ``wait==True``, or
359
+ * :class:`BlockingIOError` if ``timeout_seconds == 0`` or ``wait==False``.
360
+
361
+ If the function could not acquire a lock on the file and if ``raise_on_fail`` is ``False``,
362
+ then this function returns ``None``. This can be used for manual control workflows.
363
+
364
+ Returns
365
+ -------
366
+ Context : :class:`cdxcore.filelock.LockContext`
367
+ A context manager representing the acquired state which can be used with ``with``. If the context
368
+ manager protocol os used,
369
+ then :meth:`cdxcore.filelock.release` is called at the end of the ``with`` statement.
370
+
371
+ This function returns ``None`` if the lock could be acquired and ``raise_on_fail`` is ``False``.-heut3//..X
372
+ The method :meth:`cdxcore.filelock.FLock.attempt`
373
+ will return an unacquired context manager in case of a failure.
374
+
375
+ Raises
376
+ ------
377
+ Timeout : :class:`TimeoutError`
378
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds > 0`` and ``wait==True``, and if the call failed
379
+ to obtain the file lock.
380
+
381
+ Blocked : :class:`BlockingIOError`
382
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds == 0`` or ``wait==False``, and if the call failed
383
+ to obtain the file lock.
384
+ """
385
+
386
+ timeout_seconds = int(timeout_seconds) if not timeout_seconds is None else None
387
+ timeout_retry = int(timeout_retry) if not timeout_retry is None else None
388
+ assert not self._filename is None, ("self._filename is None. That probably means 'self' was deleted.")
389
+
390
+ if timeout_seconds is None:
391
+ timeout_seconds = 0 if not wait else 1
392
+ else:
393
+ verify( timeout_seconds>=0, "'timeout_seconds' cannot be negative", exception=ValueError)
394
+ verify( not wait or timeout_seconds>0, "Using 'timeout_seconds==0' and 'wait=True' is inconsistent.", exception=ValueError)
395
+
396
+ with self._rlock:
397
+ if not self._fd is None:
398
+ self._cnt += 1
399
+ self._verbose.write("%s: acquire(): raised lock counter to %ld", self._lid, self._cnt)
400
+ return LockContext(self,True)
401
+ assert self._cnt == 0
402
+ self._cnt = 0
403
+
404
+ i = 0
405
+ while True:
406
+ self._verbose.write("\r%s: acquire(): locking [%s]... ", self._lid, _SYSTEM, end='')
407
+ if not _IS_WINDOWS:
408
+ # Linux
409
+ # -----
410
+ # Systemwide Lock (Mutex) using files
411
+ # https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/
412
+ try:
413
+ self._fd = os.open(self._filename, os.O_CREAT|os.O_EXCL|os.O_RDWR)
414
+ os.write(self._fd, bytes("%d" % self._pid, 'utf-8'))
415
+ except OSError as e:
416
+ if not self._fd is None:
417
+ os.close(self._fd)
418
+ self._fd = None
419
+ if e.errno != 17:
420
+ self._verbose.write("failed: %s", str(e), head=False)
421
+ raise e
422
+ else:
423
+ # Windows
424
+ # ------
425
+ secur_att = win32security.SECURITY_ATTRIBUTES()
426
+ secur_att.Initialize()
427
+ try:
428
+ self._fd = win32file.CreateFile( self._filename,
429
+ win32con.GENERIC_READ|win32con.GENERIC_WRITE,
430
+ win32con.FILE_SHARE_READ|win32con.FILE_SHARE_WRITE,
431
+ secur_att,
432
+ win32con.OPEN_ALWAYS,
433
+ win32con.FILE_ATTRIBUTE_NORMAL , 0 )
434
+
435
+ ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
436
+ win32file.LockFileEx(self._fd,win32con.LOCKFILE_EXCLUSIVE_LOCK|win32con.LOCKFILE_FAIL_IMMEDIATELY,0,_WIN_HIGHBITS,ov)
437
+ except BaseException as e:
438
+ if not self._fd is None:
439
+ self._fd.Close()
440
+ self._fd = None
441
+ if e.winerror not in [17,33]:
442
+ self._verbose.write("failed: %s", str(e), head=False)
443
+ raise e
444
+
445
+ if not self._fd is None:
446
+ # success
447
+ self._cnt = 1
448
+ self._verbose.write("done; lock counter set to 1", head=False)
449
+ return LockContext(self,True)
450
+
451
+ if timeout_seconds <= 0:
452
+ break
453
+
454
+ if not timeout_retry is None:
455
+ i += 1
456
+ if i>timeout_retry:
457
+ break
458
+ self._verbose.write("locked; waiting %s retry %ld/%ld", fmt_seconds(timeout_seconds), i+1, timeout_retry, head=False)
459
+ else:
460
+ self._verbose.write("locked; waiting %s", fmt_seconds(timeout_seconds), head=False)
461
+
462
+ time.sleep(timeout_seconds)
463
+
464
+ if timeout_seconds == 0:
465
+ self._verbose.write("failed.", head=False)
466
+ if raise_on_fail: raise BlockingIOError(self._filename)
467
+ else:
468
+ self._verbose.write("timed out. Cannot access lock.", head=False)
469
+ if raise_on_fail: raise TimeoutError(self._filename, dict(timeout_retry=timeout_retry, timeout_seconds=timeout_seconds))
470
+ return None
471
+
472
+ def attempt(self, wait : bool = True,
473
+ *, timeout_seconds : int = 1,
474
+ timeout_retry : int = 5 ) -> LockContext:
475
+ """
476
+ Attempt to acquire lock.
477
+
478
+ This function attempts to obtain the file lock within the specified timeout parameters. It will return
479
+ a :class:`cdxcore.filelock.LockContext` whose property
480
+ :attr:`cdxcore.filelock.LockContext.acquired`
481
+ provides success of this attempt.
482
+
483
+ The context object can be used
484
+ using ``with`` as follows:
485
+
486
+ from cdxcore.filelock import FileLock
487
+ from cdxcore.subdir import SubDir
488
+
489
+ lock_dir = SubDir("!/locks",ext="lck")
490
+ lock_name = lock_dir.full_file_name("lock1")
491
+ lock = FileLock(lock_name)
492
+
493
+ with lock.attempt( timeout_second=2, timeout_retry=3 ) as lh:
494
+ if lh.acquired:
495
+ # do locked activity
496
+ else:
497
+ # do some other activity; warn the user; etc
498
+
499
+ # no longer locked
500
+
501
+ In contrast, the function :meth:`cdxcore.filelock.FLock.acquire` will only return a :class:`cdxcore.filelock.LockContext`
502
+ object if the acquisiton of the lock was successful.
503
+
504
+ **One-Shot**
505
+
506
+ If you only make one attempt to use a lock, it is more convenient to use :func:`cdxcore.filelock.AttemptLock`::
507
+
508
+ with AttemptLock( lock_name, timeout_second=2, timeout_retry=3 ) as lock:
509
+ if lock.acquired:
510
+ # do locked activity
511
+ else:
512
+ # do not locked activity
513
+ # no longer locked
514
+
515
+ Parameters
516
+ ----------
517
+ wait : bool, default ``True``
518
+
519
+ * If ``False``, return immediately if the lock cannot be acquired.
520
+ * If ``True``, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
521
+
522
+ timeout_seconds : int | None, default ``None``
523
+ Number of seconds to wait before retrying.
524
+ Set to ``0``` to fail immediately.
525
+ If set to ``None``, then behaviour will depend on ``wait``:
526
+
527
+ * If wait is ``True``, then ``timeout_seconds==1``.
528
+ * If wait is ``False``, then ``timeout_seconds==0``.
529
+
530
+ timeout_retry : int | None, default ``None``
531
+ How many times to retry before timing out.
532
+ Set to ``None`` to retry indefinitely.
533
+
534
+ Returns
535
+ -------
536
+ Context : :class:`cdxcore.filelock.LockContext`
537
+ A context representing the acquired state which can be used with ``with``.
538
+ Check :attr:`cdxcore.filelock.LockContext.acquired` to validate whether the lock was acquired successfully.
539
+
540
+ If ``with`` is used and :attr:`cdxcore.filelock.LockContext.acquired` is ``True``,
541
+ then :meth:`cdxcore.filelock.release` is called at the end of the ``with`` statement to
542
+ release the acquired lock.
543
+ """
544
+ r = self.acquire( wait=wait, timeout_seconds=timeout_seconds, timeout_retry=timeout_retry, raise_on_fail=False )
545
+ return r if not r is None else LockContext(self, False)
546
+
547
+ def release(self, *, force : bool = False ):
548
+ """
549
+ Release lock.
550
+
551
+ By default this function will only decreased the number of successful acquisitions by one, and will delete the file lock
552
+ only once the number of acquisitions is zero.
553
+ Use ``force`` to force an unlock.
554
+
555
+ Parameters
556
+ ----------
557
+ force : bool, default: ``False``
558
+ Whether to close the file regardless of the internal acquisition counter.
559
+
560
+ Returns
561
+ -------
562
+ Remaining : int
563
+ Returns numbner of remaining lock acquisitions; in other words returns 0 if the lock is no longer locked by this process.
564
+ """
565
+ with self._rlock:
566
+ # we must have a file handle unless 'force' is used.
567
+ if self._fd is None:
568
+ verify( force, lambda : f"Lock '{self._filename}' is not currrenty locked by this process. Use 'force' to avoid this message if need be.")
569
+ self._cnt = 0
570
+ return 0
571
+
572
+ # lower counter
573
+ assert self._cnt > 0, "Internal error - have file handle but counter is zero"
574
+ self._cnt -= 1
575
+ if self._cnt > 0 and not force:
576
+ self._verbose.write("%s: lock counter lowered to %ld", self._lid, self._cnt)
577
+ return self._cnt
578
+
579
+ # remove file
580
+ self._verbose.write("%s: releasing lock [%s]... ", self._lid, _SYSTEM, end='')
581
+ err = ""
582
+ if not _IS_WINDOWS:
583
+ # Linux
584
+ # Locks on Linxu are remarably shaky.
585
+ # In particular, it is possible to remove a locked file.
586
+ try:
587
+ os.close(self._fd)
588
+ except:
589
+ err = f"*** WARNING: could not close lock file '{self._filename}'."
590
+ pass
591
+ try:
592
+ os.remove(self._filename)
593
+ except FileNotFoundError:
594
+ pass
595
+ except:
596
+ err = f"*** WARNING: could not delete lock file '{self._filename}'." if err == "" else err
597
+ else:
598
+ try:
599
+ ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
600
+ win32file.UnlockFileEx(self._fd,0,_WIN_HIGHBITS,ov)
601
+ except:
602
+ err = "*** WARNING: could not unlock lock file '{self._filename}'."
603
+ pass
604
+ try:
605
+ self._fd.Close()
606
+ except:
607
+ err = "*** WARNING: could not close lock file '{self._filename}'." if err == "" else err
608
+ pass
609
+ try:
610
+ win32file.DeleteFile(self._filename)
611
+ except FileNotFoundError:
612
+ pass
613
+ except:
614
+ err = f"*** WARNING: could not delete lock file '{self._filename}'." if err == "" else err
615
+ pass
616
+ self._verbose.write("done; lock file deleted." if err=="" else err, head=False)
617
+ self._fd = None
618
+ self._cnt = 0
619
+ return 0
620
+
621
+ def FileLock( filename, * ,
622
+ release_on_exit : bool = True,
623
+ verbose : Context|None = None ) -> FLock:
624
+ """
625
+ Acquire a file lock object shared among threads.
626
+
627
+ This function is useful if a lock is going the be used iteratively, including
628
+ passing it to sub-routines::
629
+
630
+ from cdxcore.filelock import FileLock
631
+ from cdxcore.subdir import SubDir
632
+
633
+ def subroutine( lock ):
634
+ with lock.aquire( lock_name, timeout_second=2, timeout_retry=3 ):
635
+ # do locked activity
636
+
637
+ def mainroutine():
638
+ lock_dir = SubDir("!/locks",ext="lck")
639
+ lock_name = lock_dir.full_file_name("lock1")
640
+ lock = FileLock(lock_name)
641
+
642
+ with lock.aquire( timeout_second=2, timeout_retry=3 ):
643
+ # do locked activity
644
+
645
+ subroutine( lock )
646
+
647
+ If the lock is only used for a one-of acquisition, it is usally
648
+ prettier to use :func:`cdxcore.filelock.AcquireLock` instead.
649
+
650
+
651
+ Parameters
652
+ ----------
653
+ filename : str
654
+ Filename of the lock.
655
+
656
+ ``filename`` may start with ``'!/'`` to refer to the temp directory, or ``'~/'`` to refer to the user directory.
657
+ On Unix ``'/dev/shm/'`` can be used to refer to the standard shared memory directory in case a shared memory
658
+ file is being locked.
659
+
660
+ release_on_exit : bool, default ``True``
661
+ Whether to auto-release the lock upon exit.
662
+
663
+ verbose : :class:`cdxcore.verbose.Context` | None, default ``None``
664
+ Context which will print out operating information of the lock. This is helpful for debugging.
665
+ In particular, it will track ``__del__()`` function calls.
666
+ Set to ``None`` to supress printing any context.
667
+
668
+ Returns
669
+ -------
670
+ lock : :class:`cdxcore.filelock.FLock`
671
+ The lock. This function will re-use an existing lock if it has been created elsewhere by the same process.
672
+ """
673
+ return FLock._create( filename=filename,
674
+ release_on_exit=release_on_exit,
675
+ verbose=verbose )
676
+
677
+ def AttemptLock(filename, * ,
678
+ wait : bool = True,
679
+ timeout_seconds : int|None = None,
680
+ timeout_retry : int|None = None,
681
+ verbose : Context|None = None ) -> FLock:
682
+ """
683
+ Attempt to acquire a file lock and return a context handler even if the lock was not acquired.
684
+ The context handler's :attr:`cdxcore.filelock.LockContext.acquired` can be used to assess
685
+ whether the lock was acquired.
686
+
687
+ The pattern is as follows::
688
+
689
+ from cdxcore.filelock import FileLock
690
+ from cdxcore.subdir import SubDir
691
+
692
+ lock_dir = SubDir("!/locks",ext="lck")
693
+ lock_name = lock_dir.full_file_name("lock1")
694
+
695
+ with AttemptLock( lock_name, timeout_second=2, timeout_retry=3 ) as lock:
696
+ if lock.acquired:
697
+ # do locked activity
698
+ else:
699
+ # do not locked activity
700
+ # no longer locked
701
+
702
+ Parameters
703
+ ----------
704
+ filename : str
705
+ Filename of the lock.
706
+
707
+ ``filename`` may start with ``'!/'`` to refer to the temp directory, or ``'~/'`` to refer to the user directory.
708
+ On Unix ``'/dev/shm/'`` can be used to refer to the standard shared memory directory in case a shared memory
709
+ file is being locked.
710
+
711
+ wait : bool, default ``True``
712
+
713
+ * If ``False``, return immediately if the lock cannot be acquired.
714
+ * If ``True``, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
715
+
716
+ timeout_seconds : int | None, default ``None``
717
+ Number of seconds to wait before retrying.
718
+ Set to ``0``` to fail immediately.
719
+ If set to ``None``, then behaviour will depend on ``wait``:
720
+
721
+ * If wait is ``True``, then ``timeout_seconds==1``.
722
+ * If wait is ``False``, then ``timeout_seconds==0``.
723
+
724
+ timeout_retry : int | None, default ``None``
725
+ How many times to retry before timing out.
726
+ Set to ``None`` to retry indefinitely.
727
+
728
+ verbose : :class:`cdxcore.verbose.Context` | None, default ``None``
729
+ Context which will print out operating information of the lock. This is helpful for debugging.
730
+ In particular, it will track ``__del__()`` function calls.
731
+ Set to ``None`` to supress printing any context.
732
+
733
+ Exceptions
734
+ ----------
735
+ Will not raise any exceptions
736
+
737
+ Returns
738
+ -------
739
+ Filelock if acquired or None
740
+ """
741
+ flock = FLock._create( filename=filename,
742
+ release_on_exit=True,
743
+ verbose=verbose )
744
+ return flock.attempt( wait=wait, timeout_seconds=timeout_seconds, timeout_retry=timeout_retry, raise_on_fail=True )
745
+
746
+ def AcquireLock(filename, * ,
747
+ wait : bool = True,
748
+ timeout_seconds : int|None = None,
749
+ timeout_retry : int|None = None,
750
+ verbose : Context|None = None ) -> LockContext:
751
+ """
752
+ Acquire a file lock and return a context handler, or raise an exception.
753
+ The context handler can be used in a ``with`` statement as follows::
754
+
755
+ from cdxcore.filelock import FileLock
756
+ from cdxcore.subdir import SubDir
757
+
758
+ lock_dir = SubDir("!/locks",ext="lck")
759
+ lock_name = lock_dir.full_file_name("lock1")
760
+
761
+ with AcquireLock( lock_name, timeout_second=2, timeout_retry=3 ):
762
+ # do locked activity
763
+ # no longer locked
764
+
765
+ Note that this function will raise an exception if the lock could be acquired.
766
+ Use :func:`cdxcore.filelock.AttemptLock` to obtain a context handler
767
+ even if the lock was not acquired.
768
+
769
+ Parameters
770
+ ----------
771
+ filename : str
772
+ Filename of the lock.
773
+
774
+ ``filename`` may start with ``'!/'`` to refer to the temp directory, or ``'~/'`` to refer to the user directory.
775
+ On Unix ``'/dev/shm/'`` can be used to refer to the standard shared memory directory in case a shared memory
776
+ file is being locked.
777
+
778
+ wait : bool, default ``True``
779
+
780
+ * If ``False``, return immediately if the lock cannot be acquired.
781
+ * If ``True``, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
782
+
783
+ timeout_seconds : int | None, default ``None``
784
+ Number of seconds to wait before retrying.
785
+ Set to ``0``` to fail immediately.
786
+ If set to ``None``, then behaviour will depend on ``wait``:
787
+
788
+ * If wait is ``True``, then ``timeout_seconds==1``.
789
+ * If wait is ``False``, then ``timeout_seconds==0``.
790
+
791
+ timeout_retry : int | None, default ``None``
792
+ How many times to retry before timing out.
793
+ Set to ``None`` to retry indefinitely.
794
+
795
+ verbose : :class:`cdxcore.verbose.Context` | None, default ``None``
796
+ Context which will print out operating information of the lock. This is helpful for debugging.
797
+ In particular, it will track ``__del__()`` function calls.
798
+ Set to ``None`` to supress printing any context.
799
+
800
+ Returns
801
+ -------
802
+ Context : :class:`cdxcore.filelock.LockContext`
803
+ A context representing the acquired state which can be used with ``with``. The function
804
+ :meth:`cdxcore.filelock.release` is called at the end of the ``with`` statement to
805
+ release the acquired lock.
806
+
807
+ Raises
808
+ ------
809
+ Timeout : :class:`TimeoutError`
810
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds > 0`` and ``wait==True``, and if the call failed
811
+ to obtain the file lock.
812
+
813
+ Blocked : :class:`BlockingIOError`
814
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds == 0`` or ``wait==False``, and if the call failed
815
+ to obtain the file lock.
816
+ """
817
+ flock = FLock._create( filename=filename,
818
+ release_on_exit=True,
819
+ verbose=verbose )
820
+ return flock.acquire( wait=wait, timeout_seconds=timeout_seconds, timeout_retry=timeout_retry, raise_on_fail=True )
821
+
cdxcore/subdir.py CHANGED
@@ -3390,11 +3390,11 @@ def VersionedCacheRoot( directory : str, *,
3390
3390
 
3391
3391
  Returns
3392
3392
  -------
3393
- Root : SubDir
3393
+ Root : :class:`cdxcore.subdir.SubDir`
3394
3394
  A root directory suitable for caching.
3395
3395
  """
3396
3396
  controller = CacheController(**controller_kwargs) if len(controller_kwargs) > 0 else None
3397
- return SubDir( directory=directory, ext=ext, fmt=fmt, create_directory=create_directory, controller=controller )
3397
+ return SubDir( directory, ext=ext, fmt=fmt, create_directory=create_directory, controller=controller )
3398
3398
 
3399
3399
  version = version_decorator
3400
3400
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdxcore
3
- Version: 0.1.20
3
+ Version: 0.1.21
4
4
  Summary: Basic Python Tools; upgraded cdxbasics
5
5
  Author-email: Hans Buehler <github@buehler.london>
6
6
  License-Expression: MIT
@@ -1,19 +1,20 @@
1
- cdxcore/__init__.py,sha256=WVxA_BhQB2hApuwGE0ikbK9uKqgtLNdr9nmDuX02ZVs,127
1
+ cdxcore/__init__.py,sha256=tTewrG_1bFOYsVSvBReV8lY8tGQSqaDr_rEEDNk-yP0,127
2
2
  cdxcore/config.py,sha256=RaxpAnTEyGXLiq-1O2prXFBb2sdyqB6hHDHPAFGe2k0,98472
3
3
  cdxcore/deferred.py,sha256=5bREI2IQ3uW3EMDcAO17gJbVnbUIV__-bdEUQpZIEeU,44684
4
4
  cdxcore/dynalimits.py,sha256=TXEwa4wCdeM5epG1ceezaHwvmhnbU-oXlXpHNbOMQR8,11293
5
5
  cdxcore/dynaplot.py,sha256=1g4IFTfUir13BEDNxDXxUvlwh__kq5n0kgehZ-e8ah4,51031
6
6
  cdxcore/err.py,sha256=3JP1IZDMIKayTt-ZNV9PCYdjtvkybQTjZOrRmBZWyfg,14709
7
+ cdxcore/filelock.py,sha256=D2U8rzxYkeuDc8RLRbePBQ939PPxcJuRC8sahmVx-x8,34781
7
8
  cdxcore/jcpool.py,sha256=Vw8o8S4_qTVQNIr2sRaBR_kHz_wO0zpYs0QjlKSYFz8,27280
8
9
  cdxcore/npio.py,sha256=SVpKkFt6lyRogkns-oky0wEiWgVTalgB0OdaSvyWtlU,24212
9
10
  cdxcore/npshm.py,sha256=9buYPNJoNlw69NZQ-nLF13PEWBWNx51a0gjQw5Gc24U,18368
10
11
  cdxcore/pretty.py,sha256=FsI62rlaqRX1E-uPCSnu0M4UoCQ5Z55TDvYPnzTNO70,17220
11
- cdxcore/subdir.py,sha256=n3slNxyUC8_EMSlACscm3dn-WKsG_EfdK_xsuFPfwBw,176880
12
+ cdxcore/subdir.py,sha256=tSocug4h5iV2mTYOMojg2IXSwAenvYZt7cRdEXTG6UU,176894
12
13
  cdxcore/uniquehash.py,sha256=0aMJXxGzA6xLyuaygRZF2Z-1hwznrvqSZQM7K1bycTQ,50705
13
14
  cdxcore/util.py,sha256=dqCEhrDvUM4qjeUwb6n8jPfS8WC4Navcj83gDjXfRFE,39349
14
15
  cdxcore/verbose.py,sha256=vsjGTVnAHMPg2L2RfsowWKKPjUSnQJ3F653vDTydBkI,30223
15
16
  cdxcore/version.py,sha256=S3H0ktEZsM0zAj3EQ5AdrZ2KxWhQtemd8clzEFE9hOQ,27160
16
- cdxcore-0.1.20.dist-info/licenses/LICENSE,sha256=M-cisgK9kb1bqVRJ7vrCxHcMQQfDxdY3c2YFJJWfNQg,1090
17
+ cdxcore-0.1.21.dist-info/licenses/LICENSE,sha256=M-cisgK9kb1bqVRJ7vrCxHcMQQfDxdY3c2YFJJWfNQg,1090
17
18
  docs/source/conf.py,sha256=yn3LYgw3sT45mUyll-B2emVp6jg7H6KfAHOcBg_MNv4,4182
18
19
  tests/test_config.py,sha256=N86mH3y7k3LXEmU8uPLfrmRMZ-80VhlD35nBbpLmebg,15617
19
20
  tests/test_deferred.py,sha256=4Xsb76r-XqHKiBuHa4jbErjMWbrgHXfPwewzzY4lf9Y,7922
@@ -27,13 +28,13 @@ tests/test_uniquehash.py,sha256=n6ZCkdBw-iRsyzeAEmrnLK0hJLGH6l_Dtt_KIkSa6KA,2463
27
28
  tests/test_util.py,sha256=mwtz3o_RiLNn808928xP4jawHmGNxzUXiatMh0zBc3o,24342
28
29
  tests/test_verbose.py,sha256=zXheIqAVOnwML2zsCjLugjYzB_KNzU_S4Xu2CSb4o10,4723
29
30
  tests/test_version.py,sha256=m_RODPDFlXTC1jAIczm3t1v6tXzqczDiUFFJtGvRG98,4381
30
- tmp/filelock.py,sha256=HqnHZhSCESaOA3ClrdWPW_GZpyo7c3VRSEofAV-khKM,18137
31
+ tmp/filelock.py,sha256=MhLcCT4iRbNfvL6LJz_yIfUrnEnReKwk5A6VzbilcTs,21189
31
32
  tmp/np.py,sha256=2MynhiaTfmx984Gz7TwfZH3t7GCmCAQiyeWzDDCL6_k,47686
32
33
  tmp/npsh1.py,sha256=mNucUl2-jNmE84GlMlliB4aJ0UQ9FqdymgcY_9mLeZY,15432
33
34
  tmp/sharedarray.py,sha256=dNOT1ObCc3nM3qA3OA508NcENIBnkmWMxRPCqvMVa8A,12862
34
35
  up/git_message.py,sha256=EfSH7Pit3ZoCiRqSMwRCUN_QyuwreU4LTIyGSutBlm4,123
35
36
  up/pip_modify_setup.py,sha256=Esaml4yA9tFsqxLhk5bWSwvKCURONjQqfyChgFV2TSY,1584
36
- cdxcore-0.1.20.dist-info/METADATA,sha256=n_ogJ_pHnw1I5ztZ1XAVbMlsZcdB4JTUyTdNSIO2wug,5939
37
- cdxcore-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- cdxcore-0.1.20.dist-info/top_level.txt,sha256=phNSwCyJFe7UP2YMoi8o6ykhotatlIbJHjTp9EHM51k,26
39
- cdxcore-0.1.20.dist-info/RECORD,,
37
+ cdxcore-0.1.21.dist-info/METADATA,sha256=CbOmczjod2520L0-DwaavH20Dm2tJzaAOiz-mYlvZRs,5939
38
+ cdxcore-0.1.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ cdxcore-0.1.21.dist-info/top_level.txt,sha256=phNSwCyJFe7UP2YMoi8o6ykhotatlIbJHjTp9EHM51k,26
40
+ cdxcore-0.1.21.dist-info/RECORD,,
tmp/filelock.py CHANGED
@@ -1,9 +1,70 @@
1
+ # -*- coding: utf-8 -*-
1
2
  """
2
- subdir
3
- Simple class to keep track of directory sturctures and for automated caching on disk
4
- Hans Buehler 2020
5
- """
3
+ Simple filelock for both Windows and Linux.
4
+
5
+ The class :class:`cdxcore.filelock.FileLock` is for common file lock patterns for
6
+ `Linux <https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/>`__ and
7
+ `Windows <https://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html>`__.
8
+
9
+ Overview
10
+ --------
11
+
12
+ The main use of this module are the functions
13
+ :func:`cdxcore.err.verify` and :func:`cdxcore.err.warn_if`.
14
+ Both test some runtime condition and will either
15
+ raise an ``Exception`` or issue a ``Warning`` if triggered. In both cases, required string formatting is only performed
16
+ if the event is actually triggered.
17
+
18
+ This way we are able to write neat code which produces robust,
19
+ informative errors and warnings without impeding runtime performance.
20
+
21
+ Example::
22
+
23
+ from cdxcore.err import verify, warn_if
24
+ import numpy as np
25
+
26
+ def f( x : np.ndarray ):
27
+ std = np.std(x,axis=0,keepdims=True)
28
+ verify( np.all( std>1E-8 ), "Cannot normalize 'x' by standard deviation: standard deviations are {std}", std=std )
29
+ x /= std
30
+
31
+ f( np.zeros((10,10)) )
32
+
33
+ raises a :class:`RuntimeError`
34
+
35
+ .. code-block:: python
36
+
37
+ Cannot normalize 'x' by standard deviation: standard deviations are [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
38
+
39
+ For warnings, we can use ``warn_if``::
40
+
41
+ from cdxcore.err import verify, warn_if
42
+ import numpy as np
43
+
44
+ def f( x : np.ndarray ):
45
+ std = np.std(x,axis=0,keepdims=True)
46
+ warn_if( not np.all( std>1E-8 ), lambda : f"Normalizing 'x' by standard deviation: standard deviations are {std}" )
47
+ x = np.where( std<1E-8, 0., x/np.where( std<1E-8, 1., std ) )
48
+
49
+ f( np.zeros((10,10)) )
50
+
51
+ issues a warning::
52
+
53
+ RuntimeWarning: Normalizing 'x' by standard deviation: standard deviations are [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
54
+ warn_if( not np.all( std>1E-8 ), "Normalizing 'x' by standard deviation: standard deviations are {std}", std=std )
6
55
 
56
+ Note that though we used two different approaches for message formatting, the the error messages in both cases
57
+ are only formatted if the condition in ``verify`` is not met.
58
+
59
+ Import
60
+ ------
61
+ .. code-block:: python
62
+
63
+ from cdxcore.err import verify, warn_if, error, warn
64
+
65
+ Documentation
66
+ -------------
67
+ """
7
68
  from .logger import Logger
8
69
  from .verbose import Context
9
70
  from .util import datetime, fmt_datetime, fmt_seconds
@@ -29,7 +90,7 @@ if IS_WINDOWS:
29
90
  import pywintypes
30
91
  import win32security
31
92
  import win32api
32
- WIN_HIGHBITS=0xffff0000 #high-order 32 bits of byte range to lock
93
+ _WIN_HIGHBITS=0xffff0000 #high-order 32 bits of byte range to lock
33
94
 
34
95
  else:
35
96
  win32file = None
@@ -37,9 +98,67 @@ else:
37
98
  import os
38
99
 
39
100
  class FileLock(object):
40
- """
41
- Systemwide Lock (Mutex) using files
42
- https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/
101
+ r"""
102
+ System-wide file lock.
103
+
104
+
105
+
106
+
107
+ Parameters
108
+ ----------
109
+ filename : str
110
+ Filename of the lock.
111
+ ``filename`` may start with ``'!/'`` to refer to the temp directory, or ``'~/'`` to refer to the user directory.
112
+ On Unix ``'/dev/shm/'`` can be used to refer to the standard shared memory directory in case a shared memory
113
+ file is being locked.
114
+
115
+ acquire : bool, default: ``False``
116
+ Whether to attempt aquiring the lock upon initialization.
117
+
118
+ release_on_exit : bool, default ``True``
119
+ Whether to auto-release the lock upon exit.
120
+
121
+ wait : bool, default ``True``
122
+ * If ``False``, return immediately if the lock cannot be acquired.
123
+ * If ``True``, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
124
+
125
+ timeout_seconds : int | None, default ``None``
126
+ Number of seconds to wait before retrying.
127
+ Set to ``0``` to fail immediately.
128
+ If set to ``None``, then behaviour will depend on ``wait``:
129
+
130
+ * If wait is ``True``, then ``timeout_seconds==1``;
131
+ * If wait is ``False``, then ``timeout_seconds==0``.
132
+
133
+ timeout_retry : int|None, default ``None``
134
+ How many times to retry before timing out.
135
+ Set to ``None`` to retry indefinitely.
136
+
137
+ raise_on_fail : bool, default ``True``
138
+ If the constructor fails to obtain the lock, raise an exception.
139
+ This will be either of type
140
+
141
+ * :class:`TimeoutError` if ``timeout_seconds > 0`` and ``wait==True``, or
142
+ * :class:`BlockingIOError` if ``timeout_seconds == 0`` or ``wait==False``.
143
+
144
+ verbose : :class:`cdxcore.verbose.Context`|None, default ``None``
145
+ Context which will print out operating information of the lock. This is helpful for debugging.
146
+ In particular, it will track ``__del__()`` function calls.
147
+ Set to ``None`` to supress printing any context.
148
+
149
+ Exceptions
150
+ ----------
151
+ Timeout : :class:`TimeoutError`
152
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds > 0`` and ``wait==True``, and if the call failed
153
+ to obtain the file lock.
154
+
155
+ Blocked : :class:`BlockingIOError`
156
+ Raised if ``acquire`` is ``True``, if ``timeout_seconds == 0`` or ``wait==False``, and if the call failed
157
+ to obtain the file lock.
158
+
159
+ If acquire is True, then this constructor may raise an exception:
160
+ TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
161
+ BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
43
162
  """
44
163
 
45
164
  __LOCK_ID = 0
@@ -48,57 +167,19 @@ class FileLock(object):
48
167
  acquire : bool = False,
49
168
  release_on_exit : bool = True,
50
169
  wait : bool = True,
51
- timeout_seconds : int = None,
52
- timeout_retry : int = None,
170
+ timeout_seconds : int|None = None,
171
+ timeout_retry : int|None = None,
53
172
  raise_on_fail : bool = True,
54
- verbose : Context = Context.quiet ):
173
+ verbose : Context|None = None ):
55
174
  """
56
- Initialize new lock with name 'filename'
57
- Acquire the lock if 'acquire' is True
58
-
59
- Parameters
60
- ----------
61
- filename :
62
- Filename of the lock.
63
- 'filename' may start with '!/' to refer to the temp directory, or '~/' to refer to the user directory.
64
- On Unix /dev/shm/ can be used to refer to shared memory.
65
- acquire :
66
- Whether to attempt aquiring the lock upon initialization
67
- release_on_exit :
68
- Whether to auto-release the lock upon exit.
69
- wait :
70
- If False, return immediately if the lock cannot be acquired.
71
- If True, wait with below parameters; in particular if these are left as defaults the lock will wait indefinitely.
72
- timeout_seconds :
73
- Number of seconds to wait before retrying.
74
- Set to 0 to fail immediately.
75
- If set to None, then its value will depend on 'wait'.
76
- If wait is True, then timeout_seconds==1; if wait is False, then timeout_seconds==0
77
- timeout_retry :
78
- How many times to retry before timing out.
79
- Set to None to retry indefinitely.
80
- raise_on_fail :
81
- If the constructor fails to obtain the lock, raise an Exception:
82
- This will be either of type
83
- TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
84
- BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
85
- verbose :
86
- Context which will print out operating information of the lock. This is helpful for debugging.
87
- In particular, it will track __del__() function calls.
88
- Set to None to print all context.
89
-
90
- Exceptions
91
- ----------
92
- If acquire is True, then this constructor may raise an exception:
93
- TimeoutError if 'timeout_seconds' > 0 and 'wait' is True, or
94
- BlockingIOError if 'timeout_seconds' == 0 or 'wait' is False.
175
+ __init__
95
176
  """
96
177
  self._filename = SubDir.expandStandardRoot(filename)
97
178
  self._fd = None
98
179
  self._pid = os.getpid()
99
180
  self._cnt = 0
100
181
  self._lid = "LOCK" + fmt_datetime(datetime.datetime.now()) + (",%03ld:" % FileLock.__LOCK_ID) + filename
101
- self.verbose = verbose if not verbose is None else Context(None)
182
+ self.verbose = verbose if not verbose is None else Context.quiet
102
183
  self.release_on_exit = release_on_exit
103
184
  FileLock.__LOCK_ID +=1
104
185
 
@@ -133,7 +214,7 @@ class FileLock(object):
133
214
  """ Return filename """
134
215
  return self._filename
135
216
 
136
- def acquire(self, wait = True,
217
+ def acquire(self, wait : bool = True,
137
218
  *, timeout_seconds : int = 1,
138
219
  timeout_retry : int = 5,
139
220
  raise_on_fail : bool = True) -> int:
@@ -142,9 +223,10 @@ class FileLock(object):
142
223
 
143
224
  Parameters
144
225
  ----------
145
- wait :
146
- If False, return immediately if the lock cannot be acquired.
226
+ wait : bool, default ``True``
227
+ If ``False``, return immediately if the lock cannot be acquired.
147
228
  If True, wait with below parameters
229
+
148
230
  timeout_seconds :
149
231
  Number of seconds to wait before retrying. If wait is False, this must be zero.
150
232
  Set to 0 to fail immediately.
@@ -188,6 +270,8 @@ class FileLock(object):
188
270
  if not IS_WINDOWS:
189
271
  # Linux
190
272
  # -----
273
+ # Systemwide Lock (Mutex) using files
274
+ # https://code.activestate.com/recipes/519626-simple-file-based-mutex-for-very-basic-ipc/
191
275
  try:
192
276
  self._fd = os.open(self._filename, os.O_CREAT|os.O_EXCL|os.O_RDWR)
193
277
  os.write(self._fd, bytes("%d" % self._pid, 'utf-8'))
@@ -212,7 +296,7 @@ class FileLock(object):
212
296
  win32con.FILE_ATTRIBUTE_NORMAL , 0 )
213
297
 
214
298
  ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
215
- win32file.LockFileEx(self._fd,win32con.LOCKFILE_EXCLUSIVE_LOCK|win32con.LOCKFILE_FAIL_IMMEDIATELY,0,WIN_HIGHBITS,ov)
299
+ win32file.LockFileEx(self._fd,win32con.LOCKFILE_EXCLUSIVE_LOCK|win32con.LOCKFILE_FAIL_IMMEDIATELY,0,_WIN_HIGHBITS,ov)
216
300
  except BaseException as e:
217
301
  if not self._fd is None:
218
302
  self._fd.Close()
@@ -291,7 +375,7 @@ class FileLock(object):
291
375
  else:
292
376
  try:
293
377
  ov=pywintypes.OVERLAPPED() #used to indicate starting region to lock
294
- win32file.UnlockFileEx(self._fd,0,WIN_HIGHBITS,ov)
378
+ win32file.UnlockFileEx(self._fd,0,_WIN_HIGHBITS,ov)
295
379
  except:
296
380
  err = "*** WARNING: could not unlock file."
297
381
  pass