Gentoo Archives: gentoo-portage-dev

From: Arfrever Frehtes Taifersar Arahesis <arfrever.fta@×××××.com>
To: Gentoo Portage Development <gentoo-portage-dev@l.g.o>
Subject: Re: [gentoo-portage-dev] [PATCH v2] xattr: centralize the various shims in one place
Date: Tue, 22 Oct 2013 16:09:39
Message-Id: 201310221809.02426.Arfrever.FTA@gmail.com
In Reply to: [gentoo-portage-dev] [PATCH v2] xattr: centralize the various shims in one place by Mike Frysinger
1 2013-10-21 05:07 Mike Frysinger napisał(a):
2 > Rather than each module implementing its own shim around the various
3 > methods for accessing extended attributes, start a dedicated module
4 > that exports a consistent API.
5 > ---
6 > v2
7 > - passes unittests w/python 2.6 2.7 3.2 3.3
8
9 But portage.util._xattr._XattrSystemCommands does not work :) .
10
11 >>> import portage.util._xattr
12 >>> portage.util._xattr._XattrSystemCommands.list("/tmp")
13 ...
14
15 See below.
16
17 > bin/xattr-helper.py | 11 +-
18 > pym/portage/tests/util/test_xattr.py | 178 ++++++++++++++++++++++++++++++
19 > pym/portage/util/_xattr.py | 205 +++++++++++++++++++++++++++++++++++
20 > pym/portage/util/movefile.py | 100 ++++-------------
21 > 4 files changed, 407 insertions(+), 87 deletions(-)
22 > create mode 100644 pym/portage/tests/util/test_xattr.py
23 > create mode 100644 pym/portage/util/_xattr.py
24 >
25 > diff --git a/bin/xattr-helper.py b/bin/xattr-helper.py
26 > index 6d99521..69b83f7 100755
27 > --- a/bin/xattr-helper.py
28 > +++ b/bin/xattr-helper.py
29 > @@ -17,16 +17,7 @@ import re
30 > import sys
31 >
32 > from portage.util._argparse import ArgumentParser
33 > -
34 > -if hasattr(os, "getxattr"):
35 > -
36 > - class xattr(object):
37 > - get = os.getxattr
38 > - set = os.setxattr
39 > - list = os.listxattr
40 > -
41 > -else:
42 > - import xattr
43 > +from portage.util._xattr import xattr
44 >
45 >
46 > _UNQUOTE_RE = re.compile(br'\\[0-7]{3}')
47 > diff --git a/pym/portage/tests/util/test_xattr.py b/pym/portage/tests/util/test_xattr.py
48 > new file mode 100644
49 > index 0000000..e1f6ee8
50 > --- /dev/null
51 > +++ b/pym/portage/tests/util/test_xattr.py
52 > @@ -0,0 +1,178 @@
53 > +# Copyright 2010-2013 Gentoo Foundation
54 > +# Distributed under the terms of the GNU General Public License v2
55 > +
56 > +"""Tests for the portage.util._xattr module"""
57 > +
58 > +from __future__ import print_function
59 > +
60 > +try:
61 > + # Try python-3.3 module first.
62 > + from unittest import mock
63 > +except ImportError:
64 > + try:
65 > + # Try standalone module.
66 > + import mock
67 > + except ImportError:
68 > + mock = None
69 > +
70 > +import subprocess
71 > +
72 > +from portage.tests import TestCase
73 > +from portage.util._xattr import (xattr as _xattr, _XattrSystemCommands,
74 > + _XattrStub)
75 > +
76 > +
77 > +orig_popen = subprocess.Popen
78 > +def MockSubprocessPopen(stdin):
79 > + """Helper to mock (closely) a subprocess.Popen call
80 > +
81 > + The module has minor tweaks in behavior when it comes to encoding and
82 > + python versions, so use a real subprocess.Popen call to fake out the
83 > + runtime behavior. This way we don't have to also implement different
84 > + encodings as that gets ugly real fast.
85 > + """
86 > + proc = orig_popen(['cat'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
87 > + try:
88 > + proc.stdin.write(bytes(stdin))
89 > + except TypeError:
90 > + proc.stdin.write(bytes(stdin, 'ascii'))
91
92 It can fail with UnicodeEncodeError.
93 Use proc.stdin.write(portage._unicode_encode(stdin), portage._encodings['stdio']) instead of above 4 lines.
94
95 > + return proc
96 > +
97 > +
98 > +class SystemCommandsTest(TestCase):
99 > + """Test _XattrSystemCommands"""
100 > +
101 > + OUTPUT = '\n'.join([
102 > + '# file: /bin/ping',
103 > + 'security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=',
104 > + 'user.foo="asdf"',
105 > + '',
106 > + ])
107 > +
108 > + def _setUp(self):
109 > + if mock is None:
110 > + self.skipTest('need mock for testing')
111 > +
112 > + return _XattrSystemCommands
113 > +
114 > + def _testGetBasic(self):
115 > + """Verify the get() behavior"""
116 > + xattr = self._setUp()
117 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
118 > + # Verify basic behavior, and namespace arg works as expected.
119 > + xattr.get('/some/file', 'user.foo')
120 > + xattr.get('/some/file', 'foo', namespace='user')
121 > + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
122 > +
123 > + # Verify nofollow behavior.
124 > + call_mock.reset()
125 > + xattr.get('/some/file', 'user.foo', nofollow=True)
126 > + self.assertIn('-h', call_mock.call_args[0][0])
127 > +
128 > + def testGetParsing(self):
129 > + """Verify get() parses output sanely"""
130 > + xattr = self._setUp()
131 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
132 > + # Verify output parsing.
133 > + call_mock.return_value = MockSubprocessPopen('\n'.join([
134 > + '# file: /some/file',
135 > + 'user.foo="asdf"',
136 > + '',
137 > + ]))
138 > + call_mock.reset()
139 > + self.assertEqual(xattr.get('/some/file', 'user.foo'), b'"asdf"')
140 > +
141 > + def testGetAllBasic(self):
142 > + """Verify the get_all() behavior"""
143 > + xattr = self._setUp()
144 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
145 > + # Verify basic behavior.
146 > + xattr.get_all('/some/file')
147 > +
148 > + # Verify nofollow behavior.
149 > + call_mock.reset()
150 > + xattr.get_all('/some/file', nofollow=True)
151 > + self.assertIn('-h', call_mock.call_args[0][0])
152 > +
153 > + def testGetAllParsing(self):
154 > + """Verify get_all() parses output sanely"""
155 > + xattr = self._setUp()
156 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
157 > + # Verify output parsing.
158 > + call_mock.return_value = MockSubprocessPopen(self.OUTPUT)
159 > + exp = [
160 > + (b'security.capability', b'0sAQAAAgAgAAAAAAAAAAAAAAAAAAA='),
161 > + (b'user.foo', b'"asdf"'),
162 > + ]
163 > + self.assertEqual(exp, xattr.get_all('/some/file'))
164 > +
165 > + def testSetBasic(self):
166 > + """Verify the set() behavior"""
167 > + xattr = self._setUp()
168 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
169 > + # Verify basic behavior, and namespace arg works as expected.
170 > + xattr.set('/some/file', 'user.foo', 'bar')
171 > + xattr.set('/some/file', 'foo', 'bar', namespace='user')
172 > + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
173 > +
174 > + def testListBasic(self):
175 > + """Verify the list() behavior"""
176 > + xattr = self._setUp()
177 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
178 > + # Verify basic behavior.
179 > + xattr.list('/some/file')
180 > +
181 > + # Verify nofollow behavior.
182 > + call_mock.reset()
183 > + xattr.list('/some/file', nofollow=True)
184 > + self.assertIn('-h', call_mock.call_args[0][0])
185 > +
186 > + def testListParsing(self):
187 > + """Verify list() parses output sanely"""
188 > + xattr = self._setUp()
189 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
190 > + # Verify output parsing.
191 > + call_mock.return_value = MockSubprocessPopen(self.OUTPUT)
192 > + exp = [b'security.capability', b'user.foo']
193 > + self.assertEqual(exp, xattr.list('/some/file'))
194 > +
195 > + def testRemoveBasic(self):
196 > + """Verify the remove() behavior"""
197 > + xattr = self._setUp()
198 > + with mock.patch.object(subprocess, 'Popen') as call_mock:
199 > + # Verify basic behavior, and namespace arg works as expected.
200 > + xattr.remove('/some/file', 'user.foo')
201 > + xattr.remove('/some/file', 'foo', namespace='user')
202 > + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
203 > +
204 > + # Verify nofollow behavior.
205 > + call_mock.reset()
206 > + xattr.remove('/some/file', 'user.foo', nofollow=True)
207 > + self.assertIn('-h', call_mock.call_args[0][0])
208 > +
209 > +
210 > +class StubTest(TestCase):
211 > + """Test _XattrStub"""
212 > +
213 > + def testBasic(self):
214 > + """Verify the stub is stubby"""
215 > + # Would be nice to verify raised errno is OperationNotSupported.
216 > + self.assertRaises(OSError, _XattrStub.get, '/', '')
217 > + self.assertRaises(OSError, _XattrStub.set, '/', '', '')
218 > + self.assertRaises(OSError, _XattrStub.get_all, '/')
219 > + self.assertRaises(OSError, _XattrStub.remove, '/', '')
220 > + self.assertRaises(OSError, _XattrStub.list, '/')
221 > +
222 > +
223 > +class StandardTest(TestCase):
224 > + """Test basic xattr API"""
225 > +
226 > + MODULES = (_xattr, _XattrSystemCommands, _XattrStub)
227 > + FUNCS = ('get', 'get_all', 'set', 'remove', 'list')
228 > +
229 > + def testApi(self):
230 > + """Make sure the exported API matches"""
231 > + for mod in self.MODULES:
232 > + for f in self.FUNCS:
233 > + self.assertTrue(hasattr(mod, f),
234 > + '%s func missing in %s' % (f, mod))
235 > diff --git a/pym/portage/util/_xattr.py b/pym/portage/util/_xattr.py
236 > new file mode 100644
237 > index 0000000..db3cf6e
238 > --- /dev/null
239 > +++ b/pym/portage/util/_xattr.py
240 > @@ -0,0 +1,205 @@
241 > +# Copyright 2010-2013 Gentoo Foundation
242 > +# Distributed under the terms of the GNU General Public License v2
243 > +
244 > +"""Portability shim for xattr support
245 > +
246 > +Exported API is the xattr object with get/get_all/set/remove/list operations.
247 > +
248 > +See the standard xattr module for more documentation.
249 > +"""
250 > +
251 > +from __future__ import print_function
252 > +
253 > +import contextlib
254 > +import os
255 > +import subprocess
256 > +
257 > +from portage.exception import OperationNotSupported
258 > +
259 > +
260 > +class _XattrGetAll(object):
261 > + """Implement get_all() using list()/get() if there is no easy bulk method"""
262 > +
263 > + @classmethod
264 > + def get_all(cls, item, nofollow=False, namespace=None):
265 > + return [(name, cls.get(item, name, nofollow=nofollow, namespace=namespace))
266 > + for name in cls.list(item, nofollow=nofollow, namespace=namespace)]
267 > +
268 > +
269 > +class _XattrSystemCommands(_XattrGetAll):
270 > + """Implement things with getfattr/setfattr"""
271 > +
272 > + @staticmethod
273 > + def _parse_output(output):
274 > + for line in output.readlines():
275 > + if line.startswith(b'#'):
276 > + continue
277 > + line = line.rstrip()
278 > + if not line:
279 > + continue
280 > + # The lines will have the format:
281 > + # user.hex=0x12345
282 > + # user.base64=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
283 > + # user.string="value0"
284 > + # But since we don't do interpretation on the value (we just
285 > + # save & restore it), don't bother with decoding here.
286 > + yield line.split(b'=', 1)
287 > +
288 > + @staticmethod
289 > + def _call(*args, **kwargs):
290 > + proc = subprocess.Popen(*args, **kwargs)
291 > + proc.stdin.close()
292
293 AttributeError: 'NoneType' object has no attribute 'close'
294
295 If closing stdin is necessary, then I suggest:
296
297 if proc.stdin is not None:
298 proc.stdin.close()
299
300 > + proc.wait()
301 > + return proc
302 > +
303 > + @classmethod
304 > + def get(cls, item, name, nofollow=False, namespace=None):
305 > + if namespace:
306 > + name = '%s.%s' % (namespace, name)
307 > + cmd = ['getfattr', '--absolute-names', '-n', name, item]
308 > + if nofollow:
309 > + cmd += ['-h']
310 > + proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
311 > +
312 > + value = None
313 > + for _, value in cls._parse_output(proc.stdout):
314 > + break
315 > +
316 > + proc.stdout.close()
317 > + return value
318 > +
319 > + @classmethod
320 > + def set(cls, item, name, value, _flags=0, namespace=None):
321 > + if namespace:
322 > + name = '%s.%s' % (namespace, name)
323 > + cmd = ['setfattr', '-n', name, '-v', value, item]
324 > + cls._call(cmd)
325 > +
326 > + @classmethod
327 > + def remove(cls, item, name, nofollow=False, namespace=None):
328 > + if namespace:
329 > + name = '%s.%s' % (namespace, name)
330 > + cmd = ['setfattr', '-x', name, item]
331 > + if nofollow:
332 > + cmd += ['-h']
333 > + cls._call(cmd)
334 > +
335 > + @classmethod
336 > + def list(cls, item, nofollow=False, namespace=None, _names_only=True):
337 > + cmd = ['getfattr', '-d', '--absolute-names', item]
338 > + if nofollow:
339 > + cmd += ['-h']
340 > + cmd += ['-m', ('^%s[.]' % namespace) if namespace else '']
341 > + proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
342 > +
343 > + ret = []
344 > + if namespace:
345 > + namespace = '%s.' % namespace
346 > + for name, value in cls._parse_output(proc.stdout):
347 > + if namespace:
348 > + if name.startswith(namespace):
349 > + name = name[len(namespace):]
350 > + else:
351 > + continue
352 > + if _names_only:
353 > + ret.append(name)
354 > + else:
355 > + ret.append((name, value))
356 > +
357 > + proc.stdout.close()
358 > + return ret
359 > +
360 > + @classmethod
361 > + def get_all(cls, item, nofollow=False, namespace=None):
362 > + return cls.list(item, nofollow=nofollow, namespace=namespace,
363 > + _names_only=False)
364 > +
365 > +
366 > +# pylint: disable=W0613
367 > +class _XattrStub(_XattrGetAll):
368 > + """Fake object since system doesn't support xattrs"""
369 > +
370 > + @staticmethod
371 > + def _raise():
372 > + e = OSError('stub')
373 > + e.errno = OperationNotSupported.errno
374 > + raise e
375 > +
376 > + @classmethod
377 > + def get(cls, item, name, nofollow=False, namespace=None):
378 > + cls._raise()
379 > +
380 > + @classmethod
381 > + def set(cls, item, name, value, flags=0, namespace=None):
382 > + cls._raise()
383 > +
384 > + @classmethod
385 > + def remove(cls, item, name, nofollow=False, namespace=None):
386 > + cls._raise()
387 > +
388 > + @classmethod
389 > + def list(cls, item, nofollow=False, namespace=None):
390 > + cls._raise()
391 > +
392 > +
393 > +if hasattr(os, 'getxattr'):
394 > + # Easy as pie -- active python supports it.
395 > + class xattr(_XattrGetAll):
396 > + """Python >=3.3 and GNU/Linux"""
397 > + get = os.getxattr
398 > + set = os.setxattr
399 > + remove = os.removexattr
400 > + list = os.listxattr
401 > +
402 > +else:
403 > + try:
404 > + # Maybe we have the xattr module.
405 > + import xattr
406 > +
407 > + except ImportError:
408 > + try:
409 > + # Maybe we have the attr package.
410 > + with open(os.devnull, 'wb') as f:
411 > + subprocess.call(['getfattr', '--version'], stdout=f)
412 > + subprocess.call(['setfattr', '--version'], stdout=f)
413 > + xattr = _XattrSystemCommands
414 > +
415 > + except OSError:
416 > + # Stub it out completely.
417 > + xattr = _XattrStub
418 > +
419 > +
420 > +@××××××××××.contextmanager
421 > +def preserve_xattrs(path, nofollow=False, namespace=None):
422 > + """Context manager to save/restore extended attributes on |path|
423 > +
424 > + If you want to rewrite a file (possibly replacing it with a new one), but
425 > + want to preserve the extended attributes, this will do the trick.
426 > +
427 > + # First read all the extended attributes.
428 > + with save_xattrs('/some/file'):
429 > + ... rewrite the file ...
430 > + # Now the extended attributes are restored as needed.
431 > + """
432 > + kwargs = {'nofollow': nofollow,}
433 > + if namespace:
434 > + # Compiled xattr python module does not like it when namespace=None.
435 > + kwargs['namespace'] = namespace
436 > +
437 > + old_attrs = dict(xattr.get_all(path, **kwargs))
438 > + try:
439 > + yield
440 > + finally:
441 > + new_attrs = dict(xattr.get_all(path, **kwargs))
442 > + for name, value in new_attrs.iteritems():
443 > + if name not in old_attrs:
444 > + # Clear out new ones.
445 > + xattr.remove(path, name, **kwargs)
446 > + elif new_attrs[name] != old_attrs[name]:
447 > + # Update changed ones.
448 > + xattr.set(path, name, value, **kwargs)
449 > +
450 > + for name, value in old_attrs.iteritems():
451 > + if name not in new_attrs:
452 > + # Re-add missing ones.
453 > + xattr.set(path, name, value, **kwargs)
454 > diff --git a/pym/portage/util/movefile.py b/pym/portage/util/movefile.py
455 > index 452e77f..20859fe 100644
456 > --- a/pym/portage/util/movefile.py
457 > +++ b/pym/portage/util/movefile.py
458 > @@ -11,7 +11,6 @@ import os as _os
459 > import shutil as _shutil
460 > import stat
461 > import sys
462 > -import subprocess
463 > import textwrap
464 >
465 > import portage
466 > @@ -23,6 +22,7 @@ from portage.exception import OperationNotSupported
467 > from portage.localization import _
468 > from portage.process import spawn
469 > from portage.util import writemsg
470 > +from portage.util._xattr import xattr
471 >
472 > def _apply_stat(src_stat, dest):
473 > _os.chown(dest, src_stat.st_uid, src_stat.st_gid)
474 > @@ -68,86 +68,32 @@ class _xattr_excluder(object):
475 >
476 > return False
477 >
478 > -if hasattr(_os, "getxattr"):
479 > - # Python >=3.3 and GNU/Linux
480 > - def _copyxattr(src, dest, exclude=None):
481 > -
482 > - try:
483 > - attrs = _os.listxattr(src)
484 > - except OSError as e:
485 > - if e.errno != OperationNotSupported.errno:
486 > - raise
487 > - attrs = ()
488 > - if attrs:
489 > - if exclude is not None and isinstance(attrs[0], bytes):
490 > - exclude = exclude.encode(_encodings['fs'])
491 > - exclude = _get_xattr_excluder(exclude)
492 > -
493 > - for attr in attrs:
494 > - if exclude(attr):
495 > - continue
496 > - try:
497 > - _os.setxattr(dest, attr, _os.getxattr(src, attr))
498 > - raise_exception = False
499 > - except OSError:
500 > - raise_exception = True
501 > - if raise_exception:
502 > - raise OperationNotSupported(_("Filesystem containing file '%s' "
503 > - "does not support extended attribute '%s'") %
504 > - (_unicode_decode(dest), _unicode_decode(attr)))
505 > -else:
506 > +def _copyxattr(src, dest, exclude=None):
507 > + """Copy the extended attributes from |src| to |dest|"""
508 > try:
509 > - import xattr
510 > - except ImportError:
511 > - xattr = None
512 > - if xattr is not None:
513 > - def _copyxattr(src, dest, exclude=None):
514 > -
515 > - try:
516 > - attrs = xattr.list(src)
517 > - except IOError as e:
518 > - if e.errno != OperationNotSupported.errno:
519 > - raise
520 > - attrs = ()
521 > + attrs = xattr.list(src)
522 > + except (OSError, IOError) as e:
523 > + if e.errno != OperationNotSupported.errno:
524 > + raise
525 > + attrs = ()
526 >
527 > - if attrs:
528 > - if exclude is not None and isinstance(attrs[0], bytes):
529 > - exclude = exclude.encode(_encodings['fs'])
530 > - exclude = _get_xattr_excluder(exclude)
531 > + if attrs:
532 > + if exclude is not None and isinstance(attrs[0], bytes):
533 > + exclude = exclude.encode(_encodings['fs'])
534 > + exclude = _get_xattr_excluder(exclude)
535 >
536 > - for attr in attrs:
537 > - if exclude(attr):
538 > - continue
539 > - try:
540 > - xattr.set(dest, attr, xattr.get(src, attr))
541 > - raise_exception = False
542 > - except IOError:
543 > - raise_exception = True
544 > - if raise_exception:
545 > - raise OperationNotSupported(_("Filesystem containing file '%s' "
546 > - "does not support extended attribute '%s'") %
547 > - (_unicode_decode(dest), _unicode_decode(attr)))
548 > - else:
549 > + for attr in attrs:
550 > + if exclude(attr):
551 > + continue
552 > try:
553 > - with open(_os.devnull, 'wb') as f:
554 > - subprocess.call(["getfattr", "--version"], stdout=f)
555 > - subprocess.call(["setfattr", "--version"], stdout=f)
556 > - except OSError:
557 > - def _copyxattr(src, dest, exclude=None):
558 > - # TODO: implement exclude
559 > - getfattr_process = subprocess.Popen(["getfattr", "-d", "--absolute-names", src], stdout=subprocess.PIPE)
560 > - getfattr_process.wait()
561 > - extended_attributes = getfattr_process.stdout.readlines()
562 > - getfattr_process.stdout.close()
563 > - if extended_attributes:
564 > - extended_attributes[0] = b"# file: " + _unicode_encode(dest) + b"\n"
565 > - setfattr_process = subprocess.Popen(["setfattr", "--restore=-"], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
566 > - setfattr_process.communicate(input=b"".join(extended_attributes))
567 > - if setfattr_process.returncode != 0:
568 > - raise OperationNotSupported("Filesystem containing file '%s' does not support extended attributes" % dest)
569 > - else:
570 > - def _copyxattr(src, dest, exclude=None):
571 > - pass
572 > + xattr.set(dest, attr, xattr.get(src, attr))
573 > + raise_exception = False
574 > + except (OSError, IOError):
575 > + raise_exception = True
576 > + if raise_exception:
577 > + raise OperationNotSupported(_("Filesystem containing file '%s' "
578 > + "does not support extended attribute '%s'") %
579 > + (_unicode_decode(dest), _unicode_decode(attr)))
580 >
581 > def movefile(src, dest, newmtime=None, sstat=None, mysettings=None,
582 > hardlink_candidates=None, encoding=_encodings['fs']):
583 >
584
585 --
586 Arfrever Frehtes Taifersar Arahesis

Attachments

File name MIME type
signature.asc application/pgp-signature