Skip to content

ENH: replace portalocker with filelock #3025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 11, 2019
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
13 changes: 3 additions & 10 deletions nipype/algorithms/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,11 +799,11 @@ def _run_interface(self, runtime):
'(http://pandas.pydata.org/) to run.'), e)

try:
import lockfile as pl
from filelock import SoftFileLock
self._have_lock = True
except ImportError:
from warnings import warn
warn(('Python module lockfile was not found: AddCSVRow will not be'
warn(('Python module filelock was not found: AddCSVRow will not be'
' thread-safe in multi-processor execution'))

input_dict = {}
Expand All @@ -822,7 +822,7 @@ def _run_interface(self, runtime):
df = pd.DataFrame([input_dict])

if self._have_lock:
self._lock = pl.FileLock(self.inputs.in_file)
self._lock = SoftFileLock('%s.lock' % self.inputs.in_file)

# Acquire lock
self._lock.acquire()
Expand All @@ -837,13 +837,6 @@ def _run_interface(self, runtime):
if self._have_lock:
self._lock.release()

# Using nipype.external.portalocker this might be something like:
# with pl.Lock(self.inputs.in_file, timeout=1) as fh:
# if op.exists(fh):
# formerdf = pd.read_csv(fh, index_col=0)
# df = pd.concat([formerdf, df], ignore_index=True)
# df.to_csv(fh)

return runtime

def _list_outputs(self):
Expand Down
23 changes: 5 additions & 18 deletions nipype/external/cloghandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@
testing, performance was more than adequate, but if you need a high-volume or
low-latency solution, I suggest you look elsewhere.

This module currently only support the 'nt' and 'posix' platforms due to the
usage of the portalocker module. I do not have access to any other platforms
for testing, patches are welcome.

See the README file for an example usage of this module.

"""
Expand All @@ -63,13 +59,7 @@
except ImportError:
codecs = None

# Question/TODO: Should we have a fallback mode if we can't load portalocker /
# we should still be better off than with the standard RotattingFileHandler
# class, right? We do some rename checking... that should prevent some file
# clobbering that the builtin class allows.

# sibling module than handles all the ugly platform-specific details of file locking
from .portalocker import lock, unlock, LOCK_EX, LOCK_NB, LockException
from filelock import SoftFileLock

# A client can set this to true to automatically convert relative paths to
# absolute paths (which will also hide the absolute path warnings)
Expand Down Expand Up @@ -168,11 +158,8 @@ def __init__(self,
self.maxBytes = maxBytes
self.backupCount = backupCount
# Prevent multiple extensions on the lock file (Only handles the normal "*.log" case.)
if filename.endswith(".log"):
lock_file = filename[:-4]
else:
lock_file = filename
self.stream_lock = open(lock_file + ".lock", "w")
self.lock_file = '%s.lock' % filename
self.stream_lock = SoftFileLock(self.lock_file)

# For debug mode, swap out the "_degrade()" method with a more a verbose one.
if debug:
Expand All @@ -189,7 +176,7 @@ def acquire(self):
in 'degraded' mode. """
# handle thread lock
Handler.acquire(self)
lock(self.stream_lock, LOCK_EX)
self.stream_lock.acquire()
if self.stream.closed:
self._openFile(self.mode)

Expand All @@ -206,7 +193,7 @@ def release(self):
self.stream.close()
finally:
try:
unlock(self.stream_lock)
self.stream_lock.release()
finally:
# release thread lock
Handler.release(self)
Expand Down
145 changes: 0 additions & 145 deletions nipype/external/portalocker.py

This file was deleted.

1 change: 1 addition & 0 deletions nipype/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def get_nipype_gitversion():
'scipy>=%s,<%s ; python_version <= "3.4"' % (SCIPY_MIN_VERSION, SCIPY_MAX_VERSION_34),
'simplejson>=%s' % SIMPLEJSON_MIN_VERSION,
'traits>=%s,!=5.0' % TRAITS_MIN_VERSION,
'filelock>=3.0.0'
]

# neurdflib has to come after prov
Expand Down
22 changes: 11 additions & 11 deletions nipype/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from future import standard_library

from .misc import str2bool
from ..external import portalocker
from filelock import SoftFileLock

standard_library.install_aliases()

Expand Down Expand Up @@ -209,9 +209,9 @@ def get_data(self, key):
"""Read options file"""
if not os.path.exists(self.data_file):
return None
with open(self.data_file, 'rt') as file:
portalocker.lock(file, portalocker.LOCK_EX)
datadict = load(file)
with SoftFileLock('%s.lock' % self.data_file):
with open(self.data_file, 'rt') as file:
datadict = load(file)
if key in datadict:
return datadict[key]
return None
Expand All @@ -220,17 +220,17 @@ def save_data(self, key, value):
"""Store config flie"""
datadict = {}
if os.path.exists(self.data_file):
with open(self.data_file, 'rt') as file:
portalocker.lock(file, portalocker.LOCK_EX)
datadict = load(file)
with SoftFileLock('%s.lock' % self.data_file):
with open(self.data_file, 'rt') as file:
datadict = load(file)
else:
dirname = os.path.dirname(self.data_file)
if not os.path.exists(dirname):
mkdir_p(dirname)
with open(self.data_file, 'wt') as file:
portalocker.lock(file, portalocker.LOCK_EX)
datadict[key] = value
dump(datadict, file)
with SoftFileLock('%s.lock' % self.data_file):
with open(self.data_file, 'wt') as file:
datadict[key] = value
dump(datadict, file)

def update_config(self, config_dict):
"""Extend internal dictionary with config_dict"""
Expand Down
71 changes: 33 additions & 38 deletions nipype/utils/filemanip.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import contextlib
import posixpath
import simplejson as json
from filelock import SoftFileLock

from builtins import str, bytes, open

Expand Down Expand Up @@ -684,36 +685,33 @@ def loadpkl(infile):

unpkl = None
with indirectory(infile.parent):
pkl_file = pklopen(infile.name, 'rb')

try: # Look if pkl file contains version file
pkl_metadata_line = pkl_file.readline()
pkl_metadata = json.loads(pkl_metadata_line)
except (UnicodeDecodeError, json.JSONDecodeError):
# Could not get version info
pkl_file.seek(0)

try:
unpkl = pickle.load(pkl_file)
except UnicodeDecodeError:
# Was this pickle created with Python 2.x?
unpkl = pickle.load(pkl_file, fix_imports=True, encoding='utf-8')
fmlogger.info('Successfully loaded pkl in compatibility mode.')
# Unpickling problems
except Exception as e:
if pkl_metadata and 'version' in pkl_metadata:
from nipype import __version__ as version
if pkl_metadata['version'] != version:
fmlogger.error("""\
with SoftFileLock('%s.lock' % infile.name):
with pklopen(infile.name, 'rb') as pkl_file:
try: # Look if pkl file contains version file
pkl_metadata_line = pkl_file.readline()
pkl_metadata = json.loads(pkl_metadata_line)
except (UnicodeDecodeError, json.JSONDecodeError):
# Could not get version info
pkl_file.seek(0)
try:
unpkl = pickle.load(pkl_file)
except UnicodeDecodeError:
# Was this pickle created with Python 2.x?
unpkl = pickle.load(pkl_file, fix_imports=True, encoding='utf-8')
fmlogger.info('Successfully loaded pkl in compatibility mode.')
# Unpickling problems
except Exception as e:
if pkl_metadata and 'version' in pkl_metadata:
from nipype import __version__ as version
if pkl_metadata['version'] != version:
fmlogger.error("""\
Attempted to open a results file generated by Nipype version %s, \
with an incompatible Nipype version (%s)""", pkl_metadata['version'], version)
raise e
fmlogger.error("""\
raise e
fmlogger.error("""\
No metadata was found in the pkl file. Make sure you are currently using \
the same Nipype version from the generated pkl.""")
raise e
finally:
pkl_file.close()
raise e

if unpkl is None:
raise ValueError('Loading %s resulted in None.' % infile)
Expand Down Expand Up @@ -754,20 +752,17 @@ def read_stream(stream, logger=None, encoding=None):


def savepkl(filename, record, versioning=False):
if filename.endswith('pklz'):
pkl_file = gzip.open(filename, 'wb')
else:
pkl_file = open(filename, 'wb')

if versioning:
from nipype import __version__ as version
metadata = json.dumps({'version': version})
pklopen = gzip.open if filename.endswith('.pklz') else open
with SoftFileLock('%s.lock' % filename):
with pklopen(filename, 'wb') as pkl_file:
if versioning:
from nipype import __version__ as version
metadata = json.dumps({'version': version})

pkl_file.write(metadata.encode('utf-8'))
pkl_file.write('\n'.encode('utf-8'))
pkl_file.write(metadata.encode('utf-8'))
pkl_file.write('\n'.encode('utf-8'))

pickle.dump(record, pkl_file)
pkl_file.close()
pickle.dump(record, pkl_file)


rst_levels = ['=', '-', '~', '+']
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ python-dateutil>=2.2
scipy>=0.14
simplejson>=3.8.0
traits>=4.6
filelock>= 3.0.0