From 13ea02843bd5698cd598ace187e9033ff3406ea5 Mon Sep 17 00:00:00 2001 From: Iustin Pop Date: Sun, 29 Nov 2020 21:06:58 +0100 Subject: [PATCH] New upstream version 0.7.2 --- MANIFEST.in | 9 +- Makefile | 47 +++- NEWS | 38 +++ PKG-INFO | 16 +- README.md | 68 +++++ README.rst | 41 --- doc/conf.py | 13 +- doc/index.rst | 5 +- doc/news.rst | 204 --------------- pyxattr.egg-info/PKG-INFO | 16 +- pyxattr.egg-info/SOURCES.txt | 7 +- setup.py | 21 +- test/test_xattr.py | 463 --------------------------------- {test => tests}/__init__.py | 0 tests/test_xattr.py | 489 +++++++++++++++++++++++++++++++++++ xattr.c | 97 +++---- 16 files changed, 727 insertions(+), 807 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 doc/news.rst delete mode 100644 test/test_xattr.py rename {test => tests}/__init__.py (100%) create mode 100644 tests/test_xattr.py diff --git a/MANIFEST.in b/MANIFEST.in index 6c709af..4960670 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ include COPYING include NEWS -include README.rst +include README.md include Makefile include doc/conf.py -include doc/*.rst +include doc/index.rst +include doc/module.rst include setup.cfg -include test/test_xattr.py -include test/__init__.py +include tests/test_xattr.py +include tests/__init__.py include xattr.c diff --git a/Makefile b/Makefile index 141685f..f57b8bf 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,23 @@ +PYTHON = python3 SPHINXOPTS = -W -SPHINXBUILD = sphinx-build +SPHINXBUILD = $(PYTHON) -m sphinx DOCDIR = doc DOCHTML = $(DOCDIR)/html DOCTREES = $(DOCDIR)/doctrees ALLSPHINXOPTS = -d $(DOCTREES) $(SPHINXOPTS) $(DOCDIR) +VERSION = 0.7.2 +FULLVER = pyxattr-$(VERSION) +DISTFILE = $(FULLVER).tar.gz MODNAME = xattr.so -RSTFILES = doc/index.rst doc/module.rst NEWS README.rst doc/conf.py -PYVERS = 2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 +RSTFILES = doc/index.rst doc/module.rst doc/news.rst doc/readme.md doc/conf.py +PYVERS = 3.4 3.5 3.6 3.7 3.8 3.9 REPS = 5 all: doc test $(MODNAME): xattr.c - ./setup.py build_ext --inplace + $(PYTHON) ./setup.py build_ext --inplace $(DOCHTML)/index.html: $(MODNAME) $(RSTFILES) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DOCHTML) @@ -21,23 +25,43 @@ $(DOCHTML)/index.html: $(MODNAME) $(RSTFILES) doc: $(DOCHTML)/index.html +doc/readme.md: README.md + ln -s ../README.md doc/readme.md + +doc/news.rst: NEWS + ln -s ../NEWS doc/news.rst + dist: - fakeroot ./setup.py sdist + fakeroot $(PYTHON) ./setup.py sdist + +distcheck: dist + set -e; \ + TDIR=$$(mktemp -d) && \ + trap "rm -rf $$TDIR" EXIT; \ + tar xzf dist/$(DISTFILE) -C $$TDIR && \ + (cd $$TDIR/$(FULLVER) && make doc && make test && make dist) && \ + echo "All good, you can upload $(DISTFILE)!" test: @for ver in $(PYVERS); do \ for flavour in "" "-dbg"; do \ if type python$$ver$$flavour >/dev/null; then \ echo Testing with python$$ver$$flavour; \ - python$$ver$$flavour ./setup.py test -q; \ + python$$ver$$flavour setup.py build_ext -i; \ + python$$ver$$flavour -m pytest tests; \ fi; \ done; \ done; - @if type pypy >/dev/null; then \ - echo Testing with pypy; \ - pypy ./setup.py test -q; \ + @if type pypy3 >/dev/null; then \ + echo Testing with pypy3; \ + pypy3 setup.py build_ext -i; \ + pypy3 -m pytest tests; \ fi +fast-test: + python3 setup.py build_ext -i + python3 -m pytest tests -v + benchmark: $(MODNAME) @set -e; \ TESTFILE=`mktemp`;\ @@ -60,13 +84,14 @@ benchmark: $(MODNAME) coverage: $(MAKE) clean $(MAKE) test CFLAGS="-coverage" - lcov --capture --directory . --output-file coverage.info + lcov --capture --directory . --no-external --output-file coverage.info genhtml coverage.info --output-directory out clean: rm -rf $(DOCHTML) $(DOCTREES) + rm -f doc/readme.md doc/news.rst rm -f $(MODNAME) rm -f *.so rm -rf build -.PHONY: doc test clean dist coverage +.PHONY: doc test fast-test clean dist distcheck coverage diff --git a/NEWS b/NEWS index 6bee508..87caf52 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,44 @@ News ==== +Version 0.7.2 +------------- + +*Sun, 29 Nov 2020* + +Minor release: + +* Expand testing by adding better mixed-access checks (e.g. set via + symlink and read on file) and by not leaking resources during tests. +* Enable testing with Python 3.9 and confirm compatibility with it. +* Fix documentation building with Sphinx 3.0+. + +Version 0.7.1 +------------- + +*released Tue, 26 Nov 2019* + +Typo fix release in the bug tracker link :/ + +Version 0.7.0 +------------- + +*released Tue, 26 Nov 2019* + +Major change: drop compatibility with Python 2, which allows significant +code cleanups. + +Other changes: + +* Switch internal implementation of argument parsing to a built-in one + (`PyUnicode_FSConverter`), which brings automatic support for + path-like objects in Python 3.6+ (#20), and also a more uniform + handling of Unicode path arguments with respect to other Python code. +* Fix missing error check in list operations in `get_all` (#17). +* Switch test library to pytest; not that a reasonable recent version is + needed. Additionally, expand test coverage, although not directly + visible in actual coverage reports… + Version 0.6.1 ------------- diff --git a/PKG-INFO b/PKG-INFO index 525c3b3..efe5a0c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,13 +1,25 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: pyxattr -Version: 0.6.1 +Version: 0.7.2 Summary: Filesystem extended attributes for python Home-page: http://pyxattr.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL Download-URL: http://pyxattr.k1024.org/downloads/ +Project-URL: Bug Tracker, https://github.com/iustin/pyxattr/issues Description: This is a C extension module for Python which implements extended attributes manipulation. It is a wrapper on top of the attr C library - see attr(5). Platform: Linux +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX :: Linux +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Filesystems +Requires-Python: >=3.4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..693be67 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# pyxattr + +This is the pyxattr module, a Python extension module which gives access +to the extended attributes for filesystem objects available in some +operating systems. + +[![Travis](https://img.shields.io/travis/iustin/pyxattr)](https://travis-ci.org/iustin/pyxattr) +[![Codecov](https://img.shields.io/codecov/c/github/iustin/pyxattr)](https://codecov.io/gh/iustin/pyxattr) +[![Read the Docs](https://img.shields.io/readthedocs/pyxattr)](http://pyxattr.readthedocs.io/en/latest/?badge=latest) +[![GitHub issues](https://img.shields.io/github/issues/iustin/pyxattr)](https://github.com/iustin/pyxattr/issues) +![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/iustin/pyxattr) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/iustin/pyxattr)](https://github.com/iustin/pyxattr/releases) +[![PyPI](https://img.shields.io/pypi/v/pyxattr)](https://pypi.org/project/pyxattr/) +![Debian package](https://img.shields.io/debian/v/python-pyxattr) +![Ubuntu package](https://img.shields.io/ubuntu/v/python-pyxattr) +![GitHub Release Date](https://img.shields.io/github/release-date/iustin/pyxattr) +![GitHub commits since latest release](https://img.shields.io/github/commits-since/iustin/pyxattr/latest) +![GitHub last commit](https://img.shields.io/github/last-commit/iustin/pyxattr) + +Downloads: go to . The source +repository is either at or at +. + +## Requirements + +The current supported Python versions are 3.4+ (tested up to 3.9). + +The library has been written and tested on Linux, kernel v2.4 or +later, with XFS and ext2/ext3/ext3 file systems. If any other platform +implements the same behaviour, pyxattr could be used. + +You need to have the setuptools tool installed in order to build and +install the module, and for building the documentation you need to +have Sphinx installed. + +Alternatively, you can install directly from pip: + + $ pip install pyxattr + +Or from your distribution, e.g. in Debian: + + $ sudo apt install python3-pyxattr + +## Basic example + + >>> import xattr + >>> xattr.listxattr("file.txt") + ['user.mime_type'] + >>> xattr.getxattr("file.txt", "user.mime_type") + 'text/plain' + >>> xattr.setxattr("file.txt", "user.comment", "Simple text file") + >>> xattr.listxattr("file.txt") + ['user.mime_type', 'user.comment'] + >>> xattr.removexattr ("file.txt", "user.comment") + +## License + +pyxattr is Copyright 2002-2008, 2012-2015 Iustin Pop. + +pyxattr is free software; you can redistribute it and/or modify it under the +terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) any +later version. See the COPYING file for the full license terms. + +Note that previous versions had different licenses: version 0.3 was licensed +under LGPL version 3 (which, I realized later, is not compatible with GPLv2, +hence the change to LGPL 2.1), and even older versions were licensed under GPL +v2 or later. diff --git a/README.rst b/README.rst deleted file mode 100644 index 033d597..0000000 --- a/README.rst +++ /dev/null @@ -1,41 +0,0 @@ -pyxattr -======= - -This is the pyxattr module, a Python extension module which gives access -to the extended attributes for filesystem objects available in some -operating systems. - -Downloads: go to https://pyxattr.k1024.org/downloads/. Latest -version is 0.6.1. The source repository is either at -http://git.k1024.org/pyxattr.git or at -https://github.com/iustin/pyxattr. - -Requirements ------------- - -pyxattr has been written and tested on Linux, kernel v2.4 or later, with -XFS filesystems; ext2/ext3 should work also. If any other platform -implements the same behavior, pyxattr could be used. - -You need to have the setuptools tool installed in order to build and -install the module. - -License -------- - -pyxattr is Copyright 2002-2008, 2012-2015 Iustin Pop. - -pyxattr is free software; you can redistribute it and/or modify it under the -terms of the GNU Lesser General Public License as published by the Free -Software Foundation; either version 2.1 of the License, or (at your option) any -later version. See the COPYING file for the full license terms. - -Note that previous versions had different licenses: version 0.3 was licensed -under LGPL version 3 (which, I realized later, is not compatible with GPLv2, -hence the change to LGPL 2.1), and even older versions were licensed under GPL -v2 or later. - -.. Local Variables: -.. mode: rst -.. fill-column: 72 -.. End: diff --git a/doc/conf.py b/doc/conf.py index a2ac56a..c48b6d7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,13 +25,13 @@ sys.path.insert(0, os.path.abspath('../')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'recommonmark'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ['.rst', '.md'] # The encoding of source files. #source_encoding = 'utf-8-sig' @@ -48,9 +48,9 @@ copyright = u'2002, 2003, 2006, 2008, 2012, 2013, 2014, 2015, Iustin Pop' # built documents. # # The short X.Y version. -version = '0.6.1' +version = '0.7.2' # The full version, including alpha/beta/rc tags. -release = '0.6.1' +release = '0.7.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -90,6 +90,11 @@ pygments_style = 'sphinx' keep_warnings = True +# Note: this is still needed in Sphinx 1.8 with recommonmark 0.4.0 +# (https://github.com/readthedocs/recommonmark/issues/119): +source_parsers = { + '.md': 'recommonmark.parser.CommonMarkParser', +} # -- Options for HTML output --------------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst index 6032182..a918c2e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,8 +2,8 @@ Welcome to pyxattr's documentation! ====================================== -.. include:: ../README.rst - :start-line: 2 +See the :doc:`README ` for start, or the detailed :doc:`module +` information. Contents -------- @@ -11,6 +11,7 @@ Contents .. toctree:: :maxdepth: 2 + readme.md module.rst news.rst diff --git a/doc/news.rst b/doc/news.rst deleted file mode 100644 index 6bee508..0000000 --- a/doc/news.rst +++ /dev/null @@ -1,204 +0,0 @@ -News -==== - -Version 0.6.1 -------------- - -*released Tue, 24 Jul 2018* - -Minor bugfix, performance and compatibility release. - -* Minor compatibility fix: on Linux, drop the use of the `attr` library, - and instead switch to the glibc header `sys/xattr.h`, which is - provided for a really long time (since glibc 2.3). The formerly used - header `attr/xattr.h` has been removed from the `attr` library in - version 2.4.48. Fix provided by Lars Wendler, many thanks! -* Release the GIL when performing I/O. Patch proposed by xwhuang, many - thanks. I tested this a long while back it seemed to impact - performance on local filesystems, but upon further inspection, the - downsides are minor (between 0 and 5%, in many cases negligible). For - remote or slow filesystems, this should allow much increased - parallelism. -* Fix symlink set operation on MacOS X; bugfix provided by adamlin, much - appreciated! This also uncovered testing problems related to symlinks, - which are now fixed (the bug would be caught by the updated tests). - -Version 0.6.0 -------------- - -*released Mon, 23 Jan 2017* - -Bugfix and feature release (hence the version bump). - -The main change is to the implementation of how attributes are listed -and read. This was done due to existing race issues when attributes are -modified while being read (github issue #12), but basically all various -internal paths that dealt with retrieving an attribute value or listing -attributes were unified in a single helper function that does handle -such concurrent modifications. As a side effect, the size of the buffers -used for such reads have changed, which (depending on attribute value) -might change the trade-off between number of syscalls done and memory -usage. - -As feature release, OSX support was contributed by Adam Knight -, thanks a lot! I don't have access to OSX so the testing -for it is done via Travis builds; please report any issues. - -Version 0.5.6 -------------- - -*released Sat, 09 Apr 2016* - -Small bugfix release: - -* Fixes some sign-compare warnings -* Fixes potential name truncation in merge_ns() -* Fixes building on systems which don't have ENODATA - -Tested with Python 2.7.11, Python 3.5.1 and PyPy 5.0.1. - -Version 0.5.5 -------------- - -*released Fri, 01 May 2015* - -Bugfix release: - -* fixes some more memory leaks when handling out-of-memory in get_all() - function -* improve error reporting when an attribute disappears after we asked - for its length but before we managed to read it -* fix int/size_t issues found by RedHat/Fedora, - https://bugzilla.redhat.com/show_bug.cgi?id=1127310; the fix is - different than their fix, but it should accomplish the same thing -* convert all code to only do explicit casts after checking boundaries, - making the code `-Wconversion`-clean (although that warning is not - enabled by default) - -Version 0.5.4 -------------- - -*released Thu, 30 Apr 2015* - -Fix memory leaks on some of the error-handling paths of the `get()` -function. - -Version 0.5.3 -------------- - -*released Fri, 23 May 2014* - -Small optimisations release: - -* ari edelkind contributed a speed-up optimisation for handling of files - without xattrs (which is, in general, the expected case) -* Jonas Borgström contributed a behaviour change to the handling of file - names: under Python 3 and up, unicode paths are encoded/decoded using - the 'surogatee' handler, instead of the 'strict' handler; while this - can hide encoding errors, it mirrors what Python libraries do - (e.g. see os.fsencode/fsdecode) -* Sean Patrick Santos contributed improvements to the test suite so that - it can be used even on files systems which have built-in attributes - (e.g. when using SELinux, or NFSv4); to enable this, define the - attributes in the TEST_IGNORE_XATTRS environment variable - -Version 0.5.2 -------------- - -*released Thu, 03 Jan 2013* - -Bug-fix release. Thanks to Michał Górny, it looked like the library had -problem running under pypy, but actually there was a bug in the -PyArg_ParseTuple use of et# (signed vs. unsigned, and lack of compiler -warnings). This was fixed, and now the test suite passed with many -CPython versions and PyPy (version 1.9). - -Version 0.5.1 -------------- - -*released Wed, 16 May 2012* - -Bug-fix release. Thanks to Dave Malcolm and his cpychecker tool, a -number of significant bugs (refcount leaks and potential NULL-pointer -dereferences) have been fixed. - -Furthermore, compatibility with Python 3 has been improved; this however -required changing the meaning of the ``namespace`` argument to the -functions: if passed, None is no longer a valid value; pass an empty -string if (due to the structure of your program) you have to pass this -argument but want to specify no namespace. - -Also, the project home page has changed from SourceForge to GitHub, and -the documentation has been converted from epydoc-based to sphinx. - - -Version 0.5 ------------ - -*released Sun, 27 Dec 2009* - -Implemented support for Python 3. This required a significant change to -the C module, hence the new version number. - -Version 0.4 ------------ - -*released Mon, 30 Jun 2008* - -API -~~~ - -The old functions ({get,set,list,remove}xattr) are deprecated and replaced with -a new API that is namespace-aware and hopefully will allow other OSes (e.g. -FreeBSD) to be supported more naturally. - -Both the old and the new API are supported in the 0.4 versions, however users -are encouraged to migrate to the new API. - -New features -~~~~~~~~~~~~ - -A new bulk get function called get_all() has been added that should be somewhat -faster in case of querying files which have many attributes. - -License -~~~~~~~ - -Since LGPLv3 is not compatible with GPLv2 (which unfortunately I didn't realize -before), the license was changed to LGPLv2.1 or later. - -Internals -~~~~~~~~~ - -Unittest coverage was improved. - -Version 0.3 ------------ - -*released Sun, 09 Mar 2008* - -* changed licence from GPL to LGPL (3 or later) -* changed listxattr return type from tuple to a list -* developer-related: added unittests - -Version 0.2.2 -------------- - -*released Sun, 01 Jul 2007* - -* fixed listing symlink xattrs - -Version 0.2.1 -------------- - -*released Sat, 11 Feb 2006* - -* fixed a bug when reading symlink EAs (you weren't able to - do it, actually) -* fixed a possible memory leak when the actual read of the EA - failed but the call to get the length of the EA didn't - -.. Local Variables: -.. mode: rst -.. fill-column: 72 -.. End: diff --git a/pyxattr.egg-info/PKG-INFO b/pyxattr.egg-info/PKG-INFO index 525c3b3..efe5a0c 100644 --- a/pyxattr.egg-info/PKG-INFO +++ b/pyxattr.egg-info/PKG-INFO @@ -1,13 +1,25 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: pyxattr -Version: 0.6.1 +Version: 0.7.2 Summary: Filesystem extended attributes for python Home-page: http://pyxattr.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL Download-URL: http://pyxattr.k1024.org/downloads/ +Project-URL: Bug Tracker, https://github.com/iustin/pyxattr/issues Description: This is a C extension module for Python which implements extended attributes manipulation. It is a wrapper on top of the attr C library - see attr(5). Platform: Linux +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX :: Linux +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Filesystems +Requires-Python: >=3.4 diff --git a/pyxattr.egg-info/SOURCES.txt b/pyxattr.egg-info/SOURCES.txt index 55eaa50..f9162da 100644 --- a/pyxattr.egg-info/SOURCES.txt +++ b/pyxattr.egg-info/SOURCES.txt @@ -2,17 +2,16 @@ COPYING MANIFEST.in Makefile NEWS -README.rst +README.md setup.cfg setup.py xattr.c doc/conf.py doc/index.rst doc/module.rst -doc/news.rst pyxattr.egg-info/PKG-INFO pyxattr.egg-info/SOURCES.txt pyxattr.egg-info/dependency_links.txt pyxattr.egg-info/top_level.txt -test/__init__.py -test/test_xattr.py \ No newline at end of file +tests/__init__.py +tests/test_xattr.py \ No newline at end of file diff --git a/setup.py b/setup.py index 4cd944b..ac5c82b 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python3 import distutils import platform @@ -10,7 +10,7 @@ except ImportError: long_desc = """This is a C extension module for Python which implements extended attributes manipulation. It is a wrapper on top of the attr C library - see attr(5).""" -version = "0.6.1" +version = "0.7.2" author = "Iustin Pop" author_email = "iustin@k1024.org" libraries = [] @@ -33,6 +33,21 @@ setup(name = "pyxattr", define_macros=macros, extra_compile_args=["-Wall", "-Werror", "-Wsign-compare"], )], - test_suite = "test", platforms = ["Linux"], + python_requires = ">=3.4", + project_urls={ + "Bug Tracker": "https://github.com/iustin/pyxattr/issues", + }, + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Filesystems", + ] ) diff --git a/test/test_xattr.py b/test/test_xattr.py deleted file mode 100644 index 7b14eec..0000000 --- a/test/test_xattr.py +++ /dev/null @@ -1,463 +0,0 @@ -# -# - -import sys -import unittest -import tempfile -import os -import errno - -import xattr -from xattr import NS_USER, XATTR_CREATE, XATTR_REPLACE - -NAMESPACE = os.environ.get("NAMESPACE", NS_USER) - -if sys.hexversion >= 0x03000000: - PY3K = True - EMPTY_NS = bytes() -else: - PY3K = False - EMPTY_NS = '' - -TEST_DIR = os.environ.get("TEST_DIR", ".") -TEST_IGNORE_XATTRS = os.environ.get("TEST_IGNORE_XATTRS", "") -if TEST_IGNORE_XATTRS == "": - TEST_IGNORE_XATTRS = [] -else: - TEST_IGNORE_XATTRS = TEST_IGNORE_XATTRS.split(",") - # The following has to be a list comprehension, not a generator, to - # avoid weird consequences of lazy evaluation. - TEST_IGNORE_XATTRS.extend([a.encode() for a in TEST_IGNORE_XATTRS]) - -class xattrTest(unittest.TestCase): - USER_NN = "test" - USER_ATTR = NAMESPACE.decode() + "." + USER_NN - USER_VAL = "abc" - EMPTY_VAL = "" - LARGE_VAL = "x" * 2048 - MANYOPS_COUNT = 131072 - - if PY3K: - USER_NN = USER_NN.encode() - USER_VAL = USER_VAL.encode() - USER_ATTR = USER_ATTR.encode() - EMPTY_VAL = EMPTY_VAL.encode() - LARGE_VAL = LARGE_VAL.encode() - - @staticmethod - def _ignore_tuples(attrs): - """Remove ignored attributes from the output of xattr.get_all.""" - return [attr for attr in attrs - if attr[0] not in TEST_IGNORE_XATTRS] - - @staticmethod - def _ignore(attrs): - """Remove ignored attributes from the output of xattr.list""" - return [attr for attr in attrs - if attr not in TEST_IGNORE_XATTRS] - - def checkList(self, attrs, value): - """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" - self.assertEqual(self._ignore(attrs), value) - - def checkTuples(self, attrs, value): - """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" - self.assertEqual(self._ignore_tuples(attrs), value) - - def setUp(self): - """set up function""" - self.rmfiles = [] - self.rmdirs = [] - - def tearDown(self): - """tear down function""" - for fname in self.rmfiles: - try: - os.unlink(fname) - except EnvironmentError: - continue - for dname in self.rmdirs: - try: - os.rmdir(dname) - except EnvironmentError: - continue - - def _getfile(self): - """create a temp file""" - fh, fname = tempfile.mkstemp(".test", "xattr-", TEST_DIR) - self.rmfiles.append(fname) - return fh, fname - - def _getdir(self): - """create a temp dir""" - dname = tempfile.mkdtemp(".test", "xattr-", TEST_DIR) - self.rmdirs.append(dname) - return dname - - def _getsymlink(self, dangling=True): - """create a symlink""" - fh, fname = self._getfile() - os.close(fh) - if dangling: - os.unlink(fname) - sname = fname + ".symlink" - os.symlink(fname, sname) - self.rmfiles.append(sname) - return fname, sname - - def _checkDeprecated(self, item, symlink=False): - """check deprecated list, set, get operations against an item""" - self.checkList(xattr.listxattr(item, symlink), []) - self.assertRaises(EnvironmentError, xattr.setxattr, item, - self.USER_ATTR, self.USER_VAL, - XATTR_REPLACE, symlink) - try: - xattr.setxattr(item, self.USER_ATTR, self.USER_VAL, 0, symlink) - except IOError: - err = sys.exc_info()[1] - if symlink and (err.errno == errno.EPERM or - err.errno == errno.ENOENT): - # symlinks may fail, in which case we abort the rest - # of the test for this case (Linux returns EPERM; OS X - # returns ENOENT) - return - raise - self.assertRaises(EnvironmentError, xattr.setxattr, item, - self.USER_ATTR, self.USER_VAL, XATTR_CREATE, symlink) - self.checkList(xattr.listxattr(item, symlink), [self.USER_ATTR]) - self.assertEqual(xattr.getxattr(item, self.USER_ATTR, symlink), - self.USER_VAL) - self.checkTuples(xattr.get_all(item, nofollow=symlink), - [(self.USER_ATTR, self.USER_VAL)]) - xattr.removexattr(item, self.USER_ATTR, symlink) - self.checkList(xattr.listxattr(item, symlink), []) - self.checkTuples(xattr.get_all(item, nofollow=symlink), - []) - self.assertRaises(EnvironmentError, xattr.removexattr, - item, self.USER_ATTR, symlink) - - def _checkListSetGet(self, item, symlink=False, use_ns=False): - """check list, set, get operations against an item""" - self.checkList(xattr.list(item, symlink), []) - self.assertRaises(EnvironmentError, xattr.set, item, - self.USER_ATTR, self.USER_VAL, - flags=XATTR_REPLACE, - nofollow=symlink) - self.assertRaises(EnvironmentError, xattr.set, item, - self.USER_NN, self.USER_VAL, - flags=XATTR_REPLACE, - namespace=NAMESPACE, - nofollow=symlink) - try: - if use_ns: - xattr.set(item, self.USER_NN, self.USER_VAL, - namespace=NAMESPACE, - nofollow=symlink) - else: - xattr.set(item, self.USER_ATTR, self.USER_VAL, - nofollow=symlink) - except IOError: - err = sys.exc_info()[1] - if symlink and (err.errno == errno.EPERM or - err.errno == errno.ENOENT): - # symlinks may fail, in which case we abort the rest - # of the test for this case (Linux returns EPERM; OS X - # returns ENOENT) - return - raise - self.assertRaises(EnvironmentError, xattr.set, item, - self.USER_ATTR, self.USER_VAL, - flags=XATTR_CREATE, - nofollow=symlink) - self.assertRaises(EnvironmentError, xattr.set, item, - self.USER_NN, self.USER_VAL, - flags=XATTR_CREATE, - namespace=NAMESPACE, - nofollow=symlink) - self.checkList(xattr.list(item, nofollow=symlink), [self.USER_ATTR]) - self.checkList(xattr.list(item, nofollow=symlink, - namespace=EMPTY_NS), - [self.USER_ATTR]) - self.assertEqual(xattr.list(item, namespace=NAMESPACE, nofollow=symlink), - [self.USER_NN]) - self.assertEqual(xattr.get(item, self.USER_ATTR, nofollow=symlink), - self.USER_VAL) - self.assertEqual(xattr.get(item, self.USER_NN, nofollow=symlink, - namespace=NAMESPACE), self.USER_VAL) - self.checkTuples(xattr.get_all(item, nofollow=symlink), - [(self.USER_ATTR, self.USER_VAL)]) - self.assertEqual(xattr.get_all(item, nofollow=symlink, - namespace=NAMESPACE), - [(self.USER_NN, self.USER_VAL)]) - if use_ns: - xattr.remove(item, self.USER_NN, namespace=NAMESPACE, nofollow=symlink) - else: - xattr.remove(item, self.USER_ATTR, nofollow=symlink) - self.checkList(xattr.list(item, nofollow=symlink), []) - self.checkTuples(xattr.get_all(item, nofollow=symlink), - []) - self.assertRaises(EnvironmentError, xattr.remove, - item, self.USER_ATTR, nofollow=symlink) - self.assertRaises(EnvironmentError, xattr.remove, item, - self.USER_NN, namespace=NAMESPACE, nofollow=symlink) - - def testNoXattrDeprecated(self): - """test no attributes (deprecated functions)""" - fh, fname = self._getfile() - self.checkList(xattr.listxattr(fname), []) - self.checkTuples(xattr.get_all(fname), []) - self.assertRaises(EnvironmentError, xattr.getxattr, fname, - self.USER_ATTR) - dname = self._getdir() - self.checkList(xattr.listxattr(dname), []) - self.checkTuples(xattr.get_all(dname), []) - self.assertRaises(EnvironmentError, xattr.getxattr, dname, - self.USER_ATTR) - _, sname = self._getsymlink() - self.checkList(xattr.listxattr(sname, True), []) - self.checkTuples(xattr.get_all(sname, nofollow=True), []) - self.assertRaises(EnvironmentError, xattr.getxattr, fname, - self.USER_ATTR, True) - - - def testNoXattr(self): - """test no attributes""" - fh, fname = self._getfile() - self.checkList(xattr.list(fname), []) - self.assertEqual(xattr.list(fname, namespace=NAMESPACE), []) - self.checkTuples(xattr.get_all(fname), []) - self.assertEqual(xattr.get_all(fname, namespace=NAMESPACE), []) - self.assertRaises(EnvironmentError, xattr.get, fname, - self.USER_NN, namespace=NAMESPACE) - dname = self._getdir() - self.checkList(xattr.list(dname), []) - self.assertEqual(xattr.list(dname, namespace=NAMESPACE), []) - self.checkTuples(xattr.get_all(dname), []) - self.assertEqual(xattr.get_all(dname, namespace=NAMESPACE), []) - self.assertRaises(EnvironmentError, xattr.get, dname, - self.USER_NN, namespace=NAMESPACE) - _, sname = self._getsymlink() - self.checkList(xattr.list(sname, nofollow=True), []) - self.assertEqual(xattr.list(sname, nofollow=True, - namespace=NAMESPACE), []) - self.checkTuples(xattr.get_all(sname, nofollow=True), []) - self.assertEqual(xattr.get_all(sname, nofollow=True, - namespace=NAMESPACE), []) - self.assertRaises(EnvironmentError, xattr.get, sname, - self.USER_NN, namespace=NAMESPACE, nofollow=True) - - def testFileByNameDeprecated(self): - """test set and retrieve one attribute by file name (deprecated)""" - fh, fname = self._getfile() - self._checkDeprecated(fname) - os.close(fh) - - def testFileByName(self): - """test set and retrieve one attribute by file name""" - fh, fname = self._getfile() - self._checkListSetGet(fname) - self._checkListSetGet(fname, use_ns=True) - os.close(fh) - - def testFileByDescriptorDeprecated(self): - """test file descriptor operations (deprecated functions)""" - fh, fname = self._getfile() - self._checkDeprecated(fh) - os.close(fh) - - def testFileByDescriptor(self): - """test file descriptor operations""" - fh, fname = self._getfile() - self._checkListSetGet(fh) - self._checkListSetGet(fh, use_ns=True) - os.close(fh) - - def testFileByObjectDeprecated(self): - """test file descriptor operations (deprecated functions)""" - fh, fname = self._getfile() - fo = os.fdopen(fh) - self._checkDeprecated(fo) - fo.close() - - def testFileByObject(self): - """test file descriptor operations""" - fh, fname = self._getfile() - fo = os.fdopen(fh) - self._checkListSetGet(fo) - self._checkListSetGet(fo, use_ns=True) - fo.close() - - def testMixedAccessDeprecated(self): - """test mixed access to file (deprecated functions)""" - fh, fname = self._getfile() - fo = os.fdopen(fh) - self.checkList(xattr.listxattr(fname), []) - xattr.setxattr(fname, self.USER_ATTR, self.USER_VAL) - self.checkList(xattr.listxattr(fh), [self.USER_ATTR]) - self.assertEqual(xattr.getxattr(fo, self.USER_ATTR), self.USER_VAL) - self.checkTuples(xattr.get_all(fo), [(self.USER_ATTR, self.USER_VAL)]) - self.checkTuples(xattr.get_all(fname), - [(self.USER_ATTR, self.USER_VAL)]) - fo.close() - - def testMixedAccess(self): - """test mixed access to file""" - fh, fname = self._getfile() - fo = os.fdopen(fh) - self.checkList(xattr.list(fname), []) - xattr.set(fname, self.USER_ATTR, self.USER_VAL) - self.checkList(xattr.list(fh), [self.USER_ATTR]) - self.assertEqual(xattr.list(fh, namespace=NAMESPACE), [self.USER_NN]) - self.assertEqual(xattr.get(fo, self.USER_ATTR), self.USER_VAL) - self.assertEqual(xattr.get(fo, self.USER_NN, namespace=NAMESPACE), - self.USER_VAL) - self.checkTuples(xattr.get_all(fo), - [(self.USER_ATTR, self.USER_VAL)]) - self.assertEqual(xattr.get_all(fo, namespace=NAMESPACE), - [(self.USER_NN, self.USER_VAL)]) - self.checkTuples(xattr.get_all(fname), - [(self.USER_ATTR, self.USER_VAL)]) - self.assertEqual(xattr.get_all(fname, namespace=NAMESPACE), - [(self.USER_NN, self.USER_VAL)]) - fo.close() - - def testDirOpsDeprecated(self): - """test attribute setting on directories (deprecated functions)""" - dname = self._getdir() - self._checkDeprecated(dname) - - def testDirOps(self): - """test attribute setting on directories""" - dname = self._getdir() - self._checkListSetGet(dname) - self._checkListSetGet(dname, use_ns=True) - - def testSymlinkOpsDeprecated(self): - """test symlink operations (deprecated functions)""" - _, sname = self._getsymlink() - self.assertRaises(EnvironmentError, xattr.listxattr, sname) - self._checkDeprecated(sname, symlink=True) - target, sname = self._getsymlink(dangling=False) - xattr.setxattr(target, self.USER_ATTR, self.USER_VAL) - self.checkList(xattr.listxattr(target), [self.USER_ATTR]) - self.checkList(xattr.listxattr(sname, True), []) - self.assertRaises(EnvironmentError, xattr.removexattr, sname, - self.USER_ATTR, True) - xattr.removexattr(sname, self.USER_ATTR, False) - - def testSymlinkOps(self): - """test symlink operations""" - _, sname = self._getsymlink() - self.assertRaises(EnvironmentError, xattr.list, sname) - self._checkListSetGet(sname, symlink=True) - self._checkListSetGet(sname, symlink=True, use_ns=True) - target, sname = self._getsymlink(dangling=False) - xattr.set(target, self.USER_ATTR, self.USER_VAL) - self.checkList(xattr.list(target), [self.USER_ATTR]) - self.checkList(xattr.list(sname, nofollow=True), []) - self.assertRaises(EnvironmentError, xattr.remove, sname, - self.USER_ATTR, nofollow=True) - xattr.remove(sname, self.USER_ATTR, nofollow=False) - - def testBinaryPayloadDeprecated(self): - """test binary values (deprecated functions)""" - fh, fname = self._getfile() - os.close(fh) - BINVAL = "abc" + '\0' + "def" - if PY3K: - BINVAL = BINVAL.encode() - xattr.setxattr(fname, self.USER_ATTR, BINVAL) - self.checkList(xattr.listxattr(fname), [self.USER_ATTR]) - self.assertEqual(xattr.getxattr(fname, self.USER_ATTR), BINVAL) - self.checkTuples(xattr.get_all(fname), [(self.USER_ATTR, BINVAL)]) - xattr.removexattr(fname, self.USER_ATTR) - - def testBinaryPayload(self): - """test binary values""" - fh, fname = self._getfile() - os.close(fh) - BINVAL = "abc" + '\0' + "def" - if PY3K: - BINVAL = BINVAL.encode() - xattr.set(fname, self.USER_ATTR, BINVAL) - self.checkList(xattr.list(fname), [self.USER_ATTR]) - self.assertEqual(xattr.list(fname, namespace=NAMESPACE), [self.USER_NN]) - self.assertEqual(xattr.get(fname, self.USER_ATTR), BINVAL) - self.assertEqual(xattr.get(fname, self.USER_NN, - namespace=NAMESPACE), BINVAL) - self.checkTuples(xattr.get_all(fname), [(self.USER_ATTR, BINVAL)]) - self.assertEqual(xattr.get_all(fname, namespace=NAMESPACE), - [(self.USER_NN, BINVAL)]) - xattr.remove(fname, self.USER_ATTR) - - def testManyOpsDeprecated(self): - """test many ops (deprecated functions)""" - fh, fname = self._getfile() - xattr.setxattr(fh, self.USER_ATTR, self.USER_VAL) - VL = [self.USER_ATTR] - for i in range(self.MANYOPS_COUNT): - self.checkList(xattr.listxattr(fh), VL) - for i in range(self.MANYOPS_COUNT): - self.assertEqual(xattr.getxattr(fh, self.USER_ATTR), self.USER_VAL) - for i in range(self.MANYOPS_COUNT): - self.checkTuples(xattr.get_all(fh), - [(self.USER_ATTR, self.USER_VAL)]) - - def testManyOps(self): - """test many ops""" - fh, fname = self._getfile() - xattr.set(fh, self.USER_ATTR, self.USER_VAL) - VL = [self.USER_ATTR] - VN = [self.USER_NN] - for i in range(self.MANYOPS_COUNT): - self.checkList(xattr.list(fh), VL) - self.checkList(xattr.list(fh, namespace=EMPTY_NS), VL) - self.assertEqual(xattr.list(fh, namespace=NAMESPACE), VN) - for i in range(self.MANYOPS_COUNT): - self.assertEqual(xattr.get(fh, self.USER_ATTR), self.USER_VAL) - self.assertEqual(xattr.get(fh, self.USER_NN, namespace=NAMESPACE), - self.USER_VAL) - for i in range(self.MANYOPS_COUNT): - self.checkTuples(xattr.get_all(fh), - [(self.USER_ATTR, self.USER_VAL)]) - self.assertEqual(xattr.get_all(fh, namespace=NAMESPACE), - [(self.USER_NN, self.USER_VAL)]) - - def testNoneNamespace(self): - fh, fname = self._getfile() - self.assertRaises(TypeError, xattr.get, fh, self.USER_ATTR, - namespace=None) - - def testEmptyValue(self): - fh, fname = self._getfile() - xattr.set(fh, self.USER_ATTR, self.EMPTY_VAL) - self.assertEqual(xattr.get(fh, self.USER_ATTR), self.EMPTY_VAL) - - def testWrongCall(self): - for call in [xattr.get, - xattr.list, xattr.listxattr, - xattr.remove, xattr.removexattr, - xattr.set, xattr.setxattr, - xattr.get, xattr.getxattr]: - self.assertRaises(TypeError, call) - - def testWrongType(self): - self.assertRaises(TypeError, xattr.get, object(), self.USER_ATTR) - for call in [xattr.listxattr, xattr.list]: - self.assertRaises(TypeError, call, object()) - for call in [xattr.remove, xattr.removexattr, - xattr.get, xattr.getxattr]: - self.assertRaises(TypeError, call, object(), self.USER_ATTR) - for call in [xattr.set, xattr.setxattr]: - self.assertRaises(TypeError, call, object(), self.USER_ATTR, self.USER_VAL) - - - def testLargeAttribute(self): - fh, fname = self._getfile() - - xattr.set(fh, self.USER_ATTR, self.LARGE_VAL) - self.assertEqual(xattr.get(fh, self.USER_ATTR), self.LARGE_VAL) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/test_xattr.py b/tests/test_xattr.py new file mode 100644 index 0000000..4f3e663 --- /dev/null +++ b/tests/test_xattr.py @@ -0,0 +1,489 @@ +# +# + +import sys +import tempfile +import os +import errno +import pytest +import pathlib +import platform +import io +import contextlib + +import xattr +from xattr import NS_USER, XATTR_CREATE, XATTR_REPLACE + +NAMESPACE = os.environ.get("NAMESPACE", NS_USER) + +EMPTY_NS = bytes() + +TEST_DIR = os.environ.get("TEST_DIR", ".") +TEST_IGNORE_XATTRS = os.environ.get("TEST_IGNORE_XATTRS", "") +if TEST_IGNORE_XATTRS == "": + TEST_IGNORE_XATTRS = [] +else: + TEST_IGNORE_XATTRS = TEST_IGNORE_XATTRS.split(",") + # The following has to be a list comprehension, not a generator, to + # avoid weird consequences of lazy evaluation. + TEST_IGNORE_XATTRS.extend([a.encode() for a in TEST_IGNORE_XATTRS]) + +USER_NN = "test" +USER_ATTR = NAMESPACE.decode() + "." + USER_NN +USER_VAL = "abc" +EMPTY_VAL = "" +LARGE_VAL = "x" * 2048 +MANYOPS_COUNT = 16384 + +USER_NN = USER_NN.encode() +USER_VAL = USER_VAL.encode() +USER_ATTR = USER_ATTR.encode() +EMPTY_VAL = EMPTY_VAL.encode() +LARGE_VAL = LARGE_VAL.encode() + +# Helper functions + +def ignore_tuples(attrs): + """Remove ignored attributes from the output of xattr.get_all.""" + return [attr for attr in attrs + if attr[0] not in TEST_IGNORE_XATTRS] + +def ignore(attrs): + """Remove ignored attributes from the output of xattr.list""" + return [attr for attr in attrs + if attr not in TEST_IGNORE_XATTRS] + +def lists_equal(attrs, value): + """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" + assert ignore(attrs) == value + +def tuples_equal(attrs, value): + """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" + assert ignore_tuples(attrs) == value + +# Fixtures and helpers + +@pytest.fixture +def testdir(): + """per-test temp dir based in TEST_DIR""" + with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname: + yield dname + +def get_file(path): + fh, fname = tempfile.mkstemp(".test", "xattr-", path) + return fh, fname + +@contextlib.contextmanager +def get_file_name(path): + fh, fname = get_file(path) + os.close(fh) + yield fname + +@contextlib.contextmanager +def get_file_fd(path): + fd = get_file(path)[0] + yield fd + os.close(fd) + +@contextlib.contextmanager +def get_file_object(path): + fd = get_file(path)[0] + with os.fdopen(fd) as f: + yield f + +@contextlib.contextmanager +def get_dir(path): + yield tempfile.mkdtemp(".test", "xattr-", path) + +def get_symlink(path, dangling=True): + """create a symlink""" + fh, fname = get_file(path) + os.close(fh) + if dangling: + os.unlink(fname) + sname = fname + ".symlink" + os.symlink(fname, sname) + return fname, sname + +@contextlib.contextmanager +def get_valid_symlink(path): + yield get_symlink(path, dangling=False)[1] + +@contextlib.contextmanager +def get_dangling_symlink(path): + yield get_symlink(path, dangling=True)[1] + +@contextlib.contextmanager +def get_file_and_symlink(path): + yield get_symlink(path, dangling=False) + +@contextlib.contextmanager +def get_file_and_fobject(path): + fh, fname = get_file(path) + with os.fdopen(fh) as fo: + yield fname, fo + +# Wrappers that build upon existing values + +def as_wrapper(call, fn, closer=None): + @contextlib.contextmanager + def f(path): + with call(path) as r: + val = fn(r) + yield val + if closer is not None: + closer(val) + return f + +def as_bytes(call): + return as_wrapper(call, lambda r: r.encode()) + +def as_fspath(call): + return as_wrapper(call, pathlib.PurePath) + +def as_iostream(call): + opener = lambda f: io.open(f, "r") + closer = lambda r: r.close() + return as_wrapper(call, opener, closer) + +NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)", + strict=True) +NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'", + strict=False) + +# Note: user attributes are only allowed on files and directories, so +# we have to skip the symlinks here. See xattr(7). +ITEMS_P = [ + (get_file_name, False), + (as_bytes(get_file_name), False), + pytest.param((as_fspath(get_file_name), False), + marks=[NOT_BEFORE_36, NOT_PYPY]), + (get_file_fd, False), + (get_file_object, False), + (as_iostream(get_file_name), False), + (get_dir, False), + (as_bytes(get_dir), False), + pytest.param((as_fspath(get_dir), False), + marks=[NOT_BEFORE_36, NOT_PYPY]), + (get_valid_symlink, False), + (as_bytes(get_valid_symlink), False), + pytest.param((as_fspath(get_valid_symlink), False), + marks=[NOT_BEFORE_36, NOT_PYPY]), +] + +ITEMS_D = [ + "file name", + "file name (bytes)", + "file name (path)", + "file FD", + "file object", + "file io stream", + "directory", + "directory (bytes)", + "directory (path)", + "file via symlink", + "file via symlink (bytes)", + "file via symlink (path)", +] + +ALL_ITEMS_P = ITEMS_P + [ + (get_valid_symlink, True), + (as_bytes(get_valid_symlink), True), + (get_dangling_symlink, True), + (as_bytes(get_dangling_symlink), True), +] + +ALL_ITEMS_D = ITEMS_D + [ + "valid symlink", + "valid symlink (bytes)", + "dangling symlink", + "dangling symlink (bytes)" +] + +@pytest.fixture(params=ITEMS_P, ids=ITEMS_D) +def subject(testdir, request): + with request.param[0](testdir) as value: + yield value, request.param[1] + +@pytest.fixture(params=ALL_ITEMS_P, ids=ALL_ITEMS_D) +def any_subject(testdir, request): + with request.param[0](testdir) as value: + yield value, request.param[1] + +@pytest.fixture(params=[True, False], ids=["with namespace", "no namespace"]) +def use_ns(request): + return request.param + +@pytest.fixture(params=[True, False], ids=["dangling", "valid"]) +def use_dangling(request): + return request.param + +### Test functions + +def test_empty_value(subject): + item, nofollow = subject + xattr.set(item, USER_ATTR, EMPTY_VAL, nofollow=nofollow) + assert xattr.get(item, USER_ATTR, nofollow=nofollow) == EMPTY_VAL + +def test_large_value(subject): + item, nofollow = subject + xattr.set(item, USER_ATTR, LARGE_VAL) + assert xattr.get(item, USER_ATTR, nofollow=nofollow) == LARGE_VAL + +@pytest.mark.parametrize( + "gen", [ get_file_and_symlink, get_file_and_fobject ]) +def test_mixed_access(testdir, gen): + """test mixed access to file""" + with gen(testdir) as (a, b): + # Check empty + lists_equal(xattr.list(a), []) + lists_equal(xattr.listxattr(b), []) + + # Check value + xattr.set(a, USER_ATTR, USER_VAL) + for i in [a, b]: + # Deprecated functions + lists_equal(xattr.listxattr(i), [USER_ATTR]) + assert xattr.getxattr(i, USER_ATTR) == USER_VAL + tuples_equal(xattr.get_all(i), [(USER_ATTR, USER_VAL)]) + # Current functions + lists_equal(xattr.list(i), [USER_ATTR]) + assert xattr.list(i, namespace=NAMESPACE) == [USER_NN] + assert xattr.get(i, USER_ATTR) == USER_VAL + assert xattr.get(i, USER_NN, namespace=NAMESPACE) == USER_VAL + tuples_equal(xattr.get_all(i), + [(USER_ATTR, USER_VAL)]) + assert xattr.get_all(i, namespace=NAMESPACE) == \ + [(USER_NN, USER_VAL)] + + # Overwrite + xattr.set(b, USER_ATTR, LARGE_VAL, flags=xattr.XATTR_REPLACE) + assert xattr.get(a, USER_ATTR) == LARGE_VAL + assert xattr.getxattr(a, USER_ATTR) == LARGE_VAL + xattr.removexattr(b, USER_ATTR) + assert xattr.get_all(a, namespace=NAMESPACE) == [] + assert xattr.get_all(b, namespace=NAMESPACE) == [] + +def test_replace_on_missing(subject, use_ns): + item = subject[0] + lists_equal(xattr.list(item), []) + with pytest.raises(EnvironmentError): + if use_ns: + xattr.set(item, USER_NN, USER_VAL, flags=XATTR_REPLACE, + namespace=NAMESPACE) + else: + xattr.set(item, USER_ATTR, USER_VAL, flags=XATTR_REPLACE) + +def test_create_on_existing(subject, use_ns): + item = subject[0] + lists_equal(xattr.list(item), []) + if use_ns: + xattr.set(item, USER_NN, USER_VAL, + namespace=NAMESPACE) + else: + xattr.set(item, USER_ATTR, USER_VAL) + with pytest.raises(EnvironmentError): + if use_ns: + xattr.set(item, USER_NN, USER_VAL, + flags=XATTR_CREATE, namespace=NAMESPACE) + else: + xattr.set(item, USER_ATTR, USER_VAL, flags=XATTR_CREATE) + +def test_remove_on_missing(any_subject, use_ns): + item, nofollow = any_subject + lists_equal(xattr.list(item, nofollow=nofollow), []) + with pytest.raises(EnvironmentError): + if use_ns: + xattr.remove(item, USER_NN, namespace=NAMESPACE, + nofollow=nofollow) + else: + xattr.remove(item, USER_ATTR, nofollow=nofollow) + +def test_set_get_remove(subject, use_ns): + item = subject[0] + lists_equal(xattr.list(item), []) + if use_ns: + xattr.set(item, USER_NN, USER_VAL, + namespace=NAMESPACE) + else: + xattr.set(item, USER_ATTR, USER_VAL) + if use_ns: + assert xattr.list(item, namespace=NAMESPACE) == [USER_NN] + else: + lists_equal(xattr.list(item), [USER_ATTR]) + lists_equal(xattr.list(item, namespace=EMPTY_NS), + [USER_ATTR]) + if use_ns: + assert xattr.get(item, USER_NN, namespace=NAMESPACE) == USER_VAL + else: + assert xattr.get(item, USER_ATTR) == USER_VAL + if use_ns: + assert xattr.get_all(item, namespace=NAMESPACE) == \ + [(USER_NN, USER_VAL)] + else: + tuples_equal(xattr.get_all(item), + [(USER_ATTR, USER_VAL)]) + if use_ns: + xattr.remove(item, USER_NN, namespace=NAMESPACE) + else: + xattr.remove(item, USER_ATTR) + lists_equal(xattr.list(item), []) + tuples_equal(xattr.get_all(item), []) + +def test_replace_on_missing_deprecated(subject): + item = subject[0] + lists_equal(xattr.listxattr(item), []) + with pytest.raises(EnvironmentError): + xattr.setxattr(item, USER_ATTR, USER_VAL, XATTR_REPLACE) + +def test_create_on_existing_deprecated(subject): + item = subject[0] + lists_equal(xattr.listxattr(item), []) + xattr.setxattr(item, USER_ATTR, USER_VAL, 0) + with pytest.raises(EnvironmentError): + xattr.setxattr(item, USER_ATTR, USER_VAL, XATTR_CREATE) + +def test_remove_on_missing_deprecated(any_subject): + """check deprecated list, set, get operations against an item""" + item, nofollow = any_subject + lists_equal(xattr.listxattr(item, nofollow), []) + with pytest.raises(EnvironmentError): + xattr.removexattr(item, USER_ATTR) + +def test_set_get_remove_deprecated(subject): + """check deprecated list, set, get operations against an item""" + item = subject[0] + lists_equal(xattr.listxattr(item), []) + xattr.setxattr(item, USER_ATTR, USER_VAL, 0) + lists_equal(xattr.listxattr(item), [USER_ATTR]) + assert xattr.getxattr(item, USER_ATTR) == USER_VAL + tuples_equal(xattr.get_all(item), [(USER_ATTR, USER_VAL)]) + xattr.removexattr(item, USER_ATTR) + lists_equal(xattr.listxattr(item), []) + tuples_equal(xattr.get_all(item), []) + +def test_many_ops(subject): + """test many ops""" + item = subject[0] + xattr.set(item, USER_ATTR, USER_VAL) + VL = [USER_ATTR] + VN = [USER_NN] + for i in range(MANYOPS_COUNT): + lists_equal(xattr.list(item), VL) + lists_equal(xattr.list(item, namespace=EMPTY_NS), VL) + assert xattr.list(item, namespace=NAMESPACE) == VN + for i in range(MANYOPS_COUNT): + assert xattr.get(item, USER_ATTR) == USER_VAL + assert xattr.get(item, USER_NN, namespace=NAMESPACE) == USER_VAL + for i in range(MANYOPS_COUNT): + tuples_equal(xattr.get_all(item), + [(USER_ATTR, USER_VAL)]) + assert xattr.get_all(item, namespace=NAMESPACE) == \ + [(USER_NN, USER_VAL)] + +def test_many_ops_deprecated(subject): + """test many ops (deprecated functions)""" + item = subject[0] + xattr.setxattr(item, USER_ATTR, USER_VAL) + VL = [USER_ATTR] + for i in range(MANYOPS_COUNT): + lists_equal(xattr.listxattr(item), VL) + for i in range(MANYOPS_COUNT): + assert xattr.getxattr(item, USER_ATTR) == USER_VAL + for i in range(MANYOPS_COUNT): + tuples_equal(xattr.get_all(item), + [(USER_ATTR, USER_VAL)]) + +def test_no_attributes_deprecated(any_subject): + """test no attributes (deprecated functions)""" + item, nofollow = any_subject + lists_equal(xattr.listxattr(item, True), []) + tuples_equal(xattr.get_all(item, True), []) + with pytest.raises(EnvironmentError): + xattr.getxattr(item, USER_ATTR, True) + +def test_no_attributes(any_subject): + """test no attributes""" + item, nofollow = any_subject + lists_equal(xattr.list(item, nofollow=nofollow), []) + assert xattr.list(item, nofollow=nofollow, + namespace=NAMESPACE) == [] + tuples_equal(xattr.get_all(item, nofollow=nofollow), []) + assert xattr.get_all(item, nofollow=nofollow, + namespace=NAMESPACE) == [] + with pytest.raises(EnvironmentError): + xattr.get(item, USER_NN, nofollow=nofollow, + namespace=NAMESPACE) + +def test_binary_payload_deprecated(subject): + """test binary values (deprecated functions)""" + item = subject[0] + BINVAL = b"abc\0def" + xattr.setxattr(item, USER_ATTR, BINVAL) + lists_equal(xattr.listxattr(item), [USER_ATTR]) + assert xattr.getxattr(item, USER_ATTR) == BINVAL + tuples_equal(xattr.get_all(item), [(USER_ATTR, BINVAL)]) + xattr.removexattr(item, USER_ATTR) + +def test_binary_payload(subject): + """test binary values""" + item = subject[0] + BINVAL = b"abc\0def" + xattr.set(item, USER_ATTR, BINVAL) + lists_equal(xattr.list(item), [USER_ATTR]) + assert xattr.list(item, namespace=NAMESPACE) == [USER_NN] + assert xattr.get(item, USER_ATTR) == BINVAL + assert xattr.get(item, USER_NN, namespace=NAMESPACE) == BINVAL + tuples_equal(xattr.get_all(item), [(USER_ATTR, BINVAL)]) + assert xattr.get_all(item, namespace=NAMESPACE) == [(USER_NN, BINVAL)] + xattr.remove(item, USER_ATTR) + +def test_symlinks_user_fail(testdir, use_dangling): + _, sname = get_symlink(testdir, dangling=use_dangling) + with pytest.raises(IOError): + xattr.set(sname, USER_ATTR, USER_VAL, nofollow=True) + with pytest.raises(IOError): + xattr.set(sname, USER_NN, USER_VAL, namespace=NAMESPACE, + nofollow=True) + with pytest.raises(IOError): + xattr.setxattr(sname, USER_ATTR, USER_VAL, XATTR_CREATE, True) + +@pytest.mark.parametrize( + "call, args", [(xattr.get, [USER_ATTR]), + (xattr.list, []), + (xattr.remove, [USER_ATTR]), + (xattr.get, [USER_ATTR]), + (xattr.set, [USER_ATTR, USER_VAL])]) +def test_none_namespace(testdir, call, args): + # Don't want to use subject, since that would prevent xfail test + # on path objects (due to hiding the exception here). + f = get_file_name(testdir) + with pytest.raises(TypeError): + call(f, *args, namespace=None) + fd = get_file_fd(testdir) + with pytest.raises(TypeError): + call(fd, *args, namespace=None) + +@pytest.mark.parametrize( + "call", + [xattr.get, xattr.list, xattr.listxattr, + xattr.remove, xattr.removexattr, + xattr.set, xattr.setxattr, + xattr.get, xattr.getxattr]) +def test_wrong_call(call): + with pytest.raises(TypeError): + call() + +@pytest.mark.parametrize( + "call, args", [(xattr.get, [USER_ATTR]), + (xattr.listxattr, []), + (xattr.list, []), + (xattr.remove, [USER_ATTR]), + (xattr.removexattr, [USER_ATTR]), + (xattr.get, [USER_ATTR]), + (xattr.getxattr, [USER_ATTR]), + (xattr.set, [USER_ATTR, USER_VAL]), + (xattr.setxattr, [USER_ATTR, USER_VAL])]) +def test_wrong_argument_type(call, args): + with pytest.raises(TypeError): + call(object(), *args) diff --git a/xattr.c b/xattr.c index 0087b7e..e118f79 100644 --- a/xattr.c +++ b/xattr.c @@ -28,30 +28,10 @@ #endif #include -/* Compatibility with python 2.4 regarding python size type (PEP 353) */ -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#endif - -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#define BYTES_CHAR "y" -#define BYTES_TUPLE "yy#" -#else -#define BYTES_CHAR "s" -#define BYTES_TUPLE "ss#" -#define PyBytes_Check PyString_Check -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_FromString PyString_FromString -#endif - #define ITEM_DOC \ - ":param item: a string representing a file-name, or a file-like\n" \ - " object, or a file descriptor; this represents the file on \n" \ - " which to act\n" + ":param item: a string representing a file-name, a file-like\n" \ + " object, a file descriptor, or (in Python 3.6+) a path-like\n" \ + " object; this represents the file on which to act\n" #define NOFOLLOW_DOC \ ":param nofollow: if true and if\n" \ @@ -150,33 +130,28 @@ static int merge_ns(const char *ns, const char *name, static int convert_obj(PyObject *myobj, target_t *tgt, int nofollow) { int fd; tgt->tmp = NULL; - if(PyBytes_Check(myobj)) { - tgt->type = nofollow ? T_LINK : T_PATH; - tgt->name = PyBytes_AS_STRING(myobj); - } else if(PyUnicode_Check(myobj)) { - tgt->type = nofollow ? T_LINK : T_PATH; - tgt->tmp = \ - PyUnicode_AsEncodedString(myobj, - Py_FileSystemDefaultEncoding, -#ifdef IS_PY3K - "surrogateescape" -#else - "strict" -#endif - ); - if(tgt->tmp == NULL) - return -1; - tgt->name = PyBytes_AS_STRING(tgt->tmp); - } else if((fd = PyObject_AsFileDescriptor(myobj)) != -1) { + if((fd = PyObject_AsFileDescriptor(myobj)) != -1) { tgt->type = T_FD; tgt->fd = fd; + return 0; + } + // PyObject_AsFileDescriptor sets an error when failing, so clear + // it such that further code works; some method lookups fail if an + // error already occured when called, which breaks at least + // PyOS_FSPath (called by FSConverter). + PyErr_Clear(); + + if(PyUnicode_FSConverter(myobj, &(tgt->tmp))) { + tgt->type = nofollow ? T_LINK : T_PATH; + tgt->name = PyBytes_AS_STRING(tgt->tmp); + return 0; } else { - PyErr_SetString(PyExc_TypeError, "argument must be string or int"); + // Don't set our own exception type, since we'd ignore the + // FSConverter-generated one. tgt->type = T_PATH; tgt->name = NULL; return -1; } - return 0; } /* Combine a namespace string and an attribute name into a @@ -537,7 +512,7 @@ xattr_get(PyObject *self, PyObject *args, PyObject *keywds) static char *kwlist[] = {"item", "name", "nofollow", "namespace", NULL}; /* Parse the arguments */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oet|i" BYTES_CHAR, kwlist, + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oet|iy", kwlist, &myarg, NULL, &attrname, &nofollow, &ns)) return NULL; res = NULL; @@ -624,7 +599,7 @@ get_all(PyObject *self, PyObject *args, PyObject *keywds) int io_errno; /* Parse the arguments */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i" BYTES_CHAR, kwlist, + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|iy", kwlist, &myarg, &nofollow, &ns)) return NULL; if(convert_obj(myarg, &tgt, nofollow) < 0) @@ -665,12 +640,15 @@ get_all(PyObject *self, PyObject *args, PyObject *keywds) goto free_buf_val; } } - my_tuple = Py_BuildValue(BYTES_TUPLE, name, buf_val, nval); + my_tuple = Py_BuildValue("yy#", name, buf_val, nval); if (my_tuple == NULL) { Py_DECREF(mylist); goto free_buf_val; } - PyList_Append(mylist, my_tuple); + if(PyList_Append(mylist, my_tuple) < 0) { + Py_DECREF(mylist); + goto free_buf_val; + } Py_DECREF(my_tuple); } @@ -807,7 +785,7 @@ xattr_set(PyObject *self, PyObject *args, PyObject *keywds) "nofollow", "namespace", NULL}; /* Parse the arguments */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oetet#|ii" BYTES_CHAR, + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oetet#|iiy", kwlist, &myarg, NULL, &attrname, NULL, &buf, &bufsize_s, &flags, &nofollow, &ns)) return NULL; @@ -938,7 +916,7 @@ xattr_remove(PyObject *self, PyObject *args, PyObject *keywds) static char *kwlist[] = {"item", "name", "nofollow", "namespace", NULL}; /* Parse the arguments */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oet|i" BYTES_CHAR, kwlist, + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oet|iy", kwlist, &myarg, NULL, &attrname, &nofollow, &ns)) return NULL; @@ -1085,7 +1063,7 @@ xattr_list(PyObject *self, PyObject *args, PyObject *keywds) static char *kwlist[] = {"item", "nofollow", "namespace", NULL}; /* Parse the arguments */ - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i" BYTES_CHAR, kwlist, + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|iy", kwlist, &myarg, &nofollow, &ns)) return NULL; res = NULL; @@ -1184,7 +1162,7 @@ static char __xattr_doc__[] = \ " a :exc:`EnvironmentError`; under\n" " Linux, the following ``errno`` values are used:\n" "\n" - " - ``ENODATA`` means that the attribute name is\n invalid\n" + " - ``ENODATA`` means that the attribute name is invalid\n" " - ``ENOTSUP`` and ``EOPNOTSUPP`` mean that the filesystem does not\n" " support extended attributes, or that the namespace is invalid\n" " - ``E2BIG`` mean that the attribute value is too big\n" @@ -1197,8 +1175,6 @@ static char __xattr_doc__[] = \ "\n" ; -#ifdef IS_PY3K - static struct PyModuleDef xattrmodule = { PyModuleDef_HEAD_INIT, "xattr", @@ -1212,23 +1188,14 @@ static struct PyModuleDef xattrmodule = { PyMODINIT_FUNC PyInit_xattr(void) -#else -#define INITERROR return -void -initxattr(void) -#endif { PyObject *ns_security = NULL; PyObject *ns_system = NULL; PyObject *ns_trusted = NULL; PyObject *ns_user = NULL; -#ifdef IS_PY3K PyObject *m = PyModule_Create(&xattrmodule); -#else - PyObject *m = Py_InitModule3("xattr", xattr_methods, __xattr_doc__); -#endif if (m==NULL) - INITERROR; + return NULL; PyModule_AddStringConstant(m, "__author__", _XATTR_AUTHOR); PyModule_AddStringConstant(m, "__contact__", _XATTR_EMAIL); @@ -1262,11 +1229,7 @@ initxattr(void) goto err_out; ns_user = NULL; -#ifdef IS_PY3K return m; -#else - return; -#endif err_out: Py_XDECREF(ns_user); -- 2.39.5