diff options
author | ZhiQiang Fan <aji.zqfan@gmail.com> | 2015-11-28 02:17:05 +0800 |
---|---|---|
committer | ZhiQiang Fan <aji.zqfan@gmail.com> | 2015-12-12 10:58:55 +0800 |
commit | e46a46ba90741987f1147afc56876e3d0d27e8a2 (patch) | |
tree | a81fa40db81ccee482ef46b1d0f4ae72452f8244 | |
parent | d423b3b74cfae284c4b46d3bad5232e3840e05f3 (diff) | |
download | oslo-utils-e46a46ba90741987f1147afc56876e3d0d27e8a2.tar.gz |
re-implement thread safe fnmatch
fnmatch is not thread safe for versions <= 2.7.9. We have used
it in some projects without any lock for concurrency scenario. This patch
re-implements a thread safe fnmatch which is very similar to standard
nmatch.
Change-Id: I610ffcdf58d1590b8e0c0faefd8832563b33ab30
ref: https://bugs.python.org/issue23191
-rw-r--r-- | oslo_utils/fnmatch.py | 76 | ||||
-rw-r--r-- | oslo_utils/tests/test_fnmatch.py | 30 |
2 files changed, 106 insertions, 0 deletions
diff --git a/oslo_utils/fnmatch.py b/oslo_utils/fnmatch.py new file mode 100644 index 0000000..8dd7aa0 --- /dev/null +++ b/oslo_utils/fnmatch.py @@ -0,0 +1,76 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Thread safe fnmatch re-implementation. + +Standard library fnmatch in Python versions <= 2.7.9 has thread safe +issue, this module is created for such case. see: +https://bugs.python.org/issue23191 +""" + +from __future__ import absolute_import + +import fnmatch as standard_fnmatch +import os +import posixpath +import re +import sys + + +if sys.version_info > (2, 7, 9): + fnmatch = standard_fnmatch.fnmatch + fnmatchcase = standard_fnmatch.fnmatchcase + filter = standard_fnmatch.filter + translate = standard_fnmatch.translate +else: + _MATCH_CACHE = {} + _MATCH_CACHE_MAX = 100 + + translate = standard_fnmatch.translate + + def _get_cached_pattern(pattern): + cached_pattern = _MATCH_CACHE.get(pattern) + if cached_pattern is None: + translated_pattern = translate(pattern) + cached_pattern = re.compile(translated_pattern) + if len(_MATCH_CACHE) >= _MATCH_CACHE_MAX: + _MATCH_CACHE.clear() + _MATCH_CACHE[pattern] = cached_pattern + return cached_pattern + + def fnmatchcase(filename, pattern): + cached_pattern = _get_cached_pattern(pattern) + return cached_pattern.match(filename) is not None + + def fnmatch(filename, pattern): + filename = os.path.normcase(filename) + pattern = os.path.normcase(pattern) + return fnmatchcase(filename, pattern) + + def filter(filenames, pattern): + filtered_filenames = [] + + pattern = os.path.normcase(pattern) + cached_pattern = _get_cached_pattern(pattern) + + if os.path is posixpath: + # normcase on posix is NOP. Optimize it away from the loop. + for filename in filenames: + if cached_pattern.match(filename): + filtered_filenames.append(filename) + else: + for filename in filenames: + filename = os.path.normcase(filename) + if cached_pattern.match(filename): + filtered_filenames.append(filename) + + return filtered_filenames diff --git a/oslo_utils/tests/test_fnmatch.py b/oslo_utils/tests/test_fnmatch.py new file mode 100644 index 0000000..537d72a --- /dev/null +++ b/oslo_utils/tests/test_fnmatch.py @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +import mock +from oslotest import base + +from oslo_utils import fnmatch + + +class TestFnmatch(base.BaseTestCase): + + @mock.patch.object(sys, 'version_info', new=(2, 7, 0)) + def test_fnmatch(self): + self.assertFalse(fnmatch.fnmatch("tesX", "Test")) + self.assertTrue(fnmatch.fnmatch("test", "test")) + self.assertFalse(fnmatch.fnmatchcase("test", "Test")) + self.assertTrue(fnmatch.fnmatchcase("test", "test")) + self.assertTrue(fnmatch.fnmatch("testX", "test*")) + self.assertEqual(["test"], fnmatch.filter(["test", "testX"], "test")) |